orbit/dev/route-pulse-live.mjs

216 lines
9.7 KiB
JavaScript

/**
* Dev-only: boot GameScene and drive the ROUTE-PUSE marks end-to-end
* (js/entities/RoutePulse.js):
* Phase A — the route GATE: set a neighboring-system destination and
* assert the current system's route gate (routeNextGate) alone wears
* the pulse (the FULL cfg, route.pulse), planets/stations stay off.
* Phase B — the destination OBJECT: put the destination in the CURRENT
* system (the destination-system state) and assert the destination
* OBJECT alone (a planet or a space station) wears the pulse (the
* SUBDUED cfg, route.objectPulse), the gates stay off.
* Phase C — the pulse DROPS: clear the destination and assert nothing
* wears it any more.
*
* node dev/server.mjs 8084
* node dev/cdp-firefox.mjs \
* "http://127.0.0.1:8084/dev/route-pulse-live.html?seed=route-pulse-live-1" \
* "window.__ROUTE_PULSE_LIVE" 40000
* node dev/shot-firefox.mjs \
* "http://127.0.0.1:8084/dev/route-pulse-live.html?seed=route-pulse-live-1&shot=gate" \
* "window.__ROUTE_PULSE_LIVE ? window.__ROUTE_PULSE_LIVE.ready : null" \
* route-pulse-gate.png 40000
* (shot=gate | shot=object chooses which phase ends the run on screen)
*/
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();
config.init(data);
// Deterministic dev galaxy (same seed ⇒ same system + neighbors).
const params = new URLSearchParams(location.search);
const seedParam = params.get('seed');
if (seedParam) globalThis.__ORBIT_DEV_SEED = seedParam;
const shotParam = params.get('shot'); // 'gate' | 'object'
// Capture console errors + uncaught exceptions for the report.
const errors = [];
const origErr = console.error.bind(console);
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
window.addEventListener('error', (e) => errors.push(String(e.message)));
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
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;max-width:70%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
document.body.appendChild(report);
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
setReport('booting…');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitScene() {
for (let i = 0; i < 100; i++) {
const s = game.scene.getScene('GameScene');
if (s && s.ship && s.systemGates) return s;
await sleep(100);
}
throw new Error('GameScene never booted');
}
// Parity snapshot of the route-pulse state across all pulse-wearing
// entities (gates + planets + stations) — the assertions' subject.
function stateSnapshot(s) {
const ent = (e) => ({
kind: e.routePulseKind ?? e.constructor.name,
id: e.discoveryId,
on: e.routePulse?.on,
visible: e.routePulse?.ring?.visible,
baseRadius: e.routePulse?.cfg?.baseRadius,
});
return [
...s.systemGates.map((g) => ({ ...ent(g), kind: 'gate' })),
...(s.planet ? [{ ...ent(s.planet), kind: 'home-planet' }] : []),
...s.systemPlanets.map((p) => ({ ...ent(p), kind: 'planet' })),
...s.systemStations.map((st) => ({ ...ent(st), kind: 'station' })),
];
}
// Park the ship `gap` px (edge-to-edge) off the entity's keepout rim so
// the pulse is centered on screen, without tripping the destination
// "reached" window (keepout + 40).
function parkNear(s, entity, gap) {
const ship = s.ship;
const keepout = entity.minCenterDistance?.(ship.radius) ?? (entity.radius + (entity.clearance ?? 0) + ship.radius);
const d = keepout + gap;
ship.x = entity.x + d; // dead-right of the object — the pulse is symmetric
ship.y = entity.y;
ship.body?.setVelocity?.(0, 0);
ship.body?.setAcceleration?.(0, 0);
}
const results = [];
const record = (label, cond, extra = '') => {
results.push({ label, ok: !!cond, extra });
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}${extra ? ' — ' + extra : ''}`);
};
try {
const s = await waitScene();
await sleep(800);
const sysId = s.systemRecord.id;
const cur = s.galaxy.records.find((r) => r.id === sysId);
// --- PHASE A: the route GATE wears the FULL pulse ----------------------
const neighbor = s.systemGates[0]?.gate?.to ?? null;
const destContent = neighbor ? s.galaxy.contentOf(neighbor) : null;
const destObjId = (destContent?.planets ?? [])[0]?.name ?? null;
s.setDestination(neighbor, destObjId);
await sleep(300);
const plan = s.routePlan();
const gate = s.routeNextGate();
const gates = s.systemGates;
const allOff = (e) => !e || !(e.routePulse?.on);
record('A: a route exists (multi-stop, the current system is intermediate)',
!!plan && plan.hops >= 1 && plan.next !== null, `hops=${plan?.hops} path=${plan?.path?.join('→')}`);
record('A: the route gate is one of this system\'s gates', gates.some((g) => g === gate));
record('A: the route gate ALONE wears the pulse',
gates.length > 0 && !!gate && gate.routePulse.on === true
&& gates.filter((g) => g && g.routePulse.on).length === 1,
`gates=${gates.length} on=${gates.filter((g) => g && g.routePulse.on).length}`);
record('A: no planet/station is marked in an intermediate system',
allOff(s.planet) && s.systemPlanets.every(allOff) && s.systemStations.every(allOff));
record('A: the gate wears the FULL pulse cfg (route.pulse)',
gate.routePulse.cfg === config.section('gates.route.pulse', {}),
`baseRadius=${gate.routePulse.cfg.baseRadius}`);
const aSnap = stateSnapshot(s);
// --- PHASE B: the destination OBJECT wears the SUBDUED pulse ----------
// Destination-system state: the destination names an OBJECT in THIS
// system (what it looks like after the last jump lands).
const stations = s.systemStations;
const destEntity = stations[0] ?? s.systemPlanets[0] ?? s.planet;
const destId = destEntity?.discoveryId ?? null;
let bSnap = null;
if (destEntity) {
s.destination = { systemId: sysId, objectId: destId };
s.registry.set('destination', s.destination);
await sleep(300);
const bDest = s._routeDestinationEntity();
record('B: _routeDestinationEntity resolves the destination OBJECT',
bDest === destEntity, `${destEntity.constructor.name} ${destId}`);
record('B: the destination object ALONE wears the pulse',
destEntity.routePulse.on === true
&& gates.every(allOff)
&& s.systemPlanets.filter((p) => p !== destEntity).every(allOff)
&& (s.planet === destEntity ? s.planet.routePulse.on === true : allOff(s.planet))
&& stations.filter((e) => e !== destEntity).every(allOff),
`kind=${destEntity.constructor.name}`);
record('B: it wears the SUBDUED cfg (route.objectPulse)',
destEntity.routePulse.cfg === config.section('gates.route.objectPulse', {}),
`baseRadius=${destEntity.routePulse.cfg.baseRadius}`);
record('B: the subdued cfg is less intense than the gate\'s full cfg',
config.get('gates.route.objectPulse.ringAlpha', 1) < config.get('gates.route.pulse.ringAlpha', 0)
&& config.get('gates.route.objectPulse.rippleAlpha', 1) < config.get('gates.route.pulse.rippleAlpha', 0));
bSnap = stateSnapshot(s);
} else {
record('B: SKIPPED — this starting system has no planet/station to destination', true, 'no destination-object candidate here');
}
// --- PHASE C: the pulse DROPS when the route clears --------------------
s.destination = null;
s.registry.set('destination', null);
await sleep(300);
record('C: with no destination, nothing wears the pulse',
gates.every(allOff) && s.systemPlanets.every(allOff) && allOff(s.planet) && stations.every(allOff));
const failures = results.filter((r) => !r.ok).length;
// --- Screenshot parking (only for the shot= runs) ----------------------
if (shotParam === 'gate') {
s.setDestination(neighbor, destObjId); // restore the A state
await sleep(200);
const g = s.routeNextGate();
if (g) { parkNear(s, g, 60); await sleep(400); }
} else if (shotParam === 'object' && destEntity) {
s.destination = { systemId: sysId, objectId: destId };
s.registry.set('destination', s.destination);
await sleep(200);
parkNear(s, destEntity, 150);
await sleep(400);
}
window.__ROUTE_PULSE_LIVE = {
ready: true,
failures,
errors,
system: { id: sysId, name: cur?.name },
neighbor: { id: neighbor, destObjId },
destEntity: destEntity ? { id: destId, kind: destEntity.constructor.name } : null,
results,
phaseA: aSnap,
phaseB: bSnap,
};
setReport([
`system: ${cur?.name} (${sysId}) neighbor: ${neighbor} (obj ${destObjId ?? 'none'})`,
`dest-object case: ${destEntity ? `${destEntity.constructor.name} ${destId}` : 'none'}`,
...results.map((r) => `${r.ok ? '✔' : '✘'} ${r.label}${r.extra ? ' — ' + r.extra : ''}`),
`ERRORS: ${errors.length ? errors.join(' | ') : 'none'}`,
failures === 0 ? 'ALL ROUTE-PULSE LIVE CHECKS PASSED ✔' : `${failures} LIVE CHECK(S) FAILED ✘`,
]);
} catch (e) {
setReport(['FATAL: ' + (e?.message ?? e), String(e?.stack ?? '').split('\n').slice(0, 6).join(' | '), ...errors]);
window.__ROUTE_PULSE_LIVE = { ready: true, errors: [...errors, String(e?.message ?? e)], stack: String(e?.stack ?? '').slice(0, 1200), failures: 1, results };
}