214 lines
8.8 KiB
JavaScript
214 lines
8.8 KiB
JavaScript
/**
|
|
* SystemCategory — the SYSTEM research category: the ONE per-solar-system
|
|
* tech tree (research.json → categories, flagged `dynamic: true`).
|
|
*
|
|
* Every other category reads its tree from data/research/<id>.json —
|
|
* static data, same for every run. The SYSTEM category is different by
|
|
* design: its tree is built at runtime from the system the player is
|
|
* currently in, so the two techs are NAMED after that system:
|
|
*
|
|
* "{System} Map" granted on entry (duration 0,
|
|
* in `starting` — the scene
|
|
* unlocks it, ResearchState
|
|
* "already owned")
|
|
* └ "Unlock {System} Jumpgates" requires the map + EVERY NAV
|
|
* point of the system discovered
|
|
* (the availability hook below);
|
|
* 45 s (data/gates.json →
|
|
* activation.researchDuration);
|
|
* completing it activates the
|
|
* system's gates AND the return
|
|
* gates in the systems they
|
|
* connect to.
|
|
*
|
|
* Node ids embed the system id (`S000012_map`, `S000012_gates`) so a run
|
|
* can chart many systems in the one `system` category without collisions,
|
|
* and ResearchState's "category::id" keys stay unique per system.
|
|
*
|
|
* The tree carries two optional HOOKS the static trees don't need —
|
|
* ResearchModel consumes them (pure: the predicates are data the caller
|
|
* supplies, evaluated at paint time):
|
|
* tree.available(state, id) — extra availability gate (the chart gate:
|
|
* the jumpgate tech is researchable only while isComplete() holds);
|
|
* tree.lockNote(state, id) — the detail readout's LOCKED reason when
|
|
* the node is gated but its `requires` are all met.
|
|
*
|
|
* Pure (no Phaser) — Node-testable (dev/system-category.test.mjs).
|
|
*/
|
|
import { config } from '../config/Config.js';
|
|
|
|
/** The category id (data/research.json → categories[]). */
|
|
export const SYSTEM_CATEGORY = 'system';
|
|
|
|
/** The two node kinds. */
|
|
export const MAP_NODE = 'map';
|
|
export const GATES_NODE = 'gates';
|
|
|
|
/** A system's tech node id — unique per system (the category is shared). */
|
|
export function systemNodeId(systemId, kind) {
|
|
return `${systemId}_${kind}`;
|
|
}
|
|
|
|
const fillTemplate = (template, systemName) =>
|
|
String(template ?? '').split('{system}').join(systemName);
|
|
|
|
/**
|
|
* Build the SYSTEM category's tree for one system.
|
|
*
|
|
* @param {object} o
|
|
* @param {string} o.systemId — the system record's id (node-id prefix)
|
|
* @param {string} o.systemName — the system name (labels + copy)
|
|
* @param {string} [o.accent] — the category accent (registry, research.json)
|
|
* @param {() => boolean} [o.isComplete] — live chart check: true once
|
|
* EVERY NAV point of the system is discovered (the scene wires it to
|
|
* its Discovery state; absent = the chart never completes)
|
|
* @returns {object} a ResearchModel tree (id/label/accent/nodes/order/
|
|
* starting) + the optional `available` / `lockNote` hooks.
|
|
*/
|
|
export function buildSystemTree(o = {}) {
|
|
const systemId = String(o.systemId ?? '');
|
|
const systemName = String(o.systemName ?? systemId);
|
|
const mapId = systemNodeId(systemId, MAP_NODE);
|
|
const gatesId = systemNodeId(systemId, GATES_NODE);
|
|
const duration = Math.max(0, Number(config.get('gates.activation.researchDuration', 45)) || 0);
|
|
const isComplete = typeof o.isComplete === 'function' ? o.isComplete : () => false;
|
|
|
|
const chartLockedNote =
|
|
'NAV CHART INCOMPLETE — DISCOVER ALL PLANETS, STATIONS AND JUMP GATES OF ' +
|
|
systemName.toUpperCase();
|
|
|
|
const tree = {
|
|
id: SYSTEM_CATEGORY,
|
|
label: 'System',
|
|
accent: typeof o.accent === 'string' && o.accent ? o.accent : '#5fd4ff',
|
|
nodes: {
|
|
[mapId]: {
|
|
label: fillTemplate(config.get('gates.activation.mapTech.label', '{system} Map'), systemName),
|
|
description: fillTemplate(
|
|
config.get(
|
|
'gates.activation.mapTech.description',
|
|
'Added the Solar System of {system} to the onboard NAV System. Discover all NAV points to unlock the system Jumpgates.',
|
|
),
|
|
systemName,
|
|
),
|
|
icon: 'map',
|
|
duration: 0, // granted on entry — never researched
|
|
requires: [],
|
|
unlocks: { builds: [], research: [gatesId] },
|
|
effects: {},
|
|
},
|
|
[gatesId]: {
|
|
label: fillTemplate(config.get('gates.activation.gatesTech.label', 'Unlock {system} Jumpgates'), systemName),
|
|
description: fillTemplate(
|
|
config.get(
|
|
'gates.activation.gatesTech.description',
|
|
"With system NAV data complete, we have enough information to plot courses through this system's jumpgates.",
|
|
),
|
|
systemName,
|
|
),
|
|
icon: 'gate',
|
|
duration,
|
|
requires: [mapId],
|
|
unlocks: { builds: [], research: [] },
|
|
// The scene's effects seam reads this (GameScene._applyResearchEffects):
|
|
// activate the system's gates + the linked systems' return gates.
|
|
effects: { activateGates: true },
|
|
},
|
|
},
|
|
order: [mapId, gatesId],
|
|
starting: [mapId],
|
|
};
|
|
|
|
// The hooks (see the header) — the scene's live chart check drives both.
|
|
tree.available = (_state, id) => (id === gatesId ? isComplete() : true);
|
|
tree.lockNote = (_state, id) => (id === gatesId && !isComplete() ? chartLockedNote : null);
|
|
return tree;
|
|
}
|
|
|
|
/**
|
|
* A system's NAV points — the discovery ids the scene's
|
|
* discoverableObjects() uses for its discoverable set, minus the rocks
|
|
* (asteroid clusters are objects, not NAV points): the central body
|
|
* (every system — the scene's 'home' id, the player's home world in the
|
|
* starting system and the system's central world elsewhere), every
|
|
* planet (its name), every free-space station (its settlement id),
|
|
* every jump gate (its gate id).
|
|
*
|
|
* @param {object} content — a generated system content (ensureContent)
|
|
* @returns {string[]} the discoverable ids to check (order: stable)
|
|
*/
|
|
export function navPointIds(content) {
|
|
const ids = [];
|
|
ids.push('home'); // the central body — discoverable in every system
|
|
for (const p of content?.planets ?? []) if (p && typeof p.name === 'string') ids.push(p.name);
|
|
for (const s of content?.settlements ?? []) {
|
|
if (s && s.anchor?.type === 'space' && typeof s.id === 'string') ids.push(s.id);
|
|
}
|
|
for (const j of content?.jumps ?? []) if (j && typeof j.id === 'string') ids.push(j.id);
|
|
return ids;
|
|
}
|
|
|
|
/**
|
|
* Is the system fully charted? Every NAV point discovered
|
|
* (js/galaxy/Discovery.js — `discovery.isDiscovered(systemId, id)`).
|
|
* Vacuously true for a content with no NAV points (a degenerate
|
|
* one-system galaxy with no gates).
|
|
*
|
|
* @param {object} discovery — the scene's Discovery state
|
|
* @param {string} systemId
|
|
* @param {object} content
|
|
* @returns {boolean}
|
|
*/
|
|
export function isNavComplete(discovery, systemId, content) {
|
|
if (!discovery || typeof discovery.isDiscovered !== 'function') return false;
|
|
return navPointIds(content).every((id) => discovery.isDiscovered(systemId, id));
|
|
}
|
|
|
|
/**
|
|
* The activation keys one completed "Unlock {System} Jumpgates" grants:
|
|
* every gate in `systemId` (keyed "<systemId>><destination>") AND the
|
|
* RETURN gates in the systems those destinations connect back to —
|
|
* the gate in `<destination>` whose destination is `systemId`.
|
|
*
|
|
* Pure: computed from the jump network alone (no content generation).
|
|
*
|
|
* @param {object} galaxy — the Galaxy (reads galaxy.jumpNetwork.gates:
|
|
* Map systemId → destination ids)
|
|
* @param {string} systemId
|
|
* @returns {string[]} activation keys (deduplicated, stable order)
|
|
*/
|
|
export function activationKeys(galaxy, systemId) {
|
|
const gates = galaxy?.jumpNetwork?.gates;
|
|
if (!gates || typeof gates.get !== 'function') return [];
|
|
const out = [];
|
|
const push = (k) => {
|
|
if (k && !out.includes(k)) out.push(k);
|
|
};
|
|
for (const dest of gates.get(systemId) ?? []) {
|
|
push(`${systemId}>${dest}`);
|
|
if ((gates.get(dest) ?? []).includes(systemId)) push(`${dest}>${systemId}`);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Flip a generated system's gate records to ACTIVE for every activation
|
|
* key in `activated` (the "<systemId>><destination>" keys). Idempotent;
|
|
* returns how many gates changed.
|
|
*
|
|
* @param {object} content — the system's generated content
|
|
* @param {string} systemId — content's system id (the cache key)
|
|
* @param {Set<string>} activated — the run's activation keys
|
|
* @returns {number}
|
|
*/
|
|
export function applyActivation(content, systemId, activated) {
|
|
let n = 0;
|
|
for (const j of content?.jumps ?? []) {
|
|
if (j && typeof j.to === 'string' && !j.active && activated?.has(`${systemId}>${j.to}`)) {
|
|
j.active = true;
|
|
n++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|