orbit/js/galaxy/SystemChart.js

131 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* SystemChart — the pure geometry + stats behind the deck's MAP console
* (js/ui/MapWindow.js draws on it; GameScene feeds it).
*
* chartBounds(objects, padding) the system's chart frame: the furthest
* X/Y extents of EVERY object (planets,
* stations, jump gates AND asteroid
* clusters — discovered or not) plus
* `padding` px (data/map.json →
* bounds.padding, ~1024) on each edge.
* fitToRect(bounds, w, h) the world → plate transform that fits
* that frame into the map plate, centred.
* navDiscoveryStats(d, sysId, c) the SYSTEM DISCOVERY share — the NAV
* objects (planets + free-space stations
* + jump gates; the central body is the
* chart's anchor, not a count) vs. the
* player's Discovery state.
* resourceStats(d, sysId, c) the SYSTEM RESOURCES share — the
* asteroid fields vs. Discovery (the only
* resource kind for now).
*
* Pure (no Phaser) — Node-testable (dev/system-chart.test.mjs). The NAV
* point set is SystemCategory.navPoints (the same discoverable set the
* jumpgate chart gate uses) minus the central body, per the readout's
* rule: planets, stations, gates.
*/
import { navPoints } from '../research/SystemCategory.js';
/**
* The chart's bounding frame from a list of objects.
*
* @param {Array<{x:number, y:number, radius:number}>} objects — the system's
* objects (any discoverable set; `radius` = the object's extent from its
* center — planet disc radius, station/gate keepout, cluster bound)
* @param {number} [padding=1024] — px of margin added on every edge
* @returns {{minX:number, minY:number, maxX:number, maxY:number,
* w:number, h:number, cx:number, cy:number}}
* (a degenerate empty list yields a `2×padding` box about the origin)
*/
export function chartBounds(objects, padding = 1024) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const o of objects ?? []) {
if (!o || typeof o.x !== 'number' || typeof o.y !== 'number') continue;
const r = Math.max(0, Number(o.radius) || 0);
if (o.x - r < minX) minX = o.x - r;
if (o.y - r < minY) minY = o.y - r;
if (o.x + r > maxX) maxX = o.x + r;
if (o.y + r > maxY) maxY = o.y + r;
}
if (!Number.isFinite(minX) || !Number.isFinite(maxX) || !Number.isFinite(minY) || !Number.isFinite(maxY)) {
// No objects: a zero-size box about the origin (the padding then makes
// the chart a 2×padding box about it).
minX = 0;
minY = 0;
maxX = 0;
maxY = 0;
}
minX -= padding;
minY -= padding;
maxX += padding;
maxY += padding;
return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY, cx: (minX + maxX) / 2, cy: (minY + maxY) / 2 };
}
/**
* Fit a world-space rect into a plate of `w × h`, centred, uniform scale.
*
* @param {ReturnType<typeof chartBounds>} bounds
* @param {number} w — plate width (px)
* @param {number} h — plate height (px)
* @returns {{scale:number, ox:number, oy:number, toX:(x:number)=>number,
* toY:(y:number)=>number}}
* `toX`/`toY` map world → plate; `scale` = plate px per world px.
*/
export function fitToRect(bounds, w = 100, h = 100) {
const scale = Math.min(w, h) / Math.max(1, Math.max(bounds.w, bounds.h));
const ox = w / 2 - bounds.cx * scale;
const oy = h / 2 - bounds.cy * scale;
return {
scale,
ox,
oy,
toX: (x) => x * scale + ox,
toY: (y) => y * scale + oy,
};
}
/**
* The SYSTEM DISCOVERY share (data/map.json → stats): the player's found /
* total over the system's NAV objects — planets, free-space stations and
* jump gates (SystemCategory.navPoints minus the central body 'home',
* which is the chart's anchor rather than a discoverable count — and
* minus the asteroid clusters, which are the RESOURCE side of the readout).
*
* @param {object} discovery — the Discovery state (isDiscovered)
* @param {string} systemId
* @param {object} content — the generated system content
* @returns {{total:number, found:number, pct:number}} — `pct` is 0..1
* (0 when the system has no NAV objects, which the readout renders as —)
*/
export function navDiscoveryStats(discovery, systemId, content) {
const points = navPoints(content).filter((p) => p.kind !== 'home');
const canQuery = typeof discovery?.isDiscovered === 'function';
let found = 0;
for (const p of points) if (canQuery && discovery.isDiscovered(systemId, p.id)) found++;
const total = points.length;
return { total, found, pct: total > 0 ? found / total : 0 };
}
/**
* The SYSTEM RESOURCES share (data/map.json → stats): the player's found /
* total over the system's asteroid fields (content.asteroids — the only
* resource kind for now).
*
* @param {object} discovery — the Discovery state (isDiscovered)
* @param {string} systemId
* @param {object} content — the generated system content
* @returns {{total:number, found:number, pct:number}}
*/
export function resourceStats(discovery, systemId, content) {
const clusters = (content?.asteroids ?? []).filter((c) => c && typeof c.id === 'string');
const canQuery = typeof discovery?.isDiscovered === 'function';
let found = 0;
for (const c of clusters) if (canQuery && discovery.isDiscovered(systemId, c.id)) found++;
const total = clusters.length;
return { total, found, pct: total > 0 ? found / total : 0 };
}