180 lines
7.6 KiB
JavaScript
180 lines
7.6 KiB
JavaScript
/**
|
||
* 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 target’s 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');
|