/** * 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);