orbit/dev/menu-intro-test.mjs

132 lines
5.5 KiB
JavaScript

/**
* Dev-only MenuScene intro driver (headless browser — NOT a Node test).
*
* Verifies the two-beat menu intro synced to the menu track's crescendo
* (assets/music/mainmenu.mp3 swells at ~2.2 s, data/menu.json revealDelay):
*
* beat 1 (t≈0): the console is up — corner frame, chrome text, and
* the "THE GALAXY AWAITS" subtitle + rule are visible,
* while the title, bloom, buttons, and seed panel
* are STILL DARK.
* beat 2 (t≈2200): the title flickers in (boot flicker armed), the
* bloom blooms, the buttons rise in priority order,
* the seed decodes, one signature glitch fires.
*
* Served by dev/menu-intro-test.html; results land in
* `window.__MENU_INTRO__` for the CDP runner (dev/cdp-firefox.mjs):
*
* python3 -m http.server 8080
* node dev/cdp-firefox.mjs http://localhost:8080/dev/menu-intro-test.html
*/
import Phaser from '../js/vendor/phaser.js';
import { config } from '../js/config/Config.js';
import { ConfigLoader } from '../js/config/ConfigLoader.js';
import { createGameConfig } from '../js/config/GameConfig.js';
import { MenuScene } from '../js/scenes/MenuScene.js';
const data = await ConfigLoader.load();
config.init(data);
// A quiet run (no audio files to fetch in headless).
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
const gameConfig = createGameConfig();
gameConfig.scene = [MenuScene]; // the menu boots first
const game = new Phaser.Game(gameConfig);
window.game = game;
const results = [];
const check = (label, cond) => {
const pass = !!cond;
results.push({ label, pass });
console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`);
};
// WAIT — throttling-proof. This headless box starves setTimeout and
// stretches its compositor clock, so the only honest barrier is the
// GAME'S OWN CLOCK: poll scene time.now on rAF until it has advanced
// past the base (same pattern as dev/saves-ui-test.mjs).
const gameClock = () => {
try {
const s = window.game.scene.getScenes(true)[0];
if (s && typeof s.time.now === 'number') return s.time.now;
} catch { /* not booted yet */ }
return null;
};
const wait = (ms) => new Promise((resolve) => {
const base = gameClock();
if (base === null) { // pre-boot: fall back to wall clock
const start = performance.now();
setTimeout(() => resolve(), ms);
return;
}
const poll = () => {
const now = gameClock();
if (now !== null && now - base >= ms) return resolve();
requestAnimationFrame(poll);
};
requestAnimationFrame(poll);
});
const near = (a, v) => Math.abs(a - v) < 0.02;
const run = async () => {
// ---- boot: the menu must come up --------------------------------------
const bootT0 = Date.now();
while (!game.scene.isActive('MenuScene') || !game.scene.getScene('MenuScene').title) {
if (Date.now() - bootT0 > 90000) throw new Error('MenuScene never came up');
await wait(200);
}
const scene = game.scene.getScene('MenuScene');
check('the menu boots', game.scene.isActive('MenuScene'));
// Beat flags for dev/shot-firefox.mjs (screenshot at the right moment).
window.__MENU_BEAT1__ = null;
window.__MENU_BEAT2__ = null;
const revealAt = config.get('menu.revealDelay', 3200);
// ---- beat 1: console up, title + buttons still dark --------------------
// Sample at +1000 ms: past beat 1's fade (60+450) and well before the
// reveal (revealDelay, default 3200).
await wait(1000);
window.__MENU_BEAT1__ = true;
check('beat 1: the "THE GALAXY AWAITS" subtitle is up',
scene.subtitle.alpha > 0.95);
check('beat 1: the subtitle rule is up',
scene.rule.alpha > 0.95);
check('beat 1: the main title is STILL DARK',
near(scene.title.alpha, 0) && scene.title.bootT0 === null);
check('beat 1: the title bloom is STILL DARK',
near(scene.titleBloom.main.alpha, 0) && near(scene.titleBloom.fringe.alpha, 0));
check('beat 1: the buttons are STILL DARK',
near(scene.continueBtn.alpha, 0) && near(scene.newGameBtn.alpha, 0) && near(scene.loadGameBtn.alpha, 0));
check('beat 1: the seed panel is STILL DARK',
scene.seedIntroTargets.every((t) => near(t.alpha, 0)) && near(scene.rerollBtn.alpha, 0));
// ---- beat 2: the downbeat — title, bloom, buttons, seed, decode -------
// The last beat 2 effect settles at revealAt + 540 + 620 (decode end);
// sample at revealAt + 1300 to be comfortably past everything.
await wait(revealAt + 1300 - 1000);
window.__MENU_BEAT2__ = true;
check('beat 2: the main title is up (boot flicker finished)',
near(scene.title.alpha, 1) && scene.title.bootT0 === null);
check('beat 2: the title bloom is up',
near(scene.titleBloom.main.alpha, 1) && near(scene.titleBloom.fringe.alpha, 1));
check('beat 2: the buttons are up (Continue / New Game / Load Game)',
near(scene.continueBtn.alpha, 1) && near(scene.newGameBtn.alpha, 1) && near(scene.loadGameBtn.alpha, 1));
check('beat 2: the seed panel is up',
scene.seedIntroTargets.every((t) => near(t.alpha, 1)) && near(scene.rerollBtn.alpha, 1));
check('beat 2: the seed decoded to its final value',
scene.decode === null && scene.seedText.text.startsWith(scene.seedValue));
const failed = results.filter((r) => !r.pass).length;
console.log(failed === 0 ? `MENU INTRO OK (${results.length} checks)` : `MENU INTRO FAILED (${failed}/${results.length})`);
window.__MENU_INTRO__ = { results, failed };
};
run().catch((err) => {
console.error('menu-intro-test crashed:', err);
window.__MENU_INTRO__ = { results, failed: results.length, error: String(err && err.stack || err) };
});