orbit/js/ui/Toast.js

119 lines
3.6 KiB
JavaScript

import Phaser from '../vendor/phaser.js';
import { config } from '../config/Config.js';
import { toColor, toCss } from '../utils/Color.js';
import { fontStack } from '../utils/Theme.js';
import { CyberShape } from './CyberShape.js';
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
/**
* A TOAST — the save pop-up's feedback line ("GAME SAVED · SLOT 03",
* "SAVE FAILED · STORAGE FULL"), a slim cut-corner strip that slides in
* above the panel, holds, and dissolves.
*
* Kinds (data/save.json → toast): ok (neon) · warn (amber) · error (red) —
* each with its own color + duration.
*
* const toast = new Toast(scene, x, y);
* toast.show('GAME SAVED · SLOT 03', { kind: 'ok' });
* toast.update(time); // called by the panel
*
* Screen-fixed (scrollFactor 0 on every child — the v4 quirk).
*/
export class Toast extends Phaser.GameObjects.Container {
constructor(scene, x, y) {
super(scene, x, y);
this.scene.add.existing(this);
this.setScrollFactor(0);
this.setSize(280, 30);
const def = config.get('save.toast.ok', {});
this.accent = toColor(def.color ?? '#00e5ff');
this.holdMs = def.durationMs ?? 2400;
this.panelG = scene.add.graphics().setScrollFactor(0);
this.add(this.panelG);
this.text = scene.add
.text(0, 1, '', {
fontFamily: fontStack('body', FONT_FALLBACK),
fontSize: '11px',
color: toCss(config.get('save.colors.ink', '#eaf6ff')),
letterSpacing: 2,
})
.setOrigin(0.5)
.setScrollFactor(0);
this.add(this.text);
this.state = 'hidden'; // hidden | in | hold | out
this.t0 = 0;
}
/**
* @param {string} msg
* @param {object} [o] { kind?: 'ok'|'warn'|'error', time? }
*/
show(msg, o = {}) {
const kind = o.kind ?? 'ok';
const def = config.get(`save.toast.${kind}`, { color: '#00e5ff', durationMs: 2400 }) ?? {};
this.accent = toColor(def.color ?? '#00e5ff');
this.holdMs = def.durationMs ?? 2400;
this.text.setText(String(msg ?? '').toUpperCase());
// Size the panel to the message (measure, then paint).
const w = Math.max(120, this.text.width + 34);
this.setSize(w, 30);
this.panelG.clear();
CyberShape.draw(this.panelG, w, 30, {
notch: 8,
fill: 0x0a1322,
fillAlpha: 0.94,
stroke: this.accent,
strokeAlpha: 0.65,
lineWidth: 1.5,
});
// Accent tick on the left (the toast's signature).
this.panelG.fillStyle(this.accent, 0.9);
this.panelG.fillRect(-w / 2 + 3, -9, 3, 18);
const t = o.time ?? this.scene.time.now;
this.state = 'in';
this.t0 = t;
this.setAlpha(0);
this.setY((this.baseY ?? this.y) + 6);
}
get isOpen() {
return this.state !== 'hidden';
}
/** Driven by the panel's update(). */
update(time) {
if (this.state === 'hidden') return;
const baseY = this.baseY ?? this.y;
const p = Phaser.Math.Clamp((time - this.t0) / 170, 0, 1);
if (this.state === 'in') {
this.setAlpha(1 - (1 - p) * (1 - p));
this.setY(baseY + 6 * (1 - p));
if (p >= 1) {
this.state = 'hold';
this.t0 = time;
}
} else if (this.state === 'hold') {
if (time - this.t0 >= this.holdMs) {
this.state = 'out';
this.t0 = time;
}
} else if (this.state === 'out') {
const q = Phaser.Math.Clamp((time - this.t0) / 200, 0, 1);
this.setAlpha(1 - q);
this.setY(baseY - 4 * q);
if (q >= 1) this.state = 'hidden';
}
}
/** Park the toast at a new spot (above the panel). */
setBaseY(y) {
this.baseY = y;
if (this.state === 'hidden') this.setY(y);
}
}