// BaseScene — shared contract for all Agent Arcade mini-games. // Provides score bridge to the HTML HUD, pause/resume hooks, and // a consistent lifecycle so the game bootstrap can swap scenes. export let W = window.innerWidth; export let H = window.innerHeight; /** Call before creating the Phaser game to ensure dimensions are current. */ export function refreshDimensions() { W = window.innerWidth; H = window.innerHeight; } export class BaseScene extends Phaser.Scene { score = 0; highScore = 0; lives = 3; level = 0; scoreAnimTimer; gameOverKeyListener; /** Full-screen dark backdrop controlled by the transparency slider. */ _backdrop = null; /** Ready-screen state */ _readyOverlay = null; _readyKeyListener; _readyOnStart; _wasOnReadyScreen = false; /** Timer for game-over delayed callback (cancel on shutdown to prevent leaks). */ _gameOverDelayTimer = null; /** Tracked particle emitters for cleanup on shutdown. */ activeEmitters = []; constructor(key) { super(key); } /** Safe localStorage helpers */ storageGet(key) { try { return localStorage.getItem(key); } catch { return null; } } storageSet(key, value) { try { localStorage.setItem(key, value); } catch { /* quota exceeded or disabled */ } } storageRemove(key) { try { localStorage.removeItem(key); } catch { /* ignore */ } } /** Safely destroy a Phaser game object and return null for assignment. */ destroyObj(obj) { if (obj) { try { obj.destroy(); } catch { } } return null; } /** Spawn a particle explosion, track the emitter, and auto-cleanup. */ spawnParticleExplosion(x, y, color, count, lifespan = 400) { try { const emitter = this.add.particles(x, y, 'spark', { speed: { min: 60, max: 180 }, angle: { min: 0, max: 360 }, scale: { start: 1.2, end: 0 }, lifespan, quantity: count, tint: color, emitting: false, }); emitter.setDepth(20); emitter.explode(count); this.activeEmitters.push(emitter); this.time.delayedCall(lifespan + 100, () => { const idx = this.activeEmitters.indexOf(emitter); if (idx >= 0) this.activeEmitters.splice(idx, 1); emitter.destroy(); }); } catch { // Particle system unavailable, skip } } /** Load high score for this scene from localStorage. */ loadHighScore() { // Clean up old agentBreak keys (from before rename) this.storageRemove(`agentBreak_board_${this.scene.key}`); this.storageRemove(`agentBreak_hi_${this.scene.key}`); const stored = this.storageGet(`agentArcade_hi_${this.scene.key}`); this.highScore = stored ? parseInt(stored, 10) || 0 : 0; this.gameOverShown = false; this.syncHighScoreToHUD(); } /** * Common create() setup. Call at the start of every scene's create(). * Registers pause bridge, shutdown listener, and resets shared state. */ initBase() { this.setupPauseBridge(); this.events.once('shutdown', () => this.shutdown()); this.createBackdrop(); } /** Create a full-screen dark backdrop whose alpha is controlled by the settings slider. */ createBackdrop() { const g = this.add.graphics().setDepth(-100); g.fillStyle(0x000000, 1); g.fillRect(0, 0, W, H); g.setScrollFactor(0); // Read saved transparency (1–100 → alpha 0.01–1.0) let alpha = 1; try { const saved = localStorage.getItem('agentArcade_bgTransparency'); if (saved !== null) alpha = Math.max(0.01, Math.min(1, parseInt(saved, 10) / 100)); } catch { /* ignore */ } g.setAlpha(alpha); this._backdrop = g; } /** Called by the HUD slider to update the backdrop opacity in real time. */ setBackdropAlpha(percent) { if (this._backdrop) { this._backdrop.setAlpha(Math.max(0.01, Math.min(1, percent / 100))); } } /** Save high score if current score exceeds it. */ checkHighScore() { if (this.score > this.highScore) { this.highScore = this.score; this.storageSet(`agentArcade_hi_${this.scene.key}`, String(this.highScore)); this.syncHighScoreToHUD(); } } /** Push current score into the HTML HUD element. */ syncScoreToHUD() { const el = document.getElementById('score-value'); if (el) el.textContent = String(this.score); } /** Push high score into the HTML HUD element. */ syncHighScoreToHUD() { const el = document.getElementById('hi-value'); if (el) el.textContent = String(this.highScore); } /** Push lives count into the HTML HUD element. */ syncLivesToHUD() { const el = document.getElementById('lives-value'); if (el) el.textContent = String(this.lives); } /** Push level/wave number into the HTML HUD element. */ syncLevelToHUD(value) { const el = document.getElementById('level-value'); if (el) el.textContent = String(value ?? this.level); } /** Animated score bump (count-up + pop class). */ addScore(points, worldX, worldY) { const prev = this.score; this.score += points; // Floating "+N" text at world position if (worldX !== undefined && worldY !== undefined) { const txt = this.add.text(worldX, worldY, `+${points}`, { fontFamily: '"Press Start 2P", monospace', fontSize: '14px', color: '#ffff00', stroke: '#000', strokeThickness: 3, }); txt.setOrigin(0.5, 0.5).setDepth(900); this.tweens.add({ targets: txt, y: worldY - 50, alpha: 0, duration: 800, onComplete: () => txt.destroy(), }); } // Count-up animation in HUD const el = document.getElementById('score-value'); if (!el) return; if (this.scoreAnimTimer) clearInterval(this.scoreAnimTimer); const start = prev; const end = this.score; const duration = 450; const startTime = performance.now(); this.scoreAnimTimer = window.setInterval(() => { const t = Math.min(1, (performance.now() - startTime) / duration); const ease = 1 - Math.pow(1 - t, 3); el.textContent = String(Math.round(start + (end - start) * ease)); if (t >= 1) { clearInterval(this.scoreAnimTimer); this.scoreAnimTimer = undefined; el.classList.remove('pop'); void el.offsetWidth; el.classList.add('pop'); } }, 16); this.checkHighScore(); } /** Get top 10 scores for this game from localStorage. */ getLeaderboard() { const stored = this.storageGet(`agentArcade_board_${this.scene.key}`); if (!stored) return []; try { const parsed = JSON.parse(stored); if (!Array.isArray(parsed)) return []; return parsed.filter((n) => typeof n === 'number'); } catch { return []; } } /** Add a score to the leaderboard, keep top 10, return rank (1-based, 0 = not in top 10). */ addToLeaderboard(score) { if (score <= 0) return 0; const board = this.getLeaderboard(); board.push(score); board.sort((a, b) => b - a); const trimmed = board.slice(0, 10); this.storageSet(`agentArcade_board_${this.scene.key}`, JSON.stringify(trimmed)); this.checkHighScore(); const rank = trimmed.indexOf(score) + 1; return rank <= 10 ? rank : 0; } gameOverShown = false; /** Show game over overlay with leaderboard. Call restartFn when dismissed. */ showGameOver(finalScore, restartFn) { if (this.gameOverShown) return; this.gameOverShown = true; const rank = this.addToLeaderboard(finalScore); let board = this.getLeaderboard(); // Reconcile: if stored high score isn't on the board, add it if (this.highScore > 0 && (board.length === 0 || this.highScore > board[0])) { board.push(this.highScore); board.sort((a, b) => b - a); board = board.slice(0, 10); this.storageSet(`agentArcade_board_${this.scene.key}`, JSON.stringify(board)); } const overlay = document.createElement('div'); overlay.id = 'gameover-overlay'; overlay.style.cssText = ` position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.75); pointer-events: auto; animation: fadeIn 0.4s ease-out; `; const modal = document.createElement('div'); modal.style.cssText = ` background: linear-gradient(145deg, #0d1b2a 0%, #1b2838 50%, #0d1b2a 100%); border: 2px solid rgba(255,215,0,0.4); border-radius: 20px; padding: 36px 48px; text-align: center; min-width: 460px; max-width: 540px; box-shadow: 0 0 60px rgba(255,215,0,0.15), 0 0 100px rgba(0,0,0,0.8), inset 0 1px 0 rgba(255,255,255,0.05); font-family: 'Press Start 2P', 'SF Mono', monospace; animation: scaleIn 0.3s ease-out; `; // Title const title = document.createElement('h2'); title.textContent = 'GAME OVER'; title.style.cssText = ` color: #ff4444; font-size: 28px; margin: 0 0 20px; text-shadow: 0 0 20px rgba(255,68,68,0.6), 0 0 40px rgba(255,0,0,0.3); letter-spacing: 4px; `; modal.appendChild(title); // Divider const div1 = document.createElement('div'); div1.style.cssText = 'height: 1px; background: linear-gradient(90deg, transparent, rgba(255,215,0,0.3), transparent); margin: 0 0 20px;'; modal.appendChild(div1); // Score const scoreLine = document.createElement('p'); scoreLine.innerHTML = `YOUR SCORE
${finalScore.toLocaleString()}`; scoreLine.style.cssText = 'color: #8899aa; font-size: 10px; margin: 0 0 12px; letter-spacing: 2px; line-height: 2.2;'; modal.appendChild(scoreLine); // Rank badge if (rank === 1) { const badge = document.createElement('div'); badge.innerHTML = '🏆 NEW HIGH SCORE!'; badge.style.cssText = ` color: #ffd700; font-size: 13px; margin: 8px 0 16px; padding: 8px 16px; border-radius: 8px; background: rgba(255,215,0,0.1); border: 1px solid rgba(255,215,0,0.3); display: inline-block; text-shadow: 0 0 8px rgba(255,215,0,0.4); `; modal.appendChild(badge); } else if (rank > 0) { const badge = document.createElement('div'); badge.textContent = `#${rank} ON LEADERBOARD`; badge.style.cssText = ` color: #4fc3f7; font-size: 11px; margin: 8px 0 16px; padding: 6px 14px; border-radius: 8px; background: rgba(79,195,247,0.1); border: 1px solid rgba(79,195,247,0.2); display: inline-block; `; modal.appendChild(badge); } else { const spacer = document.createElement('div'); spacer.style.cssText = 'height: 12px;'; modal.appendChild(spacer); } // Divider const div2 = document.createElement('div'); div2.style.cssText = 'height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); margin: 12px 0 16px;'; modal.appendChild(div2); // Leaderboard header const boardTitle = document.createElement('p'); boardTitle.textContent = '─── TOP 10 ───'; boardTitle.style.cssText = 'color: #667; font-size: 9px; margin: 0 0 10px; letter-spacing: 3px;'; modal.appendChild(boardTitle); // Score list const table = document.createElement('div'); table.style.cssText = 'margin: 0 auto; display: inline-block; width: 100%;'; board.forEach((s, i) => { const isMe = (i === rank - 1); const row = document.createElement('div'); row.style.cssText = ` display: flex; justify-content: space-between; align-items: center; font-size: 16px; padding: 8px 16px; margin: 3px 0; border-radius: 8px; background: ${isMe ? 'rgba(255,235,59,0.12)' : (i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent')}; ${isMe ? 'border: 1px solid rgba(255,235,59,0.25);' : ''} `; const rankEl = document.createElement('span'); const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`; rankEl.textContent = medal; rankEl.style.cssText = ` color: ${isMe ? '#ffeb3b' : '#778'}; min-width: 42px; text-align: left; font-size: ${i < 3 ? '20px' : '16px'}; `; const scoreEl = document.createElement('span'); scoreEl.textContent = s.toLocaleString(); scoreEl.style.cssText = ` color: ${isMe ? '#ffeb3b' : '#bcc'}; font-size: ${i < 3 ? '20px' : '16px'}; font-weight: ${i < 3 ? '900' : '700'}; ${isMe ? 'text-shadow: 0 0 10px rgba(255,235,59,0.5);' : ''} `; if (isMe) { const youTag = document.createElement('span'); youTag.textContent = '◄'; youTag.style.cssText = 'color: #ffeb3b; font-size: 10px; margin-left: 6px;'; scoreEl.appendChild(youTag); } row.appendChild(rankEl); row.appendChild(scoreEl); table.appendChild(row); }); // Fill empty slots for (let i = board.length; i < 10; i++) { const row = document.createElement('div'); row.style.cssText = ` display: flex; justify-content: space-between; font-size: 16px; padding: 8px 16px; margin: 3px 0; color: #334; `; row.innerHTML = `${i + 1}.---`; table.appendChild(row); } modal.appendChild(table); // Restart button — matches .help-close style from settings/help dialogs const restartBtn = document.createElement('button'); restartBtn.textContent = 'RESTART'; restartBtn.style.cssText = ` display: block; margin: 22px auto 0; width: 100%; padding: 9px; background: linear-gradient(180deg, #ffd54a 0%, #c9a020 100%); border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 8px; color: #1a1a1a; font-weight: 700; letter-spacing: 1px; font-size: 13px; cursor: pointer; transition: filter 120ms; `; restartBtn.addEventListener('mouseenter', () => { restartBtn.style.filter = 'brightness(1.15)'; }); restartBtn.addEventListener('mouseleave', () => { restartBtn.style.filter = ''; }); modal.appendChild(restartBtn); overlay.appendChild(modal); document.body.appendChild(overlay); // Disable click-through so the overlay is interactive const ti = window.__TAURI_INTERNALS__; if (ti) ti.invoke('set_click_through', { enabled: false }); const dismiss = () => { this.gameOverShown = false; document.removeEventListener('keydown', onKey); overlay.remove(); // Re-enable click-through if (ti) ti.invoke('set_click_through', { enabled: true }); restartFn(); }; const onKey = (ev) => { if (ev.code === 'Space' || ev.code === 'Enter') { ev.preventDefault(); dismiss(); } }; this.gameOverKeyListener = onKey; // Brief delay before accepting input (prevent accidental dismiss). // Guard against the scene being stopped during the delay. this._gameOverDelayTimer = this.time.delayedCall(500, () => { if (!this.scene.isActive()) return; document.addEventListener('keydown', onKey); restartBtn.addEventListener('click', dismiss); }); } // ── Ready screen ─────────────────────────────────────────────────────────── /** * Freeze the scene and show the "Press any key to start" screen. * Call as the LAST statement in every scene's create() so all game objects * exist but nothing moves until the player is ready. * @param onStart Optional callback invoked the moment the player presses a * key and the scene resumes — use this to defer first-wave setup so it * doesn't render on top of the ready screen. */ startWithReadyScreen(onStart) { this._readyOnStart = onStart; this.scene.pause(); this.sound.stopAll(); // stop any sounds that fired during create() this._showPressAnyKey(); } _showPressAnyKey() { this._cleanupReadyScreen(); if (!document.getElementById('ready-screen-style')) { const style = document.createElement('style'); style.id = 'ready-screen-style'; style.textContent = ` @keyframes readyBlink { 0%,100%{opacity:1} 50%{opacity:.3} } @keyframes readyGlow { 0%,100%{text-shadow:0 0 10px rgba(0,200,255,0.6),0 0 30px rgba(0,200,255,0.3)} 50%{text-shadow:0 0 20px rgba(0,200,255,0.9),0 0 50px rgba(0,200,255,0.5),0 0 80px rgba(0,100,255,0.2)} } @keyframes titleShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} } @keyframes titleFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} } @keyframes neonPulse { 0%,100%{filter:drop-shadow(0 0 15px rgba(0,255,136,0.8)) drop-shadow(0 0 40px rgba(0,255,136,0.4)) drop-shadow(0 0 80px rgba(0,255,136,0.2))} 50%{filter:drop-shadow(0 0 25px rgba(0,255,136,1)) drop-shadow(0 0 60px rgba(0,255,136,0.6)) drop-shadow(0 0 120px rgba(0,255,136,0.3))} } @keyframes dividerPulse { 0%,100%{opacity:0.6;width:280px} 50%{opacity:1;width:360px} } @keyframes fadeSlideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} } @keyframes starTwinkle { 0%,100%{opacity:0.2} 50%{opacity:1} } `; document.head.appendChild(style); } const overlay = document.createElement('div'); overlay.id = 'ready-overlay'; overlay.style.cssText = ` position:fixed;inset:0;z-index:8000;pointer-events:none; display:flex;flex-direction:column;align-items:center;justify-content:center; background:radial-gradient(ellipse at 50% 40%,rgba(0,15,60,0.80) 0%,rgba(0,5,20,0.92) 60%,rgba(0,0,0,0.95) 100%); `; // Decorative star particles for (let i = 0; i < 40; i++) { const star = document.createElement('div'); const size = Math.random() < 0.3 ? 3 : 2; const x = Math.random() * 100; const y = Math.random() * 100; const delay = Math.random() * 3; const dur = 1.5 + Math.random() * 2; star.style.cssText = ` position:absolute;left:${x}%;top:${y}%;width:${size}px;height:${size}px; background:#fff;border-radius:50%; animation:starTwinkle ${dur}s ease-in-out ${delay}s infinite; opacity:0.3; `; overlay.appendChild(star); } // Main content wrapper — styled panel matching the game-over dialog const content = document.createElement('div'); content.style.cssText = ` display:flex;flex-direction:column;align-items:center; animation:fadeSlideUp 0.6s ease-out both; position:relative;z-index:1; background:linear-gradient(145deg,#0d1b2a 0%,#1b2838 50%,#0d1b2a 100%); border:2px solid rgba(0,200,255,0.25); border-radius:20px;padding:42px 56px; box-shadow:0 0 60px rgba(0,200,255,0.1),0 0 100px rgba(0,0,0,0.8),inset 0 1px 0 rgba(255,255,255,0.05); max-width:700px; `; const title = document.createElement('div'); title.textContent = this.displayName.toUpperCase(); title.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:48px;letter-spacing:6px; -webkit-text-stroke:2px rgba(0,255,136,0.3); background:linear-gradient(90deg,#00ff88,#ffffff,#00ff88,#ffffff,#00ff88); background-size:200% auto; -webkit-background-clip:text;-webkit-text-fill-color:transparent; background-clip:text; animation:titleShimmer 8s linear infinite,titleFloat 4s ease-in-out infinite,neonPulse 3s ease-in-out infinite; margin-bottom:22px; `; const divider = document.createElement('div'); divider.style.cssText = ` width:320px;height:2px;margin-bottom:20px; background:linear-gradient(90deg,transparent 0%,#00c8ff 20%,#ff6b35 50%,#00c8ff 80%,transparent 100%); border-radius:1px;box-shadow:0 0 12px rgba(0,200,255,0.4); animation:dividerPulse 3s ease-in-out infinite; `; const prompt = document.createElement('div'); prompt.textContent = 'PRESS ANY KEY TO START'; prompt.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:16px;letter-spacing:4px; color:#fff; animation:readyBlink 1.4s ease-in-out infinite,readyGlow 2s ease-in-out infinite; text-shadow:0 0 15px rgba(0,200,255,0.8); `; content.appendChild(title); const desc = this.getDescription(); if (desc) { const descEl = document.createElement('div'); descEl.textContent = desc; descEl.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:14px;letter-spacing:1px; color:#d0e8ff;max-width:700px;text-align:center;line-height:2; margin-bottom:18px; text-shadow:0 0 10px rgba(150,210,255,0.4); `; content.appendChild(descEl); } content.appendChild(divider); // Show control hints if the scene provides them const controls = this.getControls(); if (controls.length > 0) { const controlsDiv = document.createElement('div'); controlsDiv.style.cssText = ` margin-top:24px;padding:18px 28px; background:linear-gradient(135deg,rgba(0,20,60,0.6) 0%,rgba(0,10,40,0.7) 100%); border:1px solid rgba(0,200,255,0.2); border-radius:12px;display:inline-block; box-shadow:0 4px 20px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.05); backdrop-filter:blur(4px); `; const controlsTitle = document.createElement('div'); controlsTitle.textContent = 'CONTROLS'; controlsTitle.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:13px;letter-spacing:5px; color:rgba(200,230,255,0.9);margin-bottom:16px;text-align:center; text-shadow:0 0 8px rgba(150,200,255,0.4); `; controlsDiv.appendChild(controlsTitle); for (const { key, action } of controls) { const row = document.createElement('div'); row.style.cssText = ` display:flex;justify-content:space-between;align-items:center; margin:8px 0;gap:28px; `; const keyEl = document.createElement('span'); keyEl.textContent = key; keyEl.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:15px; color:#ffd54a;background:rgba(255,213,74,0.08); padding:7px 16px;border-radius:6px;border:1px solid rgba(255,213,74,0.25); min-width:90px;text-align:center; box-shadow:0 2px 6px rgba(0,0,0,0.2),inset 0 1px 0 rgba(255,255,255,0.05); text-shadow:0 0 6px rgba(255,213,74,0.3); `; const actionEl = document.createElement('span'); actionEl.textContent = action; actionEl.style.cssText = ` font-family:'Press Start 2P',monospace;font-size:14px; color:#d0dde8;text-align:left; `; row.appendChild(keyEl); row.appendChild(actionEl); controlsDiv.appendChild(row); } content.appendChild(controlsDiv); } prompt.style.cssText += 'margin-top:32px;'; content.appendChild(prompt); overlay.appendChild(content); document.body.appendChild(overlay); this._readyOverlay = overlay; const onKey = (e) => { if (['Meta', 'Alt', 'Control', 'Shift'].includes(e.key)) return; document.removeEventListener('keydown', onKey); this._readyKeyListener = undefined; this._cleanupReadyScreen(); if (e.key === 'Escape') { // Let the normal pause system take over; re-show ready screen on resume this._wasOnReadyScreen = true; return; } e.preventDefault(); this.scene.resume(); this._fireReadyOnStart(); }; this._readyKeyListener = onKey; document.addEventListener('keydown', onKey); } _cleanupReadyScreen() { if (this._readyOverlay) { this._readyOverlay.remove(); this._readyOverlay = null; } if (this._readyKeyListener) { document.removeEventListener('keydown', this._readyKeyListener); this._readyKeyListener = undefined; } } _fireReadyOnStart() { if (this._readyOnStart) { const fn = this._readyOnStart; this._readyOnStart = undefined; fn(); } } /** Called by the pause system. Override if the scene needs custom cleanup. */ pauseGame() { this.scene.pause(); this.sound.pauseAll(); } /** Called by the resume system. Override if needed. */ resumeGame() { if (this._wasOnReadyScreen) { // Re-show the ready screen instead of resuming gameplay this._wasOnReadyScreen = false; this._showPressAnyKey(); return; } this.scene.resume(); this.sound.resumeAll(); this._fireReadyOnStart(); } /** * Wire up the pause/resume bridge between the HUD and the Phaser scene. * Call from create() — replaces the per-scene boilerplate that was duplicated * in every scene previously. */ setupPauseBridge() { // __agentArcadePauseScene: pauses/resumes the Phaser scene ONLY (no Rust call). // Used by Rust-originated pause/resume to avoid feedback loops. window.__agentArcadePauseScene = (shouldPause) => { if (shouldPause) this.pauseGame(); else this.resumeGame(); }; // __agentArcadePause: called from in-page UI (HUD buttons, game-switcher). // Pauses scene AND notifies Rust to shrink/expand window. window.__agentArcadePause = (shouldPause) => { const ab = window.agentArcade; if (shouldPause) this.pauseGame(); else this.resumeGame(); if (ab && ab.setClickThrough) ab.setClickThrough(shouldPause); if (ab && ab.setPaused) ab.setPaused(shouldPause); }; const ab = window.agentArcade; if (ab && ab.onResumeRequest) { ab.onResumeRequest(() => { const hud = document.getElementById('hud'); if (hud) hud.classList.remove('paused'); this.resumeGame(); }); } } /** * Show a "WAVE N" banner overlay — shared by space game scenes. * Auto-animates in/out and removes itself after ~2.2 seconds. */ showWaveBanner(waveNum) { const existing = document.getElementById('wave-banner'); if (existing) existing.remove(); const banner = document.createElement('div'); banner.id = 'wave-banner'; banner.style.cssText = ` position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%); padding: 12px 36px; background: linear-gradient(180deg, #1a1f3a 0%, #0a0e22 100%); border: 2px solid #ffd54a; border-radius: 12px; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08) inset, 0 6px 24px rgba(0, 0, 0, 0.7), 0 0 22px rgba(255, 213, 74, 0.45); font-family: -apple-system, system-ui, 'Helvetica Neue', sans-serif; font-size: 22px; font-weight: 700; letter-spacing: 2px; color: #ffd54a; text-shadow: 0 0 8px rgba(255, 213, 74, 0.6); z-index: 50; pointer-events: none; user-select: none; animation: waveBannerIn 0.3s ease-out; `; banner.textContent = `WAVE ${waveNum}`; document.body.appendChild(banner); if (!document.getElementById('wave-banner-style')) { const style = document.createElement('style'); style.id = 'wave-banner-style'; style.textContent = ` @keyframes waveBannerIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.85); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes waveBannerOut { from { opacity: 1; } to { opacity: 0; } } `; document.head.appendChild(style); } setTimeout(() => { banner.style.animation = 'waveBannerOut 0.6s ease-in forwards'; setTimeout(() => banner.remove(), 700); }, 1500); } /** Create the shared 'spark' texture used for particle effects. */ ensureSparkTexture() { if (this.textures.exists('spark')) return; const g = this.add.graphics(); g.fillStyle(0xffffff); g.fillCircle(4, 4, 4); g.generateTexture('spark', 8, 8); g.destroy(); } /** * Create a parallax starfield. Returns the Star array for use with updateStarfield(). * Each scene provides its own layer config (count, speed, size, alpha per layer). */ createStarfield(layers) { const stars = []; for (const l of layers) { for (let i = 0; i < l.count; i++) { const gfx = this.add.graphics(); const x = Math.random() * W; const y = Math.random() * H; gfx.fillStyle(0xffffff, l.alpha); gfx.fillCircle(0, 0, l.size); gfx.setPosition(x, y).setDepth(-9); stars.push({ x, y, speed: l.speed, size: l.size, alpha: l.alpha, gfx }); } } return stars; } /** Update parallax starfield positions (call from update). */ updateStarfield(stars, dt) { for (const s of stars) { s.y += s.speed * (dt / 1000); if (s.y > H) s.y -= H; s.gfx.setPosition(s.x, s.y); } } /** Clean up timers and listeners on scene shutdown. */ shutdown() { if (this.scoreAnimTimer) { clearInterval(this.scoreAnimTimer); this.scoreAnimTimer = undefined; } if (this.gameOverKeyListener) { document.removeEventListener('keydown', this.gameOverKeyListener); this.gameOverKeyListener = undefined; } if (this._gameOverDelayTimer) { this._gameOverDelayTimer.remove(); this._gameOverDelayTimer = null; } this._cleanupReadyScreen(); this._readyOnStart = undefined; this._wasOnReadyScreen = false; this.time.removeAllEvents(); this.activeEmitters.forEach(e => this.destroyObj(e)); this.activeEmitters = []; const overlay = document.getElementById('gameover-overlay'); if (overlay) overlay.remove(); } /** Return a one-line description for the ready screen. Override in each scene. */ getDescription() { return ''; } /** Return control hints for the ready screen. Override in each scene. */ getControls() { return []; } } //# sourceMappingURL=BaseScene.js.map