orbit/js/ui/CreditsHud.js

123 lines
3.9 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';
/**
* 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();
}
}