// 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