/** * JumpTravel — the pure geometry of a gate jump (no Phaser): * * returnGateFor(content, fromId) * The destination system's RETURN gate — the gate in `content` * whose destination is the system the ship just left (the first * `content.jumps` entry with `to === fromId`, or null). * * arrivalPoint(gate, cfg) * Where the ship materialises when it exits a gate: offset BACK * along the gate's facing (a gate's `rotation` is the bearing to * its DESTINATION star — the system we came from), just clear of * the keepout circle (gate radius + ship clearance + ship radius * + the config gap). The ship keeps its nose along the way it was * travelling (heading = facing + π). * * jumpArrival(content, fromId, cfg) * Both, together: { x, y, heading, gateId } — or null when the * destination holds no return gate (a one-way SHORTCUT jump — * JumpNetwork: tree edges run both ways, shortcuts don't). The * scene falls back to the destination's origin (its home-tether * zone) when this is null. * * GameScene.jumpThroughGate supplies the live numbers (the gate's * radius/clearance, the ship's radius, data/gates.json → jump) and * does the actual transport (re-stage the run for the destination * system via the save pipeline, then scene.restart); this module only * decides WHICH gate and WHERE — pure enough for Node tests * (dev/jump-travel.test.mjs). */ /** * @param {object|null} content the destination system's content record * (galaxy.ensureContent(id): { jumps: [{ id, name, to, x, y, rotation }] }) * @param {string} fromId the id of the system the ship is leaving * @returns {object|null} the return gate's content record, or null */ export function returnGateFor(content, fromId) { if (!content || fromId == null) return null; const jumps = Array.isArray(content.jumps) ? content.jumps : []; return jumps.find((j) => j && j.to === fromId) ?? null; } /** * The spawn point just past a gate's keepout, on the side the ship * lands on (the far side of its facing). * * @param {object} gate the gate's content record: { x, y, rotation } * @param {object} [cfg] { radius (gate keepout, px), clearance (ship * keepout from the gate, px), shipRadius (px), gap (extra px, from * data/gates.json → jump.arrivalGap) } * @returns {{x:number, y:number, heading:number}} */ export function arrivalPoint(gate, cfg = {}) { const radius = Number(cfg.radius) || 0; const clearance = Number(cfg.clearance) || 0; const shipRadius = Number(cfg.shipRadius) || 0; const gap = Number(cfg.gap) || 0; const a = Number(gate?.rotation) || 0; const offset = radius + clearance + shipRadius + gap; return { x: (Number(gate?.x) || 0) - Math.cos(a) * offset, y: (Number(gate?.y) || 0) - Math.sin(a) * offset, heading: a + Math.PI, }; } /** * The jump's arrival for a destination system: at its return gate (if * it holds one — tree-edge jumps do; one-way shortcut jumps don't). * * @returns {{x:number, y:number, heading:number, gateId:string}|null} * null when there is no return gate — the caller falls back. */ export function jumpArrival(content, fromId, cfg = {}) { const gate = returnGateFor(content, fromId); if (!gate) return null; return { ...arrivalPoint(gate, cfg), gateId: gate.id }; }