// dev/escort-compass.test.mjs — the ESCORT destination compass → autopilot // chain, verified headless (node dev/escort-compass.test.mjs). // // Replicates the real click path: // escortCompassTarget() → { kind:'destination', objectId } // updateDiscovery pushes → id `escort:obj:` // chip click → autopilotTo('escort:obj:') // and asserts the ship's target lands on the destination world's rim — // for a PLANET destination (discoveryId = its NAME) and a STATION // destination (discoveryId = settlement id). process.env.NODE_ENV = 'dev'; import { pathToFileURL } from 'node:url'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import assert from 'node:assert/strict'; import './phaser-loader.mjs'; // ../vendor/phaser.js -> ./phaser-stub.mjs (Node only) const __dirname = dirname(fileURLToPath(import.meta.url)); const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href); const fs = await import('node:fs'); const configData = {}; for (const f of fs.readdirSync(join(__dirname, '../data'))) { if (!f.endsWith('.json') || f === 'manifest.json') continue; configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(__dirname, '../data', f), 'utf8')); } config.init(configData); const { GameScene } = await import(pathToFileURL(join(__dirname, '../js/scenes/GameScene.js')).href); function makeScene(dest) { const s = Object.create(GameScene.prototype); const route = { kind: 'escort', targetSystemId: 'sys-dest', waypoints: [[120, 340]], // the RENDEZVOUS point (origin system's coords) credits: 100, xp: 10, escort: { phase: 'transit', originSystemId: 'sys-home', destSystemId: 'sys-dest', destObjectType: dest.objectType, destObjectId: dest.objectId, destObjectName: dest.objectName, hops: 1, }, }; s.ship = { x: 0, y: 0, radius: 12, setTarget: (x, y) => { s.aim = { x, y }; } }; s.systemRecord = { id: 'sys-dest' }; s.systemContent = { planets: dest.planets, settlements: dest.settlements }; s.systemPlanets = dest.planets.map((p) => ({ x: p.x, y: p.y, radius: p.radius, name: p.class ?? 'gas', // the Planet entity's `name` is its CLASS label discoveryId: p.name, discoveryName: p.name, })); s.systemStations = dest.settlements.map((st) => ({ x: st.x, y: st.y, bound: st.bound, discoveryId: st.id, discoveryName: st.name, settlement: { id: st.id }, })); s.solids = [ ...s.systemPlanets.map((p) => ({ discoveryId: p.discoveryId, clearance: 30, edgePoint: () => ({ x: p.x - 42, y: p.y }), aimPoint: (x, y) => ({ x, y }) })), ...s.systemStations.map((st) => ({ discoveryId: st.discoveryId, clearance: 40, edgePoint: () => ({ x: st.x - 80, y: st.y }), aimPoint: (x, y) => ({ x, y }) })), ]; s.asteroidClusters = []; s.systemGates = []; s.planet = null; s.sideQuestState = { activeId: 'sq-job', isCompleted: () => false, isClaimed: () => false, routeFor: () => route, }; s.tetherField = { clampPoint: (x, y) => ({ x, y }) }; s.showTargetMarker = (x, y) => { s.marker = { x, y }; }; s.hideHint = () => {}; s._commInputBlocked = () => false; s.menuSubBar = null; return s; } // --- PLANET destination (discoveryId = the planet's NAME) --------------- { const s = makeScene({ objectType: 'planet', objectId: 'p3', objectName: 'Kepler-6', planets: [{ id: 'p3', name: 'Kepler-6', x: 900, y: 400, radius: 60 }], settlements: [], }); const t = s.escortCompassTarget(); assert.ok(t, 'compass target stands'); assert.equal(t.kind, 'destination'); assert.equal(t.objectId, 'Kepler-6', 'objectId is the live entity discoveryId (the name)'); const id = `escort:obj:${t.objectId}`; s.autopilotTo(id); assert.deepEqual(s.aim, { x: 858, y: 400 }, 'autopilot aims at the planet rim'); console.log('planet destination: OK —', id, '→ aim', JSON.stringify(s.aim)); } // --- STATION destination (discoveryId = settlement id) ------------------- { const s = makeScene({ objectType: 'station', objectId: 's2', objectName: 'Relay 9', planets: [], settlements: [{ id: 's2', name: 'Relay 9', x: 700, y: 700, bound: 80 }], }); const t = s.escortCompassTarget(); assert.ok(t, 'compass target stands'); assert.equal(t.kind, 'destination'); assert.equal(t.objectId, 's2'); const id = `escort:obj:${t.objectId}`; s.autopilotTo(id); assert.deepEqual(s.aim, { x: 620, y: 700 }, 'autopilot aims at the station keepout'); console.log('station destination: OK —', id, '→ aim', JSON.stringify(s.aim)); } // --- the OLD bug must stay fixed: a `patrol:wp:` click on an escort route // must NOT silently fly the rendezvous coordinates (it resolves // route.waypoints[0] — here (120,340) — which in the destination system // is a random void point). { const s = makeScene({ objectType: 'planet', objectId: 'p3', objectName: 'Kepler-6', planets: [{ id: 'p3', name: 'Kepler-6', x: 900, y: 400, radius: 60 }], settlements: [], }); s.autopilotTo('patrol:wp:0'); // what the escort used to emit assert.deepEqual(s.aim, { x: 120, y: 340 }, 'patrol:wp:0 = the rendezvous point (the old wrong aim)'); console.log('old id shape still resolves to the rendezvous (documented):', JSON.stringify(s.aim)); } // --- STALE payload shape (a content RECORD id, as the compass stood before // the discoveryId fix — or a pre-update entry in a live session): // the autopilot must map it onto the world's discovery entry instead // of silently no-oping. { const s = makeScene({ objectType: 'planet', objectId: 'p3', objectName: 'Kepler-6', planets: [{ id: 'p3', name: 'Kepler-6', x: 900, y: 400, radius: 60 }], settlements: [], }); s.aim = null; s.autopilotTo('escort:obj:p3'); // the record id, NOT the discovery name assert.deepEqual(s.aim, { x: 858, y: 400 }, 'stale record-id payload still resolves to the planet rim'); console.log('stale record-id payload: OK — escort:obj:p3 → aim', JSON.stringify(s.aim)); } console.log('\nall escort compass → autopilot checks passed');