216 lines
9.7 KiB
JavaScript
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 };
|
|
}
|