Initial commit: Siege Protocol

Inverted tower-defense browser game — deploy enemies yourself, tower auto-kills them, pocket credits, upgrade weapons. HTML + Canvas + vanilla JS, no build step.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-16 11:36:53 -04:00
commit 622a9fd170
31 changed files with 6164 additions and 0 deletions
+253
View File
@@ -0,0 +1,253 @@
// ════════════════════════════════════════════════════════════
// DEV CONSOLE — set DEV_MODE = false to disable entirely
// ════════════════════════════════════════════════════════════
const DEV_MODE = true;
(function() {
if (!DEV_MODE) return;
const el = () => document.getElementById('dev-console');
const inputEl = () => document.getElementById('dev-input');
const outputEl = () => document.getElementById('dev-output');
let history = [], histIdx = -1;
function devLog(msg, type = 'out') {
const d = outputEl();
const line = document.createElement('div');
line.className = 'dev-line ' + type;
line.textContent = msg;
d.appendChild(line);
d.scrollTop = d.scrollHeight;
}
function devClear() { outputEl().innerHTML = ''; }
// ── COMMANDS ────────────────────────────────────────────────
const COMMANDS = {
help: {
desc: 'List all commands. Usage: help [command]',
run(args) {
if (args.length) {
const c = COMMANDS[args[0]];
if (!c) return devLog(`Unknown command: ${args[0]}`, 'err');
devLog(`${args[0]}${c.desc}`, 'info');
} else {
devLog('Available commands:', 'info');
Object.entries(COMMANDS).forEach(([k,v]) => devLog(` ${k.padEnd(16)} ${v.desc}`, 'out'));
}
}
},
credits: {
desc: 'Set or add credits. Usage: credits <amount> | credits +500 | credits max',
run(args) {
if (!args.length) return devLog(`Current credits: ${G.credits}`, 'info');
const raw = args[0];
if (raw === 'max') { G.credits = 999999; }
else if (raw.startsWith('+')) { G.credits += parseInt(raw.slice(1)) || 0; }
else if (raw.startsWith('-')) { G.credits -= parseInt(raw.slice(1)) || 0; }
else { G.credits = parseInt(raw) || 0; }
G.credits = Math.max(0, Math.floor(G.credits));
updateHUD();
devLog(`Credits set to ${G.credits}`, 'ok');
}
},
hp: {
desc: 'Set tower HP. Usage: hp <amount> | hp max | hp full',
run(args) {
if (!args.length) return devLog(`Tower HP: ${G.tower.hp}/${G.tower.maxHp}`, 'info');
const raw = args[0];
const val = (raw === 'max' || raw === 'full') ? G.tower.maxHp : parseInt(raw);
G.tower.hp = Math.max(1, Math.min(G.tower.maxHp, val || 1));
updateHUD();
devLog(`Tower HP set to ${G.tower.hp}`, 'ok');
}
},
kill: {
desc: 'Kill all active enemies. Usage: kill | kill <enemyType>',
run(args) {
let killed = 0;
for (const e of G.enemies) {
if (!e.alive) continue;
if (args.length && e.defId !== args[0]) continue;
killEnemy(e, false);
killed++;
}
devLog(`Killed ${killed} enemies`, 'ok');
}
},
spawn: {
desc: 'Spawn enemies directly. Usage: spawn <id> [qty] (ids: grunt runner brute swarm phantom iceling sparkling venom titan wraith)',
run(args) {
if (!args.length) return devLog('Usage: spawn <id> [qty]', 'err');
const id = args[0];
const qty = parseInt(args[1]) || 1;
const def = ENEMY_DEFS.find(d => d.id === id);
if (!def) return devLog(`Unknown enemy: ${id}. Try: ${ENEMY_DEFS.map(d=>d.id).join(', ')}`, 'err');
for (let i = 0; i < qty; i++) openPortal(def, 1, 0);
devLog(`Spawned ${qty}× ${def.name}`, 'ok');
}
},
god: {
desc: 'Toggle tower invincibility.',
run() {
G._godMode = !G._godMode;
if (G._godMode) {
G._origBreachTower = window.breachTower;
window.breachTower = function(e) { killEnemy(e, true); };
devLog('God mode ON — tower cannot take damage', 'warn');
} else {
if (G._origBreachTower) window.breachTower = G._origBreachTower;
devLog('God mode OFF', 'ok');
}
}
},
speed: {
desc: 'Set game speed multiplier. Usage: speed <0.15> | speed 1 to reset',
run(args) {
if (!args.length) return devLog(`Current speed: ${G._speedMult || 1}×`, 'info');
const mult = parseFloat(args[0]);
if (isNaN(mult) || mult <= 0) return devLog('Invalid speed', 'err');
G._speedMult = mult;
devLog(`Speed set to ${mult}×`, 'ok');
}
},
perf: {
desc: 'Toggle perf overlay. Usage: perf on | perf off | perf toggle | perf',
run(args) {
const mode = (args[0] || '').toLowerCase();
if (!mode) return devLog(`Perf overlay: ${G._showPerfOverlay ? 'ON' : 'OFF'}`, 'info');
if (mode === 'on' || mode === '1' || mode === 'true') {
G._showPerfOverlay = true;
devLog('Perf overlay ON', 'ok');
return;
}
if (mode === 'off' || mode === '0' || mode === 'false') {
G._showPerfOverlay = false;
const overlay = document.getElementById('perf-overlay');
if (overlay) overlay.style.display = 'none';
devLog('Perf overlay OFF', 'ok');
return;
}
if (mode === 'toggle') {
G._showPerfOverlay = !G._showPerfOverlay;
if (!G._showPerfOverlay) {
const overlay = document.getElementById('perf-overlay');
if (overlay) overlay.style.display = 'none';
}
devLog(`Perf overlay ${G._showPerfOverlay ? 'ON' : 'OFF'}`, 'ok');
return;
}
devLog('Usage: perf on | perf off | perf toggle', 'err');
}
},
wave: {
desc: 'Spawn a full wave of mixed enemies. Usage: wave [count_each]',
run(args) {
const qty = parseInt(args[0]) || 3;
const ids = ['grunt','runner','brute','phantom'];
ids.forEach(id => {
const def = ENEMY_DEFS.find(d => d.id === id);
if (def) openPortal(def, qty, 0);
});
devLog(`Spawned wave: ${ids.join(', ')} ×${qty} each`, 'ok');
}
},
state: {
desc: 'Dump key game state. Usage: state | state enemies | state tower | state weapons',
run(args) {
const key = args[0] || 'summary';
if (key === 'enemies') return devLog(JSON.stringify(G.enemies.filter(e=>e.alive).map(e=>({id:e.defId,hp:e.hp,x:Math.round(e.x),y:Math.round(e.y)})),null,2), 'out');
if (key === 'tower') return devLog(JSON.stringify({hp:G.tower.hp,maxHp:G.tower.maxHp,shield:G.tower.shield,cannonAngle:Math.round(G.tower.cannonAngle*57)+'°'},null,2), 'out');
if (key === 'weapons') return devLog(JSON.stringify(G.weapons.filter(Boolean).map(w=>({id:w.defId,elements:w.elements,instanceId:w.instanceId})),null,2), 'out');
devLog(`frame:${G.frame} credits:${G.credits} score:${G.score} kills:${G.totalKills} enemies:${G.enemies.filter(e=>e.alive).length} projectiles:${G.projectiles.length}`, 'info');
}
},
clear: {
desc: 'Clear the console output.',
run() { devClear(); }
},
gameover: {
desc: 'Trigger game over screen immediately.',
run() { endGame(); devLog('Game over triggered', 'warn'); }
},
reset: {
desc: 'Restart the game (same as clicking Restart).',
run() { restartGame(); devLog('Game restarted', 'ok'); }
},
};
// ── INPUT HANDLER ────────────────────────────────────────────
function runCommand(raw) {
const trimmed = raw.trim();
if (!trimmed) return;
history.unshift(trimmed);
if (history.length > 50) history.pop();
histIdx = -1;
devLog('siege> ' + trimmed, 'info');
const parts = trimmed.split(/\s+/);
const cmd = COMMANDS[parts[0].toLowerCase()];
if (!cmd) {
devLog(`Unknown command: "${parts[0]}". Type "help" for list.`, 'err');
} else {
try { cmd.run(parts.slice(1)); }
catch(e) { devLog('Error: ' + e.message, 'err'); }
}
}
// ── KEYBOARD ─────────────────────────────────────────────────
document.addEventListener('keydown', function(e) {
if (!DEV_MODE) return;
// Tilde opens/closes
if (e.key === '`' || e.key === '~') {
e.preventDefault();
const con = el();
con.classList.toggle('open');
if (con.classList.contains('open')) {
inputEl().focus();
devLog('Dev console ready. Type "help" for commands.', 'info');
}
return;
}
// Only intercept further keys when console is open
if (!el().classList.contains('open')) return;
if (e.key === 'Enter') {
const inp = inputEl();
runCommand(inp.value);
inp.value = '';
e.preventDefault();
} else if (e.key === 'Escape') {
el().classList.remove('open');
e.preventDefault();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (histIdx < history.length - 1) histIdx++;
inputEl().value = history[histIdx] || '';
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (histIdx > 0) histIdx--;
else { histIdx = -1; inputEl().value = ''; return; }
inputEl().value = history[histIdx] || '';
}
});
})();