orbit/dev/comms-trestle.mjs

179 lines
9.2 KiB
JavaScript

/**
* Dev-only: exercise TRESTLE's introduction pair in the live GameScene —
* the FIRST planet the player discovers in a system that is NOT home
* (trigger 'firstWorld'):
*
* trestle-meetup → ack: box holds (muted looping video + text + voice)
* until ACKNOWLEDGE.
* trestle-quest → ack: starts the moment the meetup is ACKed (delayMs 0).
*
* The roster is overridden at boot so ONLY the pair is live (the newGame
* welcome stays out of the FIFO) — but everything else is the shipped
* code: the real character entry (his clip + his speech keys), the real
* GameScene.celebrateDiscovery arm (the scene flag + the QUEST's OWN
* witness, discoveredWorldCount), the real hub (FIFO, the ack contract,
* the ledger), the real box.
*
* The witness matrix (all through the real celebrateDiscovery, with the
* discovery ledger ticked first exactly like discovery.check does):
* 1. the HOME WORLD discovered → NO arm (it never counts)
* 2. a CLUSTER discovered → NO arm (not a world/station)
* 3. a PLANET discovered (non-home) → the pair arms, the quest witness
* reads 1 — comm and quest match
*
* Headless note: the AudioContext is suspended, so the clips never reach
* a natural end — the ACK clicks drive the flow (ack comms hold until
* ACKNOWLEDGE), which is exactly the player's path.
*
* node dev/cdp-probe.mjs "http://127.0.0.1:3000/dev/comms-trestle.html" \
* "window.__trestleResult ? JSON.stringify(window.__trestleResult) : null" 180000
*/
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();
// --- override: ONLY the trestle pair is live (the welcome stays out) ----
const shipped = data.comms;
data.comms = {
...shipped,
comms: (shipped?.comms ?? []).filter((c) => c.trigger === 'firstWorld'),
};
config.init(data);
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;margin:0;padding:8px 12px;font:12px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.9);border:1px solid #1b3a5a;max-width:55%;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = lines.join('\n'); };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 20000) => {
const t0 = Date.now();
for (;;) {
const v = fn();
if (v) return v;
if (Date.now() - t0 > ms) return null;
await sleep(120);
}
};
setTimeout(async () => {
const d = [];
const errors = [];
const origErr = console.error;
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
try {
const s = game.scene.getScene('GameScene');
const ready = await until(() => (s && s.ship && s.comms ? s : null));
if (!ready) throw new Error('scene never ready');
// The scene booted in the HOME system (isHomeSystem true). The roster
// override keeps newGame quiet — verify the deck is live + nothing armed.
await sleep(600);
const quiet0 = s.comms.active === false && s.comms.queue.length === 0;
d.push(`boot: active=${s.comms.active} (expect false) queue=${s.comms.queue.length} (expect 0)`);
// A real planet of this system (the arm's witness: the quest's
// discoveredWorldCount — any non-home planet/station, any system).
const planet = (s.systemPlanets ?? [])[0];
const cluster = (s.asteroidClusters ?? [])[0];
if (!planet) throw new Error('no system planet to discover');
const disc = (o, name, typeLabel) => ({
id: o.discoveryId,
x: o.x,
y: o.y,
radius: o.radius ?? o.bound ?? 30,
name,
typeLabel,
});
// A discovery, the way the real flow does it: discovery.check adds the
// id to the ledger FIRST, then the scene celebrates it.
const discover = (o) => {
s.discovery.bySystem.get(s.systemRecord.id)?.add(o.id);
s.celebrateDiscovery(o);
};
// ---- witness 1 — the HOME WORLD never counts ----------------------
if (s.isHomeSystem !== true) throw new Error('scene should boot in the home system');
discover({ id: 'home', x: s.planet.x, y: s.planet.y, radius: s.planet.radius, name: s.planet.discoveryName, typeLabel: 'Home World' });
await sleep(400);
const homeGuard = s.comms.active === false && s.comms.queue.length === 0 && s._firstWorldSeen === false && s.discoveredWorldCount() === 0;
d.push(`witness 1 (home world): armed=${s.comms.queue.length} (expect 0 — home never counts) flag=${s._firstWorldSeen} (expect false) count=${s.discoveredWorldCount()} (expect 0)`);
if (!homeGuard) throw new Error('the home world armed the comm');
// ---- witness 2 — a CLUSTER does not count --------------------------
if (cluster) {
discover(disc(cluster, cluster.discoveryName, 'Asteroid Cluster'));
await sleep(400);
const clusterGuard = s.comms.active === false && s.comms.queue.length === 0 && s._firstWorldSeen === false;
d.push(`witness 2 (cluster): armed=${s.comms.queue.length} (expect 0 — not a world/station) flag=${s._firstWorldSeen} (expect false)`);
if (!clusterGuard) throw new Error('a cluster armed the comm');
} else {
d.push('witness 2 (cluster): skipped (no cluster in this system)');
}
// ---- witness 3 — the first NON-HOME planet — the pair --------------
discover(disc(planet, planet.discoveryName, 'Rocky World'));
const armOk = s._firstWorldSeen === true && s.discoveredWorldCount() === 1 && s.comms.queue.length + (s.comms.active ? 1 : 0) === 2;
d.push(`witness 3 (planet): flag=${s._firstWorldSeen} (expect true) questWitness=${s.discoveredWorldCount()} (expect 1) armed=${s.comms.queue.length + (s.comms.active ? 1 : 0)} (expect 2 — the pair)`);
if (!armOk) throw new Error('the planet did not arm the pair (or the quest witness drifted)');
const ackStep = async (id, voiceKey, videoKey) => {
const live = await until(() => (s.comms.active && s.comms.current?.id === id ? s : null), 20000);
if (!live) return false;
const box = s.comms.box;
const el = box?.video?.video;
// The clip witness: his LOADER entry (the authoritative key→file map,
// set before the box exists) + the element's src once the browser has
// attached it (transiently empty right after attach — grace it).
const assetUrl = s.cache?.video?.get?.(videoKey)?.url ?? '';
const clipIsHim = assetUrl.includes('trestle.mp4');
if (el) await until(() => (el.currentSrc ?? '') !== '', 2000); // src attach grace
const elSrc = el ? el.currentSrc ?? el.src ?? '' : '';
const elOk = !!el && el.muted === true && el.loop === true && el.paused === false &&
(elSrc === '' || elSrc.includes('trestle.mp4')); // never OAC's clip
const contract =
s.comms.paused === true && // the ack pauses the action
!!box &&
!!box.button && // the ACKNOWLEDGE button
clipIsHim && elOk && // his clip, muted + looping + playing
s.sound.isPlaying(voiceKey) === true; // his voice
d.push(`${id}: paused=${s.comms.paused} box=${!!box} ackBtn=${!!box?.button} clip=${el ? el.paused === false && el.muted : '—'} loop=${el?.loop ?? '—'} asset=${assetUrl.split('/').pop()} elSrc=${(elSrc.split('/').pop() || '—')} voice=${voiceKey} playing=${s.sound.isPlaying(voiceKey)}`);
// Press ACKNOWLEDGE (the player's one move while it holds).
box.button.panel.emit('pointerdown');
await until(() => (s.comms.current?.id !== id ? true : null), 6000);
return contract;
};
const meetupOk = await ackStep('trestle-meetup', 'comms_au_trestle-intro-01', 'comms_vid_trestle');
await sleep(300);
const questOk = await ackStep('trestle-quest', 'comms_au_trestle-intro-02', 'comms_vid_trestle');
// ---- the ledger holds; the pair re-arms nothing --------------------
await sleep(700); // > the endedWithin(200) race window
const quiet = s.comms.active === false && s.comms.queue.length === 0;
const ledger = s.registry.get('commsPlayed');
d.push(`ledger: played=${JSON.stringify(ledger ? Array.from(ledger) : [])}`);
const playedOk = !!ledger && ledger.has('trestle-meetup') && ledger.has('trestle-quest');
const rearm = s.comms.trigger('firstWorld');
d.push(`re-arm: firstWorld=${rearm} (expect 0 — the ledger holds)`);
const allOk = quiet0 && homeGuard && armOk && meetupOk && questOk && quiet && playedOk && rearm === 0 && errors.length === 0;
const errLine = errors.length ? ['console errors:', ...errors.slice(0, 4)] : [];
window.__trestleResult = { ok: allOk, details: [...d, ...errLine] };
setReport(['TRESTLE PAIR SELF-CHECK ' + (allOk ? 'PASS ✔' : 'FAIL ✘'), '', ...d, ...errLine]);
} catch (err) {
window.__trestleResult = { ok: false, details: ['EXCEPTION: ' + String(err?.stack ?? err), ...d] };
setReport(['TRESTLE PAIR SELF-CHECK FAIL ✘', String(err?.stack ?? err), ...d]);
}
}, 600);