179 lines
9.2 KiB
JavaScript
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);
|