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