622a9fd170
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>
254 lines
9.4 KiB
JavaScript
254 lines
9.4 KiB
JavaScript
// ════════════════════════════════════════════════════════════
|
||
// 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.1–5> | 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] || '';
|
||
}
|
||
});
|
||
|
||
})();
|