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); } }