Tune jumpgate activation research duration to 20 s

- Reduce data/gates.json activation.researchDuration from 45 s to 20 s so the "Unlock Jumpgates" tech completes faster once all NAV points are discovered.
- Update SystemCategory.js doc comment and fallback default to match the new duration.
- Adjust system-category.test.mjs assertions (duration, save/restore remaining-time math) to the 20 s value.
This commit is contained in:
Brian Fertig 2026-09-30 15:36:27 -06:00
parent 20d8fcb0ef
commit 6ecf199da5
3 changed files with 12 additions and 12 deletions

View File

@ -36,7 +36,7 @@
}, },
"activation": { "activation": {
"_comment": "ACTIVATION (the SYSTEM research category — js/research/SystemCategory.js builds the per-system tree from this): '{system} Map' is granted the moment the player is in the system (duration 0 — 'starting'), and 'Unlock Jumpgates' (researchDuration, in research.timeUnit seconds) becomes researchable once EVERY NAV point of the system is discovered (the central body, all planets, all space stations, all jump gates — the scene's discoverable set minus the asteroid clusters; data/game.json → discovery.distance). Completing it activates the system's gates AND the return gates in the systems they connect to — and per the ACTIVITY rule above, each activated gate anchors a level-`tetherLevel` tether at its own position (a TETHER ANCHOR in its own right; that tether saves/restores with the run's tether list). The map label/description copy is templated: `{system}` is replaced with the system name. The gates tech's label is FIXED (no `{system}`) — the console's node button is too narrow for the system name; the map tech carries it.", "_comment": "ACTIVATION (the SYSTEM research category — js/research/SystemCategory.js builds the per-system tree from this): '{system} Map' is granted the moment the player is in the system (duration 0 — 'starting'), and 'Unlock Jumpgates' (researchDuration, in research.timeUnit seconds) becomes researchable once EVERY NAV point of the system is discovered (the central body, all planets, all space stations, all jump gates — the scene's discoverable set minus the asteroid clusters; data/game.json → discovery.distance). Completing it activates the system's gates AND the return gates in the systems they connect to — and per the ACTIVITY rule above, each activated gate anchors a level-`tetherLevel` tether at its own position (a TETHER ANCHOR in its own right; that tether saves/restores with the run's tether list). The map label/description copy is templated: `{system}` is replaced with the system name. The gates tech's label is FIXED (no `{system}`) — the console's node button is too narrow for the system name; the map tech carries it.",
"researchDuration": 45, "researchDuration": 20,
"tetherLevel": 1, "tetherLevel": 1,
"mapTech": { "mapTech": {
"label": "{system} Map", "label": "{system} Map",

View File

@ -7,7 +7,7 @@
* SYSTEM tab — categories flagged `dynamic` in research.json, built at * SYSTEM tab — categories flagged `dynamic` in research.json, built at
* runtime by js/research/SystemCategory.js; copy + duration in * runtime by js/research/SystemCategory.js; copy + duration in
* data/gates.json → activation): * data/gates.json → activation):
* - config: the activation section (45 s research, the two tech's * - config: the activation section (20 s research, the two tech's
* label/description templates with their `{system}` placeholder); * label/description templates with their `{system}` placeholder);
* - buildSystemTree: the tree contract (ids embed the system id, the * - buildSystemTree: the tree contract (ids embed the system id, the
* map tech granted on entry, the jumpgate tech gated by the chart), * map tech granted on entry, the jumpgate tech gated by the chart),
@ -44,7 +44,7 @@ const research = read('research.json');
check('gates: activation section present', !!gates.activation && typeof gates.activation === 'object'); check('gates: activation section present', !!gates.activation && typeof gates.activation === 'object');
check('gates: researchDuration is a positive number (seconds — research.timeUnit)', check('gates: researchDuration is a positive number (seconds — research.timeUnit)',
typeof gates.activation?.researchDuration === 'number' && gates.activation.researchDuration > 0); typeof gates.activation?.researchDuration === 'number' && gates.activation.researchDuration > 0);
check('gates: the 45 s brief duration', gates.activation?.researchDuration === 45); check('gates: the 20 s brief duration', gates.activation?.researchDuration === 20);
check('gates: tetherLevel ≥ 1 (an activated gate anchors a tether — ACTIVITY)', check('gates: tetherLevel ≥ 1 (an activated gate anchors a tether — ACTIVITY)',
Number.isInteger(gates.activation?.tetherLevel) && gates.activation.tetherLevel >= 1); Number.isInteger(gates.activation?.tetherLevel) && gates.activation.tetherLevel >= 1);
check('gates: mapTech label template names the system', check('gates: mapTech label template names the system',
@ -106,8 +106,8 @@ check('tree: map tech — icon from the procedural glyph roster', map.icon === '
check('tree: gates tech — the brief label + copy', check('tree: gates tech — the brief label + copy',
gNode.label === 'Unlock Jumpgates' && gNode.label === 'Unlock Jumpgates' &&
gNode.description === "With system NAV data complete, we have enough information to plot courses through this system's jumpgates."); gNode.description === "With system NAV data complete, we have enough information to plot courses through this system's jumpgates.");
check('tree: gates tech — 45 s, requires the map, the activation effect', check('tree: gates tech — 20 s, requires the map, the activation effect',
gNode.duration === 45 && JSON.stringify(gNode.requires) === JSON.stringify([mapId]) && gNode.effects?.activateGates === true); gNode.duration === 20 && JSON.stringify(gNode.requires) === JSON.stringify([mapId]) && gNode.effects?.activateGates === true);
check('tree: map tech declares the gates tech in its unlocks', JSON.stringify(map.unlocks?.research ?? []) === JSON.stringify([gatesId])); check('tree: map tech declares the gates tech in its unlocks', JSON.stringify(map.unlocks?.research ?? []) === JSON.stringify([gatesId]));
check('tree: issues(tree) is clean', Array.isArray(issues(tree)) && issues(tree).length === 0); check('tree: issues(tree) is clean', Array.isArray(issues(tree)) && issues(tree).length === 0);
@ -132,16 +132,16 @@ chartComplete = true;
check('hooks: chart complete → the gates tech is researchable', isAvailable(tree, state, gatesId)); check('hooks: chart complete → the gates tech is researchable', isAvailable(tree, state, gatesId));
// -- the full run: one project at a time, save/restore ------------------- // -- the full run: one project at a time, save/restore -------------------
state.start(SYSTEM_CATEGORY, gatesId, 45_000, 0); state.start(SYSTEM_CATEGORY, gatesId, 20_000, 0);
check('run: the in-flight project is the gates tech', state.getActive()?.id === gatesId); check('run: the in-flight project is the gates tech', state.getActive()?.id === gatesId);
check('run: a second project is refused (one at a time)', state.start(SYSTEM_CATEGORY, gatesId, 1000, 0) === false); check('run: a second project is refused (one at a time)', state.start(SYSTEM_CATEGORY, gatesId, 1000, 0) === false);
const saved = state.toJSON(20_000); // saved 20 s into the 45 s project const saved = state.toJSON(8_000); // saved 8 s into the 20 s project
const restored = new ResearchState(); const restored = new ResearchState();
for (const k of saved.unlocked) restored.unlock(...k.split('::')); for (const k of saved.unlocked) restored.unlock(...k.split('::'));
restored.restoreActive(saved.active, 900_000); // reloaded later restored.restoreActive(saved.active, 900_000); // reloaded later
check('run: save carries the remaining time (45 s − 20 s = 25 s)', saved.active?.remainingMs === 25_000); check('run: save carries the remaining time (20 s − 8 s = 12 s)', saved.active?.remainingMs === 12_000);
check('run: restore keeps the project in flight (25 s left)', check('run: restore keeps the project in flight (12 s left)',
restored.getActive()?.id === gatesId && restored.progress(900_000).fraction < 1 && restored.progress(925_000).fraction >= 1); restored.getActive()?.id === gatesId && restored.progress(900_000).fraction < 1 && restored.progress(912_000).fraction >= 1);
// -- the chart gate: NAV points + completion ------------------------------ // -- the chart gate: NAV points + completion ------------------------------
// A NON-home system (no homeName — its center is empty, the star is // A NON-home system (no homeName — its center is empty, the star is

View File

@ -14,7 +14,7 @@
* └ "Unlock Jumpgates" requires the map + EVERY NAV * └ "Unlock Jumpgates" requires the map + EVERY NAV
* point of the system discovered * point of the system discovered
* (the availability hook below); * (the availability hook below);
* 45 s (data/gates.json → * 20 s (data/gates.json →
* activation.researchDuration); * activation.researchDuration);
* completing it activates the * completing it activates the
* system's gates AND the return * system's gates AND the return
@ -122,7 +122,7 @@ export function buildSystemTree(o = {}) {
const systemName = String(o.systemName ?? systemId); const systemName = String(o.systemName ?? systemId);
const mapId = systemNodeId(systemId, MAP_NODE); const mapId = systemNodeId(systemId, MAP_NODE);
const gatesId = systemNodeId(systemId, GATES_NODE); const gatesId = systemNodeId(systemId, GATES_NODE);
const duration = Math.max(0, Number(config.get('gates.activation.researchDuration', 45)) || 0); const duration = Math.max(0, Number(config.get('gates.activation.researchDuration', 20)) || 0);
const isComplete = typeof o.isComplete === 'function' ? o.isComplete : () => false; const isComplete = typeof o.isComplete === 'function' ? o.isComplete : () => false;
// No system name — the detail panel has room, the node button doesn't: // No system name — the detail panel has room, the node button doesn't: