orbit/dev/comms-trestle.mjs

164 lines
8.3 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 'firstForeignPlanet'):
*
* 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 witness check),
* the real hub (FIFO, the ack contract, the ledger), the real box.
*
* The witness matrix (all through the real celebrateDiscovery):
* 1. home system, a planet discovered → NO arm (home doesn't count)
* 2. foreign system, a cluster → NO arm (planets are the witness)
* 3. foreign system, a PLANET → the pair arms, in config order
*
* 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 === 'firstForeignPlanet'),
};
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: discoveryId match).
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,
});
// ---- witness 1 — home system: a planet does NOT count -------------
if (s.isHomeSystem !== true) throw new Error('scene should boot in the home system');
s.celebrateDiscovery(disc(planet, planet.discoveryName, 'Rocky World'));
await sleep(400);
const homeGuard = s.comms.active === false && s.comms.queue.length === 0 && s._firstForeignPlanetSeen === false;
d.push(`witness 1 (home planet): armed=${s.comms.queue.length} (expect 0 — home doesn't count) flag=${s._firstForeignPlanetSeen} (expect false)`);
// ---- witness 2 — foreign system: a CLUSTER does not count ---------
s.isHomeSystem = false; // simulate the jump: the scene now holds a foreign system
if (cluster) {
s.celebrateDiscovery(disc(cluster, cluster.discoveryName, 'Asteroid Cluster'));
await sleep(400);
const clusterGuard = s.comms.active === false && s.comms.queue.length === 0 && s._firstForeignPlanetSeen === false;
d.push(`witness 2 (foreign cluster): armed=${s.comms.queue.length} (expect 0 — planets are the witness) flag=${s._firstForeignPlanetSeen} (expect false)`);
if (!clusterGuard) throw new Error('a cluster armed the planet comm');
} else {
d.push('witness 2 (foreign cluster): skipped (no cluster in this system)');
}
// ---- witness 3 — foreign system: the first PLANET — the pair ------
s.celebrateDiscovery(disc(planet, planet.discoveryName, 'Rocky World'));
const armOk = s._firstForeignPlanetSeen === true && s.comms.queue.length + (s.comms.active ? 1 : 0) === 2;
d.push(`witness 3 (foreign planet): flag=${s._firstForeignPlanetSeen} (expect true) 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');
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;
const clipIsHim =
box.videoSpec?.key === videoKey && // his loader key (not OAC's)
(!el || (el.currentSrc ?? '').includes('trestle.mp4')); // his file on the wire
const contract =
s.comms.paused === true && // the ack pauses the action
!!box &&
!!box.button && // the ACKNOWLEDGE button
clipIsHim &&
!!el && el.muted === true && el.loop === true && el.paused === false && // 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 ?? '—'} videoKey=${box?.videoSpec?.key ?? '—'} elSrc=${el ? (el.currentSrc ?? '').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('firstForeignPlanet');
d.push(`re-arm: firstForeignPlanet=${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);