85 lines
3.6 KiB
JavaScript
85 lines
3.6 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). With
|
|
* shortcuts OFF (the current config) the network is a pure spanning
|
|
* tree, so a return gate always exists and this never returns null;
|
|
* the null path is defensive (re-enabled shortcuts, or a malformed
|
|
* record). 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;
|
|
* shortcuts are OFF in the current config, so the return gate always
|
|
* exists and the null path below is defensive only).
|
|
*
|
|
* @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 };
|
|
}
|