orbit/dev/comms-mine.mjs

140 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* COMMS × MINING — the TYPE-AWARE busy() contract (2026-07, user ask:
* "let audio-only comms play even if the player is currently mining").
*
* The scene's busy() (GameScene → CommsHub) is now comm-type-aware:
*
* screen-owned (modal window/panel up, or a jump in flight)
* → NO comm of any type starts until it's clear;
* action in progress (mining live, the scan sweep out)
* → box comms (ack/visual — they pause the action or take the
* screen) wait; an 'audio' comm (voice only — no box, no pause,
* no input) rides right over the action.
*
* Walk:
* 1) free scene → busy is false for every type;
* 2) start REAL mining (the pop-up's own path: Mining.begin on a live
* cluster member → the arm extends → the beam goes live)
* → busy('audio') is FALSE, busy('ack')/busy('visual') stay TRUE;
* 3) trigger oac-research-complete (the AUDIO comm) MID-MINING
* → it starts (delayMs 1500 beat, then the pump), no box, no
* pause, the voice plays, and the MINING KEEPS RUNNING (the
* comm did not stop the action);
* 4) the screen-owned branch still holds audio: a jump in flight
* (_jumping) → busy('audio') is TRUE.
*
* Headless note: the AudioContext is suspended, so the 1.7 s clip never
* reaches its natural end — the test stops the voice with stopByKey()
* (exactly what the hub's isPlaying poll reads as "the voice ended");
* the hub must then finish the comm on its own.
*
* node dev/cdp-shot.mjs "http://127.0.0.1:3000/dev/comms-mine.html" o.png \
* "window.__commsMine" 90000
*/
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();
// Roster override: only the audio comm — the newGame welcome chain must
// not pollute the walk (it is ack comms, which this test is NOT about).
const roster = (data.comms?.comms ?? []).filter((c) => c.trigger === 'researchComplete');
if (roster.length !== 1 || roster[0].id !== 'oac-research-complete') {
throw new Error('roster override expected exactly oac-research-complete');
}
data.comms = { ...(data.comms ?? {}), comms: roster };
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 log = (m) => { report.textContent += m + '\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 = [];
try {
const s = () => game.scene.getScene('GameScene');
const ready = await until(() => {
const sc = s();
return sc && sc.ship && sc.comms && (sc.asteroidClusters?.length ?? 0) > 0 ? sc : null;
}, 30000);
if (!ready) throw new Error('scene never ready (no cluster?)');
const busy = (t) => s().comms.busy(t);
// ---- 1) free scene -------------------------------------------------
const freeOk = busy('audio') === false && busy('ack') === false && busy('visual') === false;
d.push(`free: busy audio=${busy('audio')} ack=${busy('ack')} visual=${busy('visual')} (expect false/false/false) ${freeOk ? '✔' : '✘'}`);
// ---- 2) start REAL mining -----------------------------------------
const cluster = s().asteroidClusters[0];
const member = cluster.members[0];
const began = s().mining.begin(cluster, member); // the pop-up's own path
if (began === false) throw new Error('mining.begin refused (already extending?)');
const miningLive = await until(() => (s().mining.isActive ? s() : null), 8000);
if (!miningLive) throw new Error('mining never went active');
const phaseOk = busy('audio') === false && busy('ack') === true && busy('visual') === true;
d.push(`mining live (state=${s().mining.state}, ship=${s().ship.state}): busy audio=${busy('audio')} (expect false — RIDES OVER) ack=${busy('ack')} (expect true) visual=${busy('visual')} (expect true) ${phaseOk ? '✔' : '✘'}`);
// ---- 3) the audio comm MID-MINING (the user ask) -------------------
s().comms.trigger('researchComplete');
const live = await until(
() => (s().comms.active && s().comms.current?.id === 'oac-research-complete' ? s() : null),
10000,
);
if (!live) throw new Error('the audio comm never came up mid-mining');
const vKey = s().comms._voiceKey;
const vPlaying = vKey ? s().sound.isPlaying(vKey) : false;
const stillMining = s().mining.isActive === true && s().ship.state === 'mining';
const step3 =
s().comms.box === null && // NO box (the audio contract)
s().comms.paused === false && // NO pause
vPlaying === true && // the voice is talking
stillMining; // the action KEEPS RUNNING
d.push(`mid-mining: active=true box=${!!s().comms.box} (expect false) paused=${s().comms.paused} (expect false) voice=${vKey ? 'playing' : 'SILENT'} (expect playing) miningStillActive=${stillMining} (expect true) ${step3 ? '✔' : '✘'}`);
// End the voice the way the hub's poll sees an end; the comm must
// finish on its own (and leave the action untouched).
if (vKey) s().sound.stopByKey(vKey);
const done = await until(() => (s().comms.current?.id !== 'oac-research-complete' ? true : null), 6000);
d.push(`voice-ended: comm auto-finished=${!!done} miningStillActive=${s().mining.isActive} (expect true) paused=${s().comms.paused}`);
const step3b = !!done && s().mining.isActive === true;
// ---- 4) screen-owned still holds audio (a jump in flight) ----------
s()._jumping = true;
const jumpAudio = busy('audio');
const jumpAck = busy('ack');
const jumpHolds = jumpAudio === true && jumpAck === true;
s()._jumping = false;
const afterAudio = busy('audio');
const afterAck = busy('ack');
d.push(`jump in flight: busy audio=${jumpAudio} ack=${jumpAck} (both expect true) → after reset audio=${afterAudio} ack=${afterAck} (mining still live → false/true) ${jumpHolds ? '✔' : '✘'}`);
const ok = freeOk && phaseOk && step3 && step3b && jumpHolds;
window.__commsMine = { ok, details: d };
console.log('[comms-mine]', ok ? 'PASS' : 'FAIL', d.join(' | '));
} catch (err) {
window.__commsMine = { ok: false, details: [...d, 'EXCEPTION: ' + String(err?.stack ?? err)] };
console.log('[comms-mine] FAIL', String(err));
}
}, 800);