Fix escort compass chip click not retargeting the ship
- `escortCompassTarget()` now emits a `destination` kind carrying the world's discovery id (a planet's name / a station's settlement id) instead of reusing the `waypoint`/idx shape, which autopilot resolved to `route.waypoints[0]` — the escort's rendezvous point in the destination system - `autopilotTo()` gains an `escort:obj:` branch that resolves the payload to the world's discovery entry, and maps a stale content-record-id payload onto the live entity so the click no longer silently no-ops - `_escortDestinationObject()` now matches the live planet/station by discovery name (a Planet's `name` is its class label) and returns the entity's `discoveryId` for compass/autopilot identity - Compass offscreen listing suppresses the destination world's own slot when the escort arrow stands it, mirroring the existing route-gate rule - Add headless unit test (`dev/escort-compass.test.mjs`) covering planet/station destinations, the stale record-id payload, and the old `patrol:wp:` shape, plus a Playwright repro (`dev/escort-compass-click.mjs`) that drives a real mouse click on the chip in a live browser
This commit is contained in:
parent
c6c3f8087f
commit
b66b3b73e3
|
|
@ -0,0 +1,140 @@
|
|||
// dev/escort-compass-click.mjs — REPRO: the escort's FINAL-STOP compass box,
|
||||
// clicked in a REAL BROWSER (Playwright + vendored Phaser, true per-frame
|
||||
// order). Run:
|
||||
//
|
||||
// node dev/server.mjs 8123 &
|
||||
// node dev/escort-compass-click.mjs
|
||||
//
|
||||
// Sets an escort contract whose destination is the CURRENT system's planet
|
||||
// (off-screen), waits for the compass to stand its magenta entry, then
|
||||
// CLICKS the chip with a real mouse event and asserts the ship retargets
|
||||
// to the planet's rim.
|
||||
|
||||
const PORT = process.env.ORBIT_PORT || '8123';
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
const { pathToFileURL } = await import('node:url');
|
||||
const { chromium } = await import(pathToFileURL('/home/brianfertig/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs').href);
|
||||
|
||||
const consoleErrors = [];
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: '/home/brianfertig/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome',
|
||||
args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
||||
page.on('pageerror', (e) => consoleErrors.push(`pageerror: ${e.message}`));
|
||||
|
||||
await page.goto(`${BASE}/index.html`, { waitUntil: 'load' });
|
||||
await page.waitForFunction(() => !!(window.game && window.game.scene && window.game.scene.getScene && window.game.scene.getScene('MenuScene')), null, { timeout: 20000 });
|
||||
await page.waitForFunction(() => {
|
||||
const m = window.game?.scene?.getScene?.('MenuScene');
|
||||
return !!(m && m.overlay && m.title && m.seedValue && !m.dead);
|
||||
}, null, { timeout: 20000 });
|
||||
await page.evaluate(() => {
|
||||
const m = window.game.scene.getScene('MenuScene');
|
||||
if (!m.seedValue) m.seedValue = 'ESCORT01';
|
||||
m.startNewGame();
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
return !!(s && s.systemRecord && s.ship);
|
||||
}, null, { timeout: 40000 });
|
||||
console.log(' ✓ game booted, GameScene active');
|
||||
|
||||
// ---- stand the escort contract: destination = a planet of the current system
|
||||
const setup = await page.evaluate(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
const sysId = s.systemRecord.id;
|
||||
const p = (s.systemPlanets ?? [])[0];
|
||||
if (!p) return { error: 'no planets in the current system' };
|
||||
// Park the ship far from the planet so the planet is off-screen.
|
||||
const away = Math.hypot(p.x, p.y) > 10 ? 1 : -1;
|
||||
s.ship.x = p.x + away * 3000; s.ship.y = p.y + 3000;
|
||||
s.ship.target = null;
|
||||
s.ship.thrustDir = null;
|
||||
const st = s.sideQuestState;
|
||||
const rec = (s.systemContent.planets ?? []).find((o) => o && o.name === p.discoveryName) ?? null;
|
||||
st.accept('repro-job');
|
||||
const route = {
|
||||
kind: 'escort',
|
||||
targetSystemId: sysId,
|
||||
waypoints: [[10, 10]],
|
||||
credits: 50, xp: 5,
|
||||
escort: {
|
||||
phase: 'transit',
|
||||
originSystemId: 'sys-elsewhere',
|
||||
destSystemId: sysId,
|
||||
destObjectType: 'planet',
|
||||
destObjectId: rec ? rec.id : '',
|
||||
destObjectName: p.discoveryName,
|
||||
hops: 1,
|
||||
transportMaxHull: 1200, transportHull: 1200,
|
||||
transportMaxShields: 500, transportShields: 500,
|
||||
escortCount: 1, name: 'Repro Convoy',
|
||||
},
|
||||
};
|
||||
st.setRoute('repro-job', route);
|
||||
st.setActive('repro-job');
|
||||
return { planet: { x: p.x, y: p.y, name: p.discoveryName, discoveryId: p.discoveryId }, ship: { x: s.ship.x, y: s.ship.y }, activeId: st.activeId };
|
||||
});
|
||||
console.log(' setup:', JSON.stringify(setup));
|
||||
|
||||
// Wait for the compass to stand the escort's entry.
|
||||
await page.waitForFunction(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
const keys = [...s.compass.entries.keys()];
|
||||
return keys.some((x) => x.startsWith('escort:obj:'));
|
||||
}, null, { timeout: 15000 }).catch(() => null);
|
||||
const entry = await page.evaluate(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
const k = [...s.compass.entries.keys()].find((x) => x.startsWith('escort:obj:'));
|
||||
if (!k) return null;
|
||||
const e = s.compass.entries.get(k);
|
||||
return { id: k, chip: { x: e.chipRoot.x, y: e.chipRoot.y }, target: { x: e.x, y: e.y } };
|
||||
});
|
||||
if (!entry) {
|
||||
const diag = await page.evaluate(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
return { keys: [...s.compass.entries.keys()], t: s.escortCompassTarget ? s.escortCompassTarget() : null };
|
||||
});
|
||||
console.log(' ✗ NO COMPASS ENTRY', JSON.stringify(diag), '\n CONSOLE', consoleErrors.slice(0, 10));
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(` ✓ compass entry stands: ${entry.id} at chip (${Math.round(entry.chip.x)}, ${Math.round(entry.chip.y)})`);
|
||||
|
||||
// ---- CLICK the chip with a real mouse event -------------------------------
|
||||
// Isolate the chain from comm timing: in transit the escort lane can fire a
|
||||
// wave → an OAC comm starts and (by design) owns the input, so a click in
|
||||
// that window is a legitimate no-op. Stub the gate to false to test the
|
||||
// COMPASS → AUTOPILOT chain itself.
|
||||
await page.evaluate(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
s._commInputBlocked = () => false;
|
||||
s.__setTargetCalls = [];
|
||||
const oSet = s.ship.setTarget.bind(s.ship);
|
||||
s.ship.setTarget = (x, y) => { s.__setTargetCalls.push({ x, y }); return oSet(x, y); };
|
||||
});
|
||||
await page.mouse.move(entry.chip.x, entry.chip.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(300);
|
||||
// The REGRESSION assertion for the user's bug: the chip click must REACH
|
||||
// autopilotTo and issue a setTarget (before the fix the whole chain was a
|
||||
// silent no-op — the compass stored the content RECORD id and the
|
||||
// discovery lookup missed). Whether the target SURVIVES a frame later is
|
||||
// up to the rest of the game (a comm's pause contract legitimately stops
|
||||
// the ship — that is orthogonal to this chain).
|
||||
const result = await page.evaluate(() => {
|
||||
const s = window.game.scene.getScene('GameScene');
|
||||
return { calls: s.__setTargetCalls, target: s.ship.target, state: s.ship.state };
|
||||
});
|
||||
console.log(' click result:', JSON.stringify(result));
|
||||
const ok = result.calls.length > 0;
|
||||
console.log(ok
|
||||
? ` ✓ chip click drove the autopilot → setTarget(${result.calls.at(-1).x?.toFixed(1)}, ${result.calls.at(-1).y?.toFixed(1)}) — the destination world's rim (tether-clamped)`
|
||||
: ' ✗ NO RETARGET — the click did nothing');
|
||||
if (consoleErrors.length) console.log(' CONSOLE ERRORS\n ' + consoleErrors.slice(0, 10).join('\n '));
|
||||
await browser.close();
|
||||
process.exit(ok ? 0 : 1);
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// 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');
|
||||
|
|
@ -2929,12 +2929,16 @@ export class GameScene extends Phaser.Scene {
|
|||
// suppressed (the route gate's rule, a different color).
|
||||
const patrolT = this.activeRouteCompassTarget();
|
||||
const patrolGateId = patrolT?.kind === 'gate' ? patrolT.gate?.discoveryId : null;
|
||||
// The ESCORT's destination world (its magenta arrow takes the world's slot —
|
||||
// the route gate's suppression rule).
|
||||
const patrolDestId = patrolT?.kind === 'destination' ? patrolT.objectId : null;
|
||||
|
||||
const offscreen = [];
|
||||
for (const o of objects) {
|
||||
if (o.id === routeGateId) continue; // the route arrow takes this slot
|
||||
if (o.id === destObjId) continue; // the destination arrow takes this slot
|
||||
if (o.id === patrolGateId) continue; // the patrol arrow takes this slot
|
||||
if (o.id === patrolDestId) continue; // the escort destination arrow takes this slot
|
||||
if (this.discovery.isDiscovered(sysId, o.id) && !circleInView(o.x, o.y, o.radius, view)) {
|
||||
offscreen.push(o);
|
||||
}
|
||||
|
|
@ -2984,6 +2988,20 @@ export class GameScene extends Phaser.Scene {
|
|||
// autopilot-resolvable (the `patrol:gate:` id's own branch).
|
||||
alwaysFull: true,
|
||||
});
|
||||
} else if (patrolT.kind === 'destination' && patrolT.objectId && !circleInView(patrolT.x, patrolT.y, patrolT.radius ?? 40, view)) {
|
||||
offscreen.push({
|
||||
id: `escort:obj:${patrolT.objectId}`,
|
||||
x: patrolT.x,
|
||||
y: patrolT.y,
|
||||
radius: patrolT.radius ?? 40,
|
||||
typeLabel: patrolT.typeLabel ?? config.get('sidequests.escort.compass.typeLabel', 'Escort'),
|
||||
color: patrolT.color ?? config.get('sidequests.escort.compass.color', '#e87cf5'),
|
||||
name: patrolT.name ?? 'DESTINATION',
|
||||
// The escort's final stop — the full readout (like the route's
|
||||
// destination), autopilot-resolvable (the `escort:obj:` id's own
|
||||
// branch: the world's real discovery id, not a waypoint index).
|
||||
alwaysFull: true,
|
||||
});
|
||||
} else if (patrolT.kind === 'waypoint' && !circleInView(patrolT.x, patrolT.y, 32, view)) {
|
||||
offscreen.push({
|
||||
id: `patrol:wp:${patrolT.idx}`,
|
||||
|
|
@ -3112,7 +3130,30 @@ export class GameScene extends Phaser.Scene {
|
|||
if (typeof id === 'string') {
|
||||
if (id.startsWith('route:')) effectiveId = id.slice('route:'.length);
|
||||
else if (id.startsWith('dest:')) effectiveId = id.slice('dest:'.length);
|
||||
else if (id.startsWith('patrol:')) {
|
||||
else if (id.startsWith('escort:')) {
|
||||
// The ESCORT's destination world (`escort:obj:<objectId>` — the
|
||||
// world's real discovery id, resolved like `route:`/`dest:`):
|
||||
// autopiloting it by the `patrol:wp:` index shape would have aimed
|
||||
// the ship at the escort's RENDEZVOUS waypoint instead. A payload
|
||||
// that is a content RECORD id (an entry stood before the
|
||||
// discoveryId fix, or a stale session) is mapped onto the world's
|
||||
// discovery entry here — without it the lookup would miss and the
|
||||
// click would silently no-op.
|
||||
const payload = id.slice('escort:'.length);
|
||||
if (payload.startsWith('obj:')) {
|
||||
const oid = payload.slice('obj:'.length);
|
||||
const objs = this.discoverableObjects();
|
||||
const recs = [
|
||||
...(Array.isArray(this.systemContent?.planets) ? this.systemContent.planets : []),
|
||||
...(Array.isArray(this.systemContent?.settlements) ? this.systemContent.settlements : []),
|
||||
];
|
||||
const rec = recs.find((r) => r && String(r.id) === oid);
|
||||
const hit =
|
||||
objs.find((v) => v.id === oid) ??
|
||||
(rec ? objs.find((v) => v.name === rec.name) : undefined);
|
||||
effectiveId = hit ? hit.id : oid; // no hit → the generic lookup below misses → no-op
|
||||
}
|
||||
} else if (id.startsWith('patrol:')) {
|
||||
// The active PATROL's compass entry (see updateDiscovery):
|
||||
// `patrol:gate:<gateId>` — the yellow route gate in ANOTHER
|
||||
// system (the gate's own autopilot flies it — the orange route
|
||||
|
|
@ -5549,10 +5590,17 @@ export class GameScene extends Phaser.Scene {
|
|||
for (const gt of this.systemGates ?? []) if (gt.gate?.to === originId) return { kind: 'gate', gate: gt, ...style };
|
||||
return null;
|
||||
}
|
||||
// AFTER the link: the DESTINATION.
|
||||
// AFTER the link: the DESTINATION. Its own `destination` kind (NOT the
|
||||
// waypoint/idx shape): the idx would resolve autopilot's `patrol:wp:` to
|
||||
// route.waypoints[0] — the escort's RENDEZVOUS point — and the ship would
|
||||
// fly the rendezvous coordinates inside the destination system instead
|
||||
// of the world itself.
|
||||
if (destId === this.systemRecord.id) {
|
||||
const obj = this._escortDestinationObject(route);
|
||||
if (obj) return { kind: 'waypoint', idx: 0, x: obj.x, y: obj.y, ...style };
|
||||
// The autopilot id is the world's DISCOVERY id (a planet's NAME,
|
||||
// a station's settlement id) — its record id would miss the
|
||||
// discoverable/solid lookups and the click would no-op.
|
||||
if (obj) return { kind: 'destination', objectId: obj.discoveryId ?? obj.id, name: obj.name, x: obj.x, y: obj.y, radius: obj.radius, ...style };
|
||||
}
|
||||
// Not there yet — the gate toward the next hop on the way to it.
|
||||
let next = null;
|
||||
|
|
@ -5577,24 +5625,33 @@ export class GameScene extends Phaser.Scene {
|
|||
if (!content) return null;
|
||||
const id = String(esc.destObjectId ?? '');
|
||||
const name = String(esc.destObjectName ?? '');
|
||||
const pool = [
|
||||
...(Array.isArray(content.planets) ? content.planets : []),
|
||||
...(Array.isArray(content.settlements) ? content.settlements : []),
|
||||
];
|
||||
const planets = Array.isArray(content.planets) ? content.planets : [];
|
||||
const settlements = Array.isArray(content.settlements) ? content.settlements : [];
|
||||
const rec =
|
||||
pool.find((o) => o && String(o.id) === id && id !== '') ??
|
||||
pool.find((o) => o && String(o.name) === name && name !== '');
|
||||
planets.find((o) => o && String(o.id) === id && id !== '') ??
|
||||
settlements.find((o) => o && String(o.id) === id && id !== '') ??
|
||||
planets.find((o) => o && String(o.name) === name && name !== '') ??
|
||||
settlements.find((o) => o && String(o.name) === name && name !== '');
|
||||
if (rec && Number.isFinite(rec.x) && Number.isFinite(rec.y)) {
|
||||
// The world's rendered radius — the live entity's disc/keepout when it
|
||||
// stands here (a planet's disc / a station's keepout), else a fallback.
|
||||
// The delivery anchors the convoy's landing just off this disc.
|
||||
let radius = Number(rec.radius) || 0;
|
||||
if (!radius) {
|
||||
const planet = (this.systemPlanets ?? []).find((e) => e && (e.name === rec.name || e.discoveryId === rec.id));
|
||||
const station = (this.systemStations ?? []).find((e) => e && (e.settlement?.id === rec.id || e.discoveryName === rec.name));
|
||||
radius = Number(planet?.radius) || Number(station?.bound) || Number(station?.radius) || 0;
|
||||
}
|
||||
return { x: rec.x, y: rec.y, id: rec.id, name: rec.name, radius: radius > 0 ? radius : 40 };
|
||||
// The LIVE entity behind this record, when it stands here (a planet's
|
||||
// disc / a station's keepout). Match on the entity's DISCOVERY name
|
||||
// (the world's name) — the Planet's `name` field is its CLASS label,
|
||||
// and its discoveryId is the world's NAME, not the record id — so
|
||||
// discoveryName/discoveryId === rec.name is the planet's join key.
|
||||
const isPlanet = planets.includes(rec);
|
||||
const planet = (this.systemPlanets ?? []).find((e) => e && (e.discoveryName === rec.name || e.discoveryId === rec.id || e.discoveryId === rec.name));
|
||||
const station = (this.systemStations ?? []).find((e) => e && (e.settlement?.id === rec.id || e.discoveryName === rec.name));
|
||||
const live = planet ?? station ?? null;
|
||||
const radius = Number(planet?.radius) || Number(station?.bound) || Number(station?.radius) || Number(rec.radius) || 40;
|
||||
// discoveryId = the compass/autopilot identity (a planet's NAME, a
|
||||
// station's settlement id) — the autopilot's `escort:obj:` resolution
|
||||
// matches on it (the record id would miss the lookups and the click
|
||||
// would no-op).
|
||||
return {
|
||||
x: rec.x, y: rec.y, id: rec.id,
|
||||
discoveryId: live?.discoveryId ?? (isPlanet ? rec.name : rec.id),
|
||||
name: rec.name, radius,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue