// ═══ main.js ═══ // ============================================================ // MAIN.JS — Game loop, init, HUD updates // ============================================================ const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // Fixed logical resolution — all gameplay runs at this size. // The canvas is CSS-scaled to fill the viewport on resize. const GAME_W = 1600; const GAME_H = 900; canvas.width = GAME_W; canvas.height = GAME_H; function resize() { const vw = window.innerWidth; const vh = window.innerHeight; const scale = Math.min(vw / GAME_W, vh / GAME_H); const w = Math.floor(GAME_W * scale); const h = Math.floor(GAME_H * scale); canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; canvas.style.left = Math.floor((vw - w) / 2) + 'px'; canvas.style.top = Math.floor((vh - h) / 2) + 'px'; } window.addEventListener('resize', resize); resize(); // ── HIGH SCORE ──────────────────────────────────────────────── let _best = (() => { try { return JSON.parse(localStorage.getItem('siegeprotocol_best')) || null; } catch(e) { return null; } })(); const PERF_METRICS = { lastStamp: null, fps: 60, frameMs: 16.7, updateMs: 0, renderMs: 0, sampleMs: 0, sampleFrames: 0, uiTick: 0, uiEveryFrames: 6, }; function perfNow() { return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(); } function setPaused(paused, showOverlay = true) { G.paused = paused; document.body.classList.toggle('paused', !!paused && showOverlay && !G.shopOpen && !document.body.classList.contains('inventory-open') && !G.gameOver); } function togglePause() { setPaused(!document.body.classList.contains('paused'), true); } function updatePerfOverlay(frameStamp, updateMs, renderMs) { const overlay = document.getElementById('perf-overlay'); if (!overlay) return; if (!DEV_MODE || !G._showPerfOverlay) { overlay.style.display = 'none'; return; } overlay.style.display = 'block'; const frameDelta = PERF_METRICS.lastStamp == null ? 16.7 : (frameStamp - PERF_METRICS.lastStamp); PERF_METRICS.lastStamp = frameStamp; PERF_METRICS.frameMs = PERF_METRICS.frameMs * 0.85 + frameDelta * 0.15; PERF_METRICS.updateMs = PERF_METRICS.updateMs * 0.8 + updateMs * 0.2; PERF_METRICS.renderMs = PERF_METRICS.renderMs * 0.8 + renderMs * 0.2; PERF_METRICS.sampleMs += frameDelta; PERF_METRICS.sampleFrames += 1; if (PERF_METRICS.sampleMs >= 250) { PERF_METRICS.fps = (PERF_METRICS.sampleFrames * 1000) / PERF_METRICS.sampleMs; PERF_METRICS.sampleMs = 0; PERF_METRICS.sampleFrames = 0; } PERF_METRICS.uiTick += 1; if (PERF_METRICS.uiTick < PERF_METRICS.uiEveryFrames) return; PERF_METRICS.uiTick = 0; let enemiesAlive = 0; for (const e of G.enemies) if (e.alive) enemiesAlive++; overlay.innerHTML = `
PERF
FPS${PERF_METRICS.fps.toFixed(1)}
Frame${PERF_METRICS.frameMs.toFixed(2)} ms
Logic${PERF_METRICS.updateMs.toFixed(2)} ms
Render${PERF_METRICS.renderMs.toFixed(2)} ms
Enemies${enemiesAlive}
Projectiles${G.projectiles.length}
Portals${G.portals.length}
`; } // ── GAME LOOP ───────────────────────────────────────────────── function gameLoop() { requestAnimationFrame(gameLoop); const frameStamp = perfNow(); const updateStart = frameStamp; if (!G.paused && !G.gameOver) { const ticks = (DEV_MODE && G._speedMult && G._speedMult !== 1) ? Math.max(1, Math.round(G._speedMult)) : 1; for (let t = 0; t < ticks; t++) { G.frame++; // Decay freshness every 5 seconds (300 frames) if (G.frame % 300 === 0) { for (const id in G.enemyFreshness) { if (G.enemyFreshness[id] > 0) G.enemyFreshness[id]--; } } updatePortals(); updateEnemies(); updateWeapons(); updateShield(); updateParticles(); updateFloaters(); if (G.paused || G.gameOver) break; } } checkBankruptcy(); const updateEnd = perfNow(); render(); const renderEnd = perfNow(); updatePerfOverlay(frameStamp, updateEnd - updateStart, renderEnd - updateEnd); } // ── RESERVE ─────────────────────────────────────────────────── function adjustReserve(delta) { const cheapest = cheapestEnemyCost(); G.creditReserve = clamp(G.creditReserve + delta, cheapest, G.credits); updateHUD(); } function spendableCredits() { return Math.max(0, G.credits - G.creditReserve); } // ── HUD ─────────────────────────────────────────────────────── function updateHUD() { // Clamp state — canvas renderer reads G.* directly each frame G.credits = Math.floor(Math.max(0, G.credits)); const cheapest = cheapestEnemyCost(); G.creditReserve = clamp(G.creditReserve, cheapest, G.credits); checkBankruptcy(); } // ── COMBAT LOG ──────────────────────────────────────────────── function addLog(msg, type = '') { if (!G.logLines) G.logLines = []; G.logLines.unshift({ text: msg, type }); if (G.logLines.length > 40) G.logLines.length = 40; } // ── BANKRUPTCY CHECK ────────────────────────────────────────── function checkBankruptcy() { if (G.gameOver) return; // Bankrupt = 0 credits AND no enemies alive (no pending rewards coming in) // AND can't afford even the cheapest enemy const cheapest = cheapestEnemyCost(); const activeEnemies = countAliveEnemies(); const pendingPortals = G.portals.length; const now = typeof performance !== 'undefined' ? performance.now() : Date.now(); if (G.credits < cheapest && activeEnemies === 0 && pendingPortals === 0) { // Give a brief grace window — maybe enemies just died and reward came in. if (!G._bankruptAt) G._bankruptAt = now; if (now - G._bankruptAt > 3000) { endBankrupt(); } } else { G._bankruptAt = null; } } // ── GAME OVER ───────────────────────────────────────────────── function endGame() { if (G.shopOpen) closeShop(); setPaused(false); G.gameOver = true; G.isBankrupt = false; G._isNewBest = !_best || G.score > _best.score; if (G._isNewBest) { _best = { score: G.score, kills: G.totalKills }; try { localStorage.setItem('siegeprotocol_best', JSON.stringify(_best)); } catch(e) {} } } function endBankrupt() { if (G.shopOpen) closeShop(); setPaused(false); G.gameOver = true; G.isBankrupt = true; G._isNewBest = !_best || G.score > _best.score; if (G._isNewBest) { _best = { score: G.score, kills: G.totalKills }; try { localStorage.setItem('siegeprotocol_best', JSON.stringify(_best)); } catch(e) {} } addLog('BANKRUPT. No credits, no enemies. Game over.', 'lose'); } function restartGame() { G = makeGameState(); setPaused(false); _sidePanelScrollY = 0; _logScrollY = 0; updateHUD(); addLog('System online. Deploy enemies to earn credits.', 'info'); addLog('[1–0] deploy [scroll] qty [Space] shop [Esc/P] pause [I] inventory', 'info'); } // ── INIT ────────────────────────────────────────────────────── function init() { resize(); // set canvas size after full page load — guaranteed correct dimensions updateHUD(); initInput(); addLog('SIEGE PROTOCOL initialized.', 'info'); addLog('[1–0] deploy [scroll] qty [Space] shop [Esc/P] pause [I] inventory', 'info'); gameLoop(); } window.addEventListener('load', init);