orbit/dev/system-hud.test.mjs

180 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* System dossier HUD test (dev tool, run with Node — no browser):
*
* node dev/system-hud.test.mjs
*
* Runs the REAL GameScene.createSystemHud()/updateHudDecode()
* (js/scenes/GameScene.js) against the real galaxy + SystemReport data and
* a stubbed scene, then asserts the arrival-decode behavior — the same
* "decode" scramble the menu uses on the Galaxy Seed (js/utils/Decode.js):
* - the dossier renders the name first, then the data lines, in the same
* layout as before (y positions, hudEndY);
* - every line starts EMPTY;
* - the system name begins decoding before any line below it (stagger);
* - while a line decodes: length stays the target's, the revealed prefix
* is exact, the unrevealed tail comes from the decode alphabet;
* - once a line lands it never changes again;
* - at the end every line is exactly the report string and the decode
* state self-clears (hudDecode === null).
*/
import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// --- Stub just enough of Phaser for the module-level class declarations ----
const ClassStub = class {};
const PhaserStub = {
Scene: ClassStub,
Physics: { Arcade: { Sprite: ClassStub } },
GameObjects: { Sprite: ClassStub, Container: ClassStub },
Geom: { Rectangle: class {} },
Display: { Color: { ValueToColor: (v) => ({ color: parseInt(v.slice(1), 16) }) } },
Math: {
Linear: (a, b, t) => a + (b - a) * t,
FloatBetween: (a, b) => a + Math.random() * (b - a),
Angle: { Wrap: (a) => a },
Clamp: (v, lo, hi) => Math.min(hi, Math.max(lo, v)),
},
BlendModes: { ADD: 2 },
};
globalThis.window = { Phaser: PhaserStub }; // js/vendor/phaser.js reads this
// --- Load the real config (data/*.json) into the config singleton ----------
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
const fs = await import('node:fs');
const dataDir = join(__dirname, '../data');
const configData = {};
for (const f of fs.readdirSync(dataDir)) {
if (!f.endsWith('.json') || f === 'manifest.json') continue;
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
}
config.init(configData);
const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href);
const { formatSystemReport } = await import(pathToFileURL(join(__dirname, '../js/galaxy/SystemReport.js')).href);
const { DECODE_CHARS } = await import(pathToFileURL(join(__dirname, '../js/utils/Decode.js')).href);
const { GameScene } = await import(pathToFileURL(join(__dirname, '../js/scenes/GameScene.js')).href);
let failures = 0;
const check = (label, cond) => {
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
if (!cond) failures++;
};
// --- Real galaxy + real report for one system ------------------------------
const galaxy = Galaxy.create('decode-hud-test');
const rec = galaxy.currentSystem();
const content = galaxy.ensureContent(rec.id);
const report = formatSystemReport(content);
const expected = [
report.title,
report.subtitle,
...report.settlements.map((s) => s.text),
report.summary,
`seed ${galaxy.seed}`,
];
console.log(`dossier under test:\n${expected.map((s) => ' ' + s).join('\n')}\n`);
// --- Stub scene: just what createSystemHud()/updateHudDecode() touch -------
const texts = [];
class FakeText {
constructor(x, y, str) {
this.x = x;
this.y = y;
this.text = str;
this.depth = 0;
}
setText(s) { this.text = s; return this; }
setOrigin() { return this; }
setScrollFactor() { return this; }
setDepth(d) { this.depth = d; return this; }
}
const scene = {
time: { now: 0 },
add: { text: (x, y, str) => { const t = new FakeText(x, y, str); texts.push(t); return t; } },
systemRecord: rec,
systemContent: content,
galaxy,
};
GameScene.prototype.createSystemHud.call(scene);
// --- Layout: same lines, same positions as before the decode ---------------
check('one text per dossier line (name, subtitle, settlements, summary, seed)', texts.length === expected.length);
check('every line starts empty', texts.every((t) => t.text === ''));
check('all lines pinned left/top (16, 14+)', texts.every((t) => t.x === 16 && t.y >= 14));
{
// Expected y walk: title @14 (+26, it's 17px), subtitle @40 (+20),
// +2, then one +20 per line (each line steps the cursor on, seed last),
// hudEndY = final y + 6 — exactly the original layout.
let y = 14;
const ys = [y];
y += 26;
ys.push(y); // subtitle
y += 20;
y += 2;
for (let i = 0; i < report.settlements.length; i++) { ys.push(y); y += 20; }
ys.push(y); // summary
y += 20;
ys.push(y); // seed
y += 20;
check('y layout unchanged by the decode', texts.every((t, i) => t.y === ys[i]));
check('hudEndY unchanged by the decode', scene.hudEndY === y + 6);
}
check('decode state is live (t0 unanchored until the first frame)', scene.hudDecode !== null && scene.hudDecode.t0 === null);
// --- Simulate the arrival: frames at 16 ms from a 1000 ms "first frame" ----
// "Settled" is read from the scene's own flag (dec.finished is the real
// signal — a mid-decode full-string match is a coincidence, not settlement).
const T0 = 1000;
const firstNonEmpty = new Array(expected.length).fill(null);
let prefixOk = true;
let tailAlphabetOk = true;
let lengthOk = true;
let settledContractOk = true;
for (let t = T0; t <= T0 + 4000; t += 16) {
GameScene.prototype.updateHudDecode.call(scene, t);
const hd = scene.hudDecode;
texts.forEach((text, i) => {
const target = expected[i];
if (firstNonEmpty[i] === null && text.text !== '') firstNonEmpty[i] = t;
if (text.text === '') return;
if (text.text.length !== target.length) lengthOk = false;
// Once the scene has settled the line, it is final: exact, and only at
// or after the end of its decode window (dec.t0 + dec.dur).
if (hd && hd.lines[i].settled) {
if (text.text !== target || t < hd.lines[i].dec.t0 + hd.lines[i].dec.dur) settledContractOk = false;
return;
}
if (text.text === target) return; // mid-decode coincidence — passes the checks below
// Decoding: revealed prefix exact, tail from the decode alphabet.
let n = 0;
while (n < target.length && text.text[n] === target[n]) n++;
if (text.text.slice(0, n) !== target.slice(0, n)) prefixOk = false;
for (let k = n; k < text.text.length; k++) if (DECODE_CHARS.indexOf(text.text[k]) === -1) tailAlphabetOk = false;
});
}
check('first frame: every line still empty (the beat of arrival)', texts.every((t, i) => firstNonEmpty[i] === null || firstNonEmpty[i] > T0));
check('the system name starts decoding first', firstNonEmpty[0] !== null && firstNonEmpty.every((t, i) => (t ?? Infinity) >= firstNonEmpty[0]));
check('decode order is top-to-bottom', firstNonEmpty.every((t, i) => i === 0 || (t ?? Infinity) >= (firstNonEmpty[i - 1] ?? Infinity)));
check('display length stays the targets through the window', lengthOk);
check('revealed prefix always exact, tail from the decode alphabet', prefixOk && tailAlphabetOk);
check('every line lands as exactly the report string', texts.every((t, i) => t.text === expected[i]));
check('a settled line is final (exact, and only after its window)', settledContractOk);
check('decode state self-clears after arrival', scene.hudDecode === null);
check('updateHudDecode is a no-op once cleared', (() => {
const snap = texts.map((t) => t.text);
GameScene.prototype.updateHudDecode.call(scene, T0 + 5000);
return texts.every((t, i) => t.text === snap[i]);
})());
if (failures > 0) {
console.error(`\n${failures} system-hud test(s) FAILED`);
process.exit(1);
}
console.log('\nsystem-hud: all checks passed');