mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 05:31:27 +00:00
chore: publish from staged
This commit is contained in:
@@ -0,0 +1,785 @@
|
||||
// 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<br><span style="font-size:28px; color:#ffeb3b; text-shadow: 0 0 12px rgba(255,235,59,0.5);">${finalScore.toLocaleString()}</span>`;
|
||||
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 = `<span>${i + 1}.</span><span>---</span>`;
|
||||
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
|
||||
Reference in New Issue
Block a user