orbit/js/galaxy/Discovery.js

85 lines
3.0 KiB
JavaScript

/**
* Discovery — which objects in which system the player has found.
*
* The rule (data/game.json → discovery.distance): when the player's ship
* comes within that many pixels of an object's EDGE (edge-to-edge), the
* object counts as DISCOVERED — and stays discovered for the rest of the
* game. That's what feeds the off-screen compass (js/ui/DiscoveryCompass.js)
* with "where I've been" arrows.
*
* Pure — no Phaser — so it's testable in Node (dev/discovery.test.mjs)
* and trivially serializable for saves (toJSON / fromJSON). The scene
* keeps one instance in the shared registry, so discovery survives a
* scene restart (and later, jumps between systems: state is per-system).
*
* const disc = new Discovery(540);
* const fresh = disc.check('S000001', ship.x, ship.y, objects);
* if (fresh.length) showACompassPing(fresh[0]);
* if (disc.isDiscovered('S000001', obj.id)) ...
*/
export class Discovery {
/** @param {number} distance — the edge-to-edge discovery radius (px) */
constructor(distance = 540) {
if (!(distance >= 0)) throw new Error(`Discovery distance must be >= 0 (got ${distance})`);
this.distance = distance;
/** @type {Map<string, Set<string>>} systemId → discovered object ids */
this.bySystem = new Map();
}
isDiscovered(systemId, objectId) {
const set = this.bySystem.get(systemId);
return !!set && set.has(objectId);
}
/**
* Proximity check against a list of objects.
*
* @param {string} systemId — the system the objects live in
* @param {number} sx — ship world x
* @param {number} sy — ship world y
* @param {Array<{id: string, x: number, y: number, radius: number}>} objects
* @returns {Array<object>} the NEWLY discovered objects (a subset of
* `objects`, in the order given) — empty when nothing new is found.
*/
check(systemId, sx, sy, objects) {
let known = this.bySystem.get(systemId);
if (!known) {
known = new Set();
this.bySystem.set(systemId, known);
}
const fresh = [];
for (const o of objects) {
if (known.has(o.id)) continue;
const dx = sx - o.x;
const dy = sy - o.y;
// "Within `distance` px of the edge" = center distance <= radius + distance.
if (dx * dx + dy * dy <= (o.radius + this.distance) * (o.radius + this.distance)) {
known.add(o.id);
fresh.push(o);
}
}
return fresh;
}
/** @returns {string[]} the discovered object ids in `systemId` (may be empty) */
discoveredIds(systemId) {
return [...(this.bySystem.get(systemId) ?? [])];
}
toJSON() {
const bySystem = {};
for (const [id, set] of this.bySystem) bySystem[id] = [...set];
return { distance: this.distance, bySystem };
}
/** Restore state saved with toJSON() (e.g. from a save file later). */
static fromJSON(data) {
const d = new Discovery(data?.distance ?? 540);
const bySystem = data?.bySystem ?? {};
for (const [id, ids] of Object.entries(bySystem)) {
if (Array.isArray(ids)) d.bySystem.set(id, new Set(ids));
}
return d;
}
}