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'; /** * CREDITS HUD — the trade-currency readout, UPPER-RIGHT, stacked ABOVE the * minerals HUD (data/creditshud.json). One right-aligned line: * * CREDITS 1,250 * * `set(n)` counts the number up when credits land (a single tween, gen-token * guarded so quick successive updates can't snap it back). Gold (theme 'amber') * reads as value and stays distinct from the cyan minerals line below it. * * The scene stacks the two readouts: it builds this first (top), then builds * the MineralHud with `topOffset = creditsHud.height + creditsHud.gap`, so * the comms box (which anchors to the minerals bar's bottom) keeps clear of * both. * * Component usage (scenes own one instance, like MineralHud): * * this.creditsHud = new CreditsHud(this); * this.creditsHud.set(ship.credits); * // …whenever credits change: * this.creditsHud.set(ship.credits); * shutdown() { this.creditsHud.destroy(); } */ export class CreditsHud extends Phaser.GameObjects.Container { /** @param {Phaser.Scene} scene */ constructor(scene) { super(scene, 0, 0); this.scene.add.existing(this); this.setScrollFactor(0); // UI — pinned to the screen, not the world this.setDepth(30); // with the minerals HUD and the other corner readouts const hud = config.section('creditshud', {}); this.pad = hud.pad ?? 16; this.gap = Math.max(0, hud.gap ?? 8); // space the scene leaves below this line const lab = hud.label ?? {}; const cu = hud.countUp ?? {}; this.countUpMs = Math.max(0, cu.durationMs ?? 420); this.countUpEase = cu.ease ?? 'Sine.easeOut'; this.int = toColor(hud.color, 0xffc94d); const W = scene.scale.width; const right = W - this.pad; const fam = fontStack('body'); this.label = scene.add .text(0, 0, '', { fontFamily: fam, fontSize: `${lab.fontSize ?? 12}px`, color: toCss(this.int), letterSpacing: lab.letterSpacing ?? 1, }) .setOrigin(1, 0) // right edge sticks to the corner margin .setDepth(30) .setScrollFactor(0); this.label.setPosition(right, this.pad); // State: `value` is settled (where the animation lands). A set() with the // same value is a no-op (no re-count). this.value = 0; this._gen = 0; // supersede guard: a new set() voids any in-flight tween this.label.setText(this.format(0)); } /** The rendered line height (the scene uses this to stack the minerals HUD). */ get height() { return this.label.height; } /** The label text for a settled value. */ format(n) { return `CREDITS ${Math.max(0, Math.round(n)).toLocaleString('en-US')}`; } /** * Set the credit ledger: `n` credits aboard. Counts the number up from the * last settled value (so it always reads from where the player last saw it). * A no-op when unchanged. * * @returns {boolean} true when the value actually changed */ set(n) { const v = Math.max(0, Math.round(Number(n) || 0)); if (v === this.value) return false; const from = this.value; this.value = v; if (this.countUpMs <= 0) { this.label.setText(this.format(v)); return true; } const gen = ++this._gen; const live = () => gen === this._gen; const holder = { v: from }; this.scene.tweens.add({ targets: holder, v, duration: this.countUpMs, ease: this.countUpEase, onUpdate: () => { if (!live()) return; this.label.setText(this.format(holder.v)); }, onComplete: () => { if (!live()) return; this.label.setText(this.format(v)); // land exactly, whatever the clock did }, }); return true; } /** Tear the readout down (scene shutdown). */ destroy() { this.label?.destroy(); this.label = null; super.destroy(); } }