140 lines
6.9 KiB
JavaScript
140 lines
6.9 KiB
JavaScript
/**
|
||
* 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);
|