/** * 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>} systemId → discovered object ids */ this.bySystem = new Map(); } isDiscovered(systemId, objectId) { const set = this.bySystem.get(systemId); return !!set && set.has(objectId); } /** * Mark every listed object in a system as discovered (no proximity * check) — the demo's "fully charted" start (dev/system-effects.test * verifies the behavior). Returns the number of objects marked. */ markAllDiscovered(systemId, objects) { let known = this.bySystem.get(systemId); if (!known) { known = new Set(); this.bySystem.set(systemId, known); } const before = known.size; for (const o of objects ?? []) known.add(o.id); return known.size - before; } /** * 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} 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; } }