/** * Route — a plotted course between two SYSTEMS of the jump network. * * The player sets a destination (SYSTEM tab of the MAP console) and the * run plots the route it will take to get there. The compass then marks * the NEXT point of that route — the jump gate in the current system that * leads toward the destination — in orange, so the player always knows * where to fly next. * * THE JUMP NETWORK IS A SPANNING TREE (data/gates.json → shortcuts:false), * so there is EXACTLY ONE route between any two systems — the galaxy reads * as a maze. planRoute still computes the CHEAPEST path (Dijkstra by travel * distance over the gate graph), which is that unique route under the * current config and degrades gracefully to "the fastest route" if * shortcuts are ever enabled. Deterministic: same galaxy + endpoints ⇒ * same route. * * ROUTE STATE IS DERIVED. The run persists only the DESTINATION * (systemId + objectId); the path from wherever the ship is NOW to that * destination is computed on demand. That makes the follow/detour rules * automatic, with no progress counter to desync: * · jump to the route's next system → the remainder just shortens * (re-plan from the new system, same destination); * · jump anywhere else (a DETOUR) → the route re-plots from the detour * (GameScene toasts the re-plot); * · arrive at the destination system → the route is DONE and clears. * * Pure module (no Phaser) — Node-testable (dev/route.test.mjs). */ /** Euclidean distance between two roster points. */ function dist(ax, ay, bx, by) { const dx = ax - bx; const dy = ay - by; return Math.hypot(dx, dy); } /** * Plan the route between two systems. * * @param {object} galaxy — the run's Galaxy: needs `byId` (Map sysId → * record with x/y) and `jumpNetwork.gates` (Map sysId → [sysId, …]). * @param {string} fromId — the current system. * @param {string} toId — the destination system. * @returns {{path: string[], next: string|null, hops: number, distance: number}|null} * `path` is [from, …, to]; `next` is the FIRST hop after `from` (null * when from === to — already there); `hops` = path.length - 1. `null` * when an endpoint is unknown or there is no route (should not happen — * the network is strongly connected by construction). */ export function planRoute(galaxy, fromId, toId) { if (typeof fromId !== 'string' || typeof toId !== 'string') return null; if (fromId === toId) return { path: [toId], next: null, hops: 0, distance: 0 }; const byId = galaxy?.byId; const gates = galaxy?.jumpNetwork?.gates; if (!byId || !gates || !byId.has(fromId) || !byId.has(toId)) return null; if (typeof byId.get(fromId)?.x !== 'number' || typeof byId.get(toId)?.x !== 'number') return null; // Dijkstra by travel distance (edge cost = the hop's Euclidean span). // The graph is small (a few hundred systems, degree ≤ maxGates), so a // plain O(V²) select is plenty; ties break on system id (deterministic). const best = new Map(); // sysId → best distance seen best.set(fromId, 0); const prev = new Map(); // sysId → predecessor on the best path const done = new Set(); for (;;) { let u = null; let du = Infinity; for (const [id, d] of best) { if (done.has(id)) continue; if (d < du || (d === du && (u === null || id < u))) { u = id; du = d; } } if (u === null) break; // nothing left to relax if (u === toId) break; // reached the goal at its final cost done.add(u); const ru = byId.get(u); for (const v of gates.get(u) ?? []) { if (done.has(v) || !byId.has(v)) continue; const rv = byId.get(v); const nd = du + dist(ru.x, ru.y, rv.x, rv.y); if (nd < (best.get(v) ?? Infinity)) { best.set(v, nd); prev.set(v, u); } } } if (!best.has(toId)) return null; // unreachable — defensive (a tree can't do this) const path = []; let cur = toId; for (;;) { path.push(cur); if (cur === fromId) break; cur = prev.get(cur); if (cur === undefined) return null; // no chain back — defensive } path.reverse(); return { path, next: path.length > 1 ? path[1] : null, hops: path.length - 1, distance: best.get(toId), }; } /** * The system the route leaves NEXT from `fromId` on the way to `toId` * (planRoute's `next`), or null when already there / no route. */ export function nextSystem(galaxy, fromId, toId) { return planRoute(galaxy, fromId, toId)?.next ?? null; }