142 lines
4.9 KiB
JavaScript
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();
|
|
}
|
|
}
|