orbit/js/ui/GameOverOverlay.js

142 lines
4.9 KiB
JavaScript

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);
this.scrim.hitArea.setBounds(0, 0, w, 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();
}
}