Appearance
Plugin Examples
Real-world plugin examples to learn from.
FPS Counter
A simple in-game FPS display.
plugin.js:
javascript
module.exports = {
name: 'FPS Counter',
version: '1.0.0',
inject: ['style.css', 'template.html', 'script.js'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show FPS', default: true },
{ id: 'position', type: 'select', options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'], default: 'top-left' },
{ id: 'color', type: 'color', label: 'Text Color', default: '#4ade80' }
]
};template.html:
html
{{#if enabled}}
<div class="fps-counter {{position}}" id="fps" style="color: {{color}}">-- FPS</div>
{{/if}}style.css:
css
.fps-counter { position: fixed; padding: 4px 8px; background: rgba(0,0,0,0.7); font: 14px monospace; z-index: 99999; }
.fps-counter.top-left { top: 10px; left: 10px; }
.fps-counter.top-right { top: 10px; right: 10px; }
.fps-counter.bottom-left { bottom: 10px; left: 10px; }
.fps-counter.bottom-right { bottom: 10px; right: 10px; }script.js:
javascript
const { enabled } = GEMSHELL_SETTINGS;
if (!enabled) return;
const el = document.getElementById('fps');
let frames = 0, last = performance.now();
(function loop() {
frames++;
const now = performance.now();
if (now - last >= 1000) {
el.textContent = frames + ' FPS';
frames = 0;
last = now;
}
requestAnimationFrame(loop);
})();Discord Webhook
Send build notifications to Discord.
plugin.js:
javascript
module.exports = {
name: 'Discord Notifier',
version: '1.0.0',
settings: [
{ id: 'webhookUrl', type: 'text', label: 'Webhook URL', placeholder: 'https://discord.com/api/webhooks/...' },
{ id: 'onSuccess', type: 'checkbox', label: 'Notify on success', default: true },
{ id: 'onError', type: 'checkbox', label: 'Notify on error', default: true }
],
async onPostBuild(context, settings) {
if (!settings.onSuccess || !settings.webhookUrl) return;
await gemshell.http.post(settings.webhookUrl, {
embeds: [{
title: 'Build Complete',
description: `**${context.config.title}** v${context.config.version}`,
color: 0x4ade80,
fields: [
{ name: 'Platform', value: `${context.platform}-${context.arch}`, inline: true }
],
timestamp: new Date().toISOString()
}]
});
},
async onBuildError(context, settings) {
if (!settings.onError || !settings.webhookUrl) return;
await gemshell.http.post(settings.webhookUrl, {
embeds: [{
title: 'Build Failed',
description: context.error,
color: 0xef4444,
timestamp: new Date().toISOString()
}]
});
}
};Watermark
Add a watermark to demo builds.
plugin.js:
javascript
module.exports = {
name: 'Watermark',
version: '1.0.0',
inject: ['style.css', 'template.html'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show Watermark', default: true },
{ id: 'text', type: 'text', label: 'Text', default: 'DEMO' },
{ id: 'opacity', type: 'slider', label: 'Opacity', min: 10, max: 100, default: 50 }
]
};template.html:
html
{{#if enabled}}
<div class="watermark" style="opacity: {{opacity}}%">{{text}}</div>
{{/if}}style.css:
css
.watermark {
position: fixed;
bottom: 20px;
right: 20px;
font: bold 24px sans-serif;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
pointer-events: none;
z-index: 99999;
}Build Counter
Track build numbers.
plugin.js:
javascript
module.exports = {
name: 'Build Counter',
version: '1.0.0',
async onPreBuild(context, settings) {
const count = gemshell.storage.get('buildCount', 0) + 1;
gemshell.storage.set('buildCount', count);
gemshell.log(`Build #${count}`);
},
async onModifyAssets(context, settings) {
const count = gemshell.storage.get('buildCount', 1);
// Add build number to game
gemshell.transform.injectIntoHtml(
`<script>window.BUILD_NUMBER = ${count};</script>`,
'head'
);
}
};JSON Minifier
Reduce JSON file sizes.
plugin.js:
javascript
module.exports = {
name: 'JSON Minifier',
version: '1.0.0',
async onModifyAssets(context, settings) {
const files = gemshell.glob('**/*.json');
let saved = 0;
for (const file of files) {
try {
const original = gemshell.fs.read(file);
const data = JSON.parse(original);
const minified = JSON.stringify(data);
if (minified.length < original.length) {
gemshell.fs.write(file, minified);
saved += original.length - minified.length;
}
} catch (e) {
gemshell.warn(`Could not minify ${file}`);
}
}
gemshell.log(`Saved ${(saved / 1024).toFixed(1)} KB`);
}
};Save Encryption
Encrypt save files.
plugin.js:
javascript
module.exports = {
name: 'Save Encryptor',
version: '1.0.0',
settings: [
{ id: 'key', type: 'text', label: 'Encryption Key', placeholder: 'Enter a secret key' },
{ id: 'pattern', type: 'text', label: 'File Pattern', default: 'saves/*.json' }
],
async onModifyAssets(context, settings) {
if (!settings.key) {
gemshell.warn('No encryption key set');
return;
}
const files = gemshell.glob(settings.pattern);
for (const file of files) {
const content = gemshell.fs.read(file);
const encrypted = gemshell.native.encrypt(settings.key, content);
if (encrypted) {
gemshell.fs.write(file + '.enc', encrypted);
gemshell.fs.remove(file);
gemshell.log(`Encrypted: ${file}`);
}
}
}
};Version Banner
Show version info in-game.
plugin.js:
javascript
module.exports = {
name: 'Version Banner',
version: '1.0.0',
inject: ['style.css', 'template.html'],
settings: [
{ id: 'enabled', type: 'checkbox', label: 'Show Version', default: true }
],
async onModifyAssets(context, settings) {
if (!settings.enabled) return;
// Store version for template
gemshell.transform.injectIntoHtml(
`<script>window.GAME_VERSION = "${context.config.version}";</script>`,
'head'
);
}
};template.html:
html
{{#if enabled}}
<div class="version-banner" id="version-banner"></div>
{{/if}}style.css:
css
.version-banner {
position: fixed;
bottom: 5px;
left: 5px;
font: 10px monospace;
color: rgba(255,255,255,0.5);
pointer-events: none;
z-index: 99999;
}script.js:
javascript
const { enabled } = GEMSHELL_SETTINGS;
if (!enabled) return;
const el = document.getElementById('version-banner');
if (el && window.GAME_VERSION) {
el.textContent = 'v' + window.GAME_VERSION;
}