orbit/js/galaxy/JumpTravel.js

80 lines
3.3 KiB
JavaScript

/**
* 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 };
}