orbit/dev/escort-compass.test.mjs

156 lines
6.1 KiB
JavaScript

// 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:<objectId>`
// chip click → autopilotTo('escort:obj:<objectId>')
// 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');