132 lines
5.5 KiB
JavaScript
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) };
|
|
});
|