orbit/dev/comms-intro.mjs

160 lines
8.4 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.

/**
* Dev-only: the COMMS intro self-check — boots the game straight into
* the GameScene (a FRESH run, so the 'newGame' trigger arms OAC's
* welcome) and paints a plain-text report answering "did the ack comm
* actually happen?":
*
* 1. Did the comm ARM (trigger → queue) and PLAY (box + voice)?
* 2. Is the pause contract live (comms.paused, the ship parked)?
* 3. Is the clip muted + rolling (the video's element state)?
* 4. Does ACKNOWLEDGE actually end it (button press → finish →
* the box gone, the action free, the music volume restored)?
*
* node dev/slow-server.mjs 8080 # or any static server on the repo
* → open http://127.0.0.1:8080/dev/comms-intro.html
*
* Headless screenshot:
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/comms-intro.html" \
* out.png "document.getElementById('report').textContent.includes('SELF-CHECK')"
*/
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 { GameScene } from '../js/scenes/GameScene.js';
const data = await ConfigLoader.load();
config.init(data);
const errors = [];
const origErr = console.error.bind(console);
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
if (typeof window !== 'undefined') {
window.addEventListener('error', (e) => errors.push(String(e.message)));
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
}
const gameConfig = createGameConfig();
gameConfig.scene = [GameScene];
const game = new Phaser.Game(gameConfig);
window.game = game;
const report = document.createElement('pre');
report.id = 'report';
report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:72%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
setReport('COMMS SELF-CHECK RUNNING… (waiting for OACs welcome)');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 30000) => {
const t0 = Date.now();
for (;;) {
const v = fn();
if (v) return v;
if (Date.now() - t0 > ms) return null;
await sleep(150);
}
};
const scene = () => game.scene.getScene('GameScene');
setTimeout(async () => {
const d = [];
try {
const s = scene();
const ready = await until(() => (s && s.ship && s.comms ? s : null));
if (!ready) { setReport(['FAIL — scene never became ready']); window.__commsResult = { ok: false }; return; }
// 1) The welcome armed + played (the hub's deferred load may take a
// moment — the clip + the speech come in on demand).
const live = await until(() => (s.comms.active ? s : null));
d.push(`arm+play: active=${s.comms.active} paused=${s.comms.paused} queued=${s.comms.queued}`);
if (!live) { setReport(['FAIL — the newGame comm never came up', ...d]); window.__commsResult = { ok: false, details: d }; return; }
// The clip may take a beat to start (v4's autoplay retry — the
// browser unlocks muted autoplay on its own schedule); a real
// player has the whole voice to see it, so give it 4 s here.
const box = s.comms.box;
const clip = box?.video;
const el = clip?.video;
if (el) await until(() => el.paused === false, 4000);
const snd = s.sound;
const voiceKey = s.comms._voiceKey;
d.push(`box: ${box ? `${Math.round(box.x)},${Math.round(box.y)} ${box.W}×${box.H}` : '—'}`);
d.push(`clip: present=${!!clip} muted=${el ? el.muted : '—'} paused=${el ? el.paused : '—'} loop=${el ? el.loop : '—'} t=${el && Number.isFinite(el.currentTime) ? el.currentTime.toFixed(2) + 's' : '—'}`);
d.push(`voice: key=${voiceKey ?? '—'} playing=${voiceKey ? snd.isPlaying(voiceKey) : '—'} ducked=${s.comms._ducked}`);
d.push(`ship: paused-contract=${s.ship.body.velocity.length().toFixed(1)} state=${s.ship.state}`);
d.push(`text: "${String(box?.text?.text ?? '').slice(0, 60)}…"`);
// Per-sound ducking (the volume contract): the VOICE's own sound
// stays at FULL volume (1.0); the music's own sound drops by
// musicDuck (0.6 → 0.18); the manager's MASTER is untouched. (The
// old bug: `sound.setVolume` IS the master in v4, so the voice was
// being quieted along with the music — that is why the comms
// sounded quiet.) Asserted on currentConfig.volume — the LOGICAL
// volume; the gain-node read lags in a suspended (headless) context.
const allPlaying = typeof snd.getAllPlaying === 'function' ? snd.getAllPlaying() : [];
const vs = allPlaying.find((x) => x.key === voiceKey);
const ms = allPlaying.find((x) => typeof x.key === 'string' && x.key.startsWith('music_'));
const musicVol = config.get('music.volume', 0.6);
const cfgVol = (x) => (x && x.currentConfig && typeof x.currentConfig.volume === 'number' ? x.currentConfig.volume : null);
d.push(`duck: voice.volume=${cfgVol(vs)} (expect 1.0) music.volume=${cfgVol(ms)} (expect ${(musicVol * 0.3).toFixed(2)}) master=${snd.masterVolumeNode && snd.masterVolumeNode.gain.value} (expect 1)`);
const duckOk =
s.comms._ducked === true && // the music duck was issued
cfgVol(vs) === 1.0 && // the voice is FULL volume
(cfgVol(ms) !== null && Math.abs(cfgVol(ms) - musicVol * 0.3) < 0.001); // music ducked per-sound
const ok =
s.comms.active === true &&
s.comms.paused === true && // ack: the action holds
!!box &&
!!box.button &&
!!clip &&
!!el &&
el.muted === true && // the video is silent (the voice is the speech)
el.loop === true && // the ack clip loops until acknowledged
el.paused === false && // …and it's rolling
voiceKey !== null &&
snd.isPlaying(voiceKey) === true && // the speech is talking
duckOk &&
s.ship.body.velocity.length() < 1; // the ship is parked
// 2) ACKNOWLEDGE — press the box's button (its own pointerdown, the
// save-panel's modal idiom) and the comm must end: box gone,
// action free, the voice stopped.
const before = { paused: s.comms.paused, ducked: s.comms._ducked };
box.button.panel.emit('pointerdown');
await until(() => (s.comms.active === false ? s : null), 5000);
d.push(`ack: active=${s.comms.active} paused=${before.paused}${s.comms.paused} ducked=${before.ducked}${s.comms._ducked} voice=${voiceKey ? snd.isPlaying(voiceKey) : '—'}`);
// The duck must RESTORE: the music goes back to its own volume.
const msAfter = ms ? (snd.getAll(ms.key) ?? [])[0] : null;
d.push(`duck-restore: music.volume=${msAfter ? cfgVol(msAfter) : '—'} (expect ${musicVol})`);
const duckRestoreOk = !ms || (msAfter !== null && cfgVol(msAfter) !== null && Math.abs(cfgVol(msAfter) - musicVol) < 0.001);
// The comm must NOT replay: give the hub several frames to (wrongly)
// re-arm the same entry, then assert it stays finished.
await sleep(700);
const noReplay = s.comms.active === false && s.comms.queue.length === 0;
d.push(`no-replay: active=${s.comms.active} (expect false) queue=${s.comms.queue.length} (expect 0)`);
const ackOk = s.comms.active === false && s.comms.paused === false && s.comms._ducked === false && duckRestoreOk && (voiceKey ? snd.isPlaying(voiceKey) === false : true) && noReplay;
// 3) The ledger: the welcome is PLAYED (a reload of this run would
// not re-hear it).
d.push(`ledger: played=${JSON.stringify(Array.from(s.registry.get('commsPlayed') ?? []))}`);
const playedOk = s.registry.get('commsPlayed')?.has('oac-intro') === true;
// 4) The played filter: re-triggering arms nothing.
const armed = s.comms.trigger('newGame');
d.push(`re-arm: armed=${armed} (expect 0 — the ledger holds)`);
const rearmOk = armed === 0;
const allOk = ok && ackOk && playedOk && rearmOk;
const errLine = errors.length ? ['console errors:', ...errors.slice(0, 4)] : [];
window.__commsResult = { ok: allOk, details: [...d, ...errLine] };
setReport(['COMMS SELF-CHECK ' + (allOk ? 'PASS ✔' : 'FAIL ✘'), '', ...d, ...errLine]);
} catch (err) {
window.__commsResult = { ok: false, details: ['EXCEPTION: ' + String(err?.stack ?? err)] };
setReport(['COMMS SELF-CHECK FAIL ✘', String(err?.stack ?? err)]);
}
}, 600);