import Phaser from '../vendor/phaser.js'; import { toColor, toCss } from '../utils/Color.js'; import { fontStack, themeColor } from '../utils/Theme.js'; import { MenuButton } from './MenuButton.js'; const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; /** * THE GAME OVER BEAT (js/scenes/GameScene.js → _playerWentDown): the player's * hull broke in combat and the run is over — a dim over the whole frame, a * large semi-transparent "GAME OVER" dead centre, and two buttons below it: * * RETURN TO MAIN MENU — back to MenuScene (the same door as the sub-bar's * last button, GameScene.returnToMenu); * CONTINUE — resume the NEWEST save in the bank, exactly the * menu's Continue (SaveManager.latest → prepareLoad); * dimmed when the bank is empty. * * The world stays FROZEN behind it (GameScene adds its flag to the update() * freeze, so the ship stays down and the hostiles hold their breath). */ export class GameOverOverlay { /** * @param {Phaser.Scene} scene the GameScene (screen-fixed UI, scrolling camera) */ constructor(scene) { this.scene = scene; const w = scene.scale.width; const h = scene.scale.height; // The dim over the whole frame — the world behind goes quiet. Clicking // it does nothing (the two buttons are the only doors). this.scrim = scene.add .rectangle(w / 2, h / 2, w, h, 0x02040a, 0) .setScrollFactor(0) .setInteractive({ useHandCursor: false, hitArea: new Phaser.Geom.Rectangle(0, 0, w, h), hitAreaCallback: (p, px, py) => Phaser.Geom.Rectangle.Contains(p, px, py), }); // THE HEADLINE — large, semi-transparent, dead centre (slightly above // true centre so the button row reads as its feet). const fam = fontStack('header', FONT_FALLBACK); const size = Math.max(54, Math.round(h * 0.085)); const titleCss = toCss(themeColor('neon2', 0xff2d6f)); this.title = scene.add .text(w / 2, h / 2 - h * 0.07, 'GAME OVER', { fontFamily: fam, fontSize: `${size}px`, color: titleCss, letterSpacing: Math.max(4, Math.round(size * 0.12)), }) .setOrigin(0.5) .setScrollFactor(0) .setAlpha(0) .setInteractive({ useHandCursor: false, hitArea: new Phaser.Geom.Rectangle(0, 0, 10, 10), hitAreaCallback: (p, px, py) => Phaser.Geom.Rectangle.Contains(p, px, py), }); // THE DOORS — RETURN TO MAIN MENU (the safe way out) and CONTINUE // (resume the newest save). Built screen-fixed (the game camera moves). this.menuBtn = new MenuButton( scene, 0, 0, 'Return to Main Menu', () => this.scene.returnToMenu(), { screenFixed: true, fontSize: 14, upper: true, paddingX: 26, paddingY: 12 }, ); this.continueBtn = new MenuButton( scene, 0, 0, 'Continue', () => this.scene.continueFromGameOver(), { screenFixed: true, fontSize: 14, upper: true, paddingX: 26, paddingY: 12 }, ); this.menuBtn.setAlpha(0); this.continueBtn.setAlpha(0); this._offResize = () => this._layout(); scene.scale.on('resize', this._offResize); this._layout(); } /** * Place the title + the two buttons for the current frame size: the * headline above centre, the button row centred below it. */ _layout() { const w = this.scene.scale.width; const h = this.scene.scale.height; this.scrim.setSize(w, h); const ha = this.scrim.input.hitArea; ha.x = 0; ha.y = 0; ha.width = w; ha.height = h; this.title.setPosition(w / 2, h / 2 - h * 0.07); const gap = 30; const mw = this.menuBtn.style.width; const cw = this.continueBtn.style.width; const bh = this.menuBtn.style.height; const rowW = mw + gap + cw; const y = h / 2 + h * 0.07 + bh * 0.5; const menuX = w / 2 - rowW / 2 + mw / 2; const contX = w / 2 + rowW / 2 - cw / 2; this.menuBtn.setPosition(menuX, y); this.continueBtn.setPosition(contX, y); } /** * Raise the beat: dim up, headline fade, buttons flicker in. * @param {object} [o] { canContinue?: boolean — the bank has a save } */ show(o = {}) { this.continueBtn.setDisabled(o.canContinue === false); this.scene.tweens.add({ targets: this.scrim, alpha: 0.62, duration: 320, ease: 'Sine.easeOut' }); this.scene.tweens.add({ targets: this.title, alpha: 0.82, // the semi-transparent headline (the user's ask — a ghost, not a slam) duration: 460, ease: 'Sine.easeOut', delay: 120, }); this.scene.tweens.add({ targets: [this.menuBtn, this.continueBtn], alpha: 1, duration: 180, ease: 'Sine.easeOut', delay: 320, }); } destroy() { this.scene.scale.off('resize', this._offResize); this.scrim.destroy(); this.title.destroy(); this.menuBtn.destroy(); this.continueBtn.destroy(); } }