160 lines
8.4 KiB
JavaScript
160 lines
8.4 KiB
JavaScript
/**
|
||
* 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 OAC’s 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);
|