Fix jumpgate activation bug and add NAV chart diagnostics
- `_applyResearchEffects` now receives the node id explicitly instead of reading `node.id`, which is undefined (the id is the key in the tree's `nodes` map, not a field on the object) — this was silently skipping `activateGates` so researched jumpgates stayed dark - Add `systemIdOfGatesNode()` as the pure inverse of `systemNodeId()` for the gates node, with round-trip tests - Reconcile gate activation in `_onEnterSystem()`: if the gates tech is already unlocked, re-run `_activateSystemJumpgates` (idempotent) so a missed activation self-heals on the next entry/jump/reload - Make `_activateSystemJumpgates` idempotent by tracking fresh keys and early-returning when everything is already online - Add `navPoints()` and `navChart()` to SystemCategory.js as pure, testable cores for the jumpgate unlock condition (discovered/missing split over all NAV points) - Add `js/dev/NavDiag.js` with `orbitNav()` / `orbitNavBrief()` console commands that print the live NAV chart and which objects are still undiscovered, installed by `main.js` on every page load - Update `data/landing.json` to reference the correct gasgiant surface/shop videos for slot 03 - Add tests for `navPoints`, `navChart`, and `systemIdOfGatesNode` - Document the activation wiring bug, the `orbitNav()` diagnostic, and the new pure functions in PROJECT_NOTES.md
This commit is contained in:
parent
5a67dbb206
commit
ff557f87ba
Binary file not shown.
Binary file not shown.
|
|
@ -48,7 +48,7 @@
|
|||
{ "land": "terran-land-03.mp4", "surface": "terran-surface-03.mp4", "takeoff": "terran-takeoff-03.mp4", "shop": "terran-shop-03.mp4" },
|
||||
{ "land": "gasgiant-land-01.mp4", "surface": "gasgiant-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "gasgiant-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-02.mp4", "surface": "gasgiant-surface-02.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "gasgiant-shop-02.mp4" },
|
||||
{ "land": "gasgiant-land-03.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-03.mp4", "surface": "terran-surface-03.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "gasgiant-shop-03.mp4" },
|
||||
{ "land": "gasgiant-land-01.mp4", "surface": "gasgiant-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-02.mp4", "surface": "gasgiant-surface-02.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "rocky-land-03.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "terran-shop-01.mp4" }
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ check('research: the SYSTEM category is registered + dynamic',
|
|||
const { config } = await import('../js/config/Config.js');
|
||||
config.init({ research, gates });
|
||||
const {
|
||||
SYSTEM_CATEGORY, MAP_NODE, GATES_NODE, systemNodeId,
|
||||
buildSystemTree, navPointIds, isNavComplete, activationKeys, applyActivation,
|
||||
SYSTEM_CATEGORY, MAP_NODE, GATES_NODE, systemNodeId, systemIdOfGatesNode,
|
||||
buildSystemTree, navPoints, navPointIds, navChart, isNavComplete, activationKeys, applyActivation,
|
||||
} = await import('../js/research/SystemCategory.js');
|
||||
const { issues, layoutTree, isAvailable, missingRequires, unlockIssues } = await import('../js/research/ResearchModel.js');
|
||||
const { ResearchState } = await import('../js/research/ResearchState.js');
|
||||
|
|
@ -168,6 +168,44 @@ check('nav: complete once EVERY NAV point is discovered',
|
|||
isNavComplete(makeDiscovery(['home', 'K-1', 'K-2', `${SYS}-s1`, `${SYS}-j2`, `${SYS}-j1`]), SYS, content) === true);
|
||||
check('nav: a missing discovery state never counts as complete', isNavComplete(null, SYS, content) === false);
|
||||
|
||||
// navPoints / navChart — the diagnostic's pure core (orbitNav builds on this)
|
||||
check('navPoints: each NAV point carries its kind (home/planet/station/gate)',
|
||||
JSON.stringify(navPoints(content)) === JSON.stringify([
|
||||
{ id: 'home', kind: 'home' },
|
||||
{ id: 'K-1', kind: 'planet' }, { id: 'K-2', kind: 'planet' },
|
||||
{ id: `${SYS}-s1`, kind: 'station' },
|
||||
{ id: `${SYS}-j1`, kind: 'gate' }, { id: `${SYS}-j2`, kind: 'gate' },
|
||||
]));
|
||||
|
||||
const chartMissing = navChart(makeDiscovery(['home', 'K-1', 'K-2', `${SYS}-s1`, `${SYS}-j1`]), SYS, content);
|
||||
check('navChart: reports the discovered/missing split (5 of 6, j2 still out)',
|
||||
chartMissing.discovered === 5 && chartMissing.total === 6 && chartMissing.complete === false
|
||||
&& JSON.stringify(chartMissing.missing) === JSON.stringify([`${SYS}-j2`])
|
||||
&& chartMissing.points.find((p) => p.id === `${SYS}-j2`)?.discovered === false);
|
||||
|
||||
const chartDone = navChart(makeDiscovery(['home', 'K-1', 'K-2', `${SYS}-s1`, `${SYS}-j1`, `${SYS}-j2`]), SYS, content);
|
||||
check('navChart: complete once every NAV point is discovered (6 of 6)',
|
||||
chartDone.discovered === 6 && chartDone.total === 6 && chartDone.complete === true && chartDone.missing.length === 0);
|
||||
|
||||
const chartNoState = navChart(null, SYS, content);
|
||||
check('navChart: a missing discovery state shows nothing discovered (0 of 6)',
|
||||
chartNoState.discovered === 0 && chartNoState.total === 6 && chartNoState.complete === false);
|
||||
|
||||
// node id <-> system id — the "researched but the gates stay dark" regression.
|
||||
// A node OBJECT has no `id` field (its id is the key in the tree's `nodes`
|
||||
// map), so the scene must recover the system id from the id it's PASSED, not
|
||||
// from `node.id` (which is undefined). systemIdOfGatesNode is that pure core.
|
||||
check('systemIdOfGatesNode: recovers the system id from a gates node id',
|
||||
systemIdOfGatesNode(`${SYS}_gates`) === SYS);
|
||||
check('systemIdOfGatesNode: nullish id -> empty string (the scene guards on it)',
|
||||
systemIdOfGatesNode(undefined) === '' && systemIdOfGatesNode(null) === '' && systemIdOfGatesNode('') === '');
|
||||
check('systemIdOfGatesNode: a NON-gates node id is left as-is (only the _gates suffix is stripped)',
|
||||
systemIdOfGatesNode(`${SYS}_map`) === `${SYS}_map`);
|
||||
// The round trip the scene relies on: build the id, then recover the id back.
|
||||
check('round trip: systemNodeId() then systemIdOfGatesNode() yields the system id',
|
||||
systemIdOfGatesNode(systemNodeId(SYS, GATES_NODE)) === SYS
|
||||
&& systemIdOfGatesNode(systemNodeId('S000137', GATES_NODE)) === 'S000137');
|
||||
|
||||
// -- activation: the system's gates + the linked systems' RETURN gates ----
|
||||
const galaxy = {
|
||||
jumpNetwork: {
|
||||
|
|
|
|||
|
|
@ -423,6 +423,27 @@ one shared category without collisions:
|
|||
`activationKeys`), and per the gate ACTIVITY rule each activated gate
|
||||
anchors a level-1 tether at its own position.
|
||||
|
||||
**Diagnosing "I charted everything but it stays locked":** the console
|
||||
command `orbitNav()` (DevTools → Console, space view — `js/dev/NavDiag.js`,
|
||||
installed by `main.js`) prints this system's live NAV chart: every NAV
|
||||
point with a ✓/✗, the discovered/total count, and exactly which objects
|
||||
are still undiscovered. `orbitNavBrief()` gives the one-line summary.
|
||||
It builds on the pure `navChart()` core above.
|
||||
|
||||
**Activation wiring (a real bug lived here):** completing the tech runs
|
||||
`GameScene._applyResearchEffects`, whose `activateGates` branch recovers
|
||||
the system id from the **node id** it's passed — via the pure
|
||||
`systemIdOfGatesNode(nodeId)` (`S000137_gates` → `S000137`). A node
|
||||
**object** carries no `id` field (its id is the key in the tree's
|
||||
`nodes` map), so reading `node.id` yields `undefined` and the activation
|
||||
silently never ran ("researched the jumpgates but they stay dark" — it
|
||||
was *not* a name/case mismatch; the id lookup is by system id, never by
|
||||
name). `GameScene._onEnterSystem()` now **reconciles** on every entry:
|
||||
if the system's gates tech is unlocked it re-runs
|
||||
`_activateSystemJumpgates` (idempotent + silent when already online),
|
||||
so a missed activation self-heals on the next entry/jump/reload without
|
||||
a re-research.
|
||||
|
||||
The gates node's chart gate is an **availability HOOK**, not a `requires`
|
||||
edge (its requires are all met at grant — the gate is live-world state):
|
||||
the tree object carries optional `available(state, id)` / `lockNote(state, id)`
|
||||
|
|
@ -472,7 +493,14 @@ than follow-on tech. Each node's `unlocks` is the declaration side:
|
|||
- `js/research/SystemCategory.js` — PURE (no Phaser): the dynamic
|
||||
SYSTEM category. `buildSystemTree({systemId, systemName, accent,
|
||||
isComplete})` (the per-system tree + the `available`/`lockNote`
|
||||
hooks), `navPointIds(content)` (the chart's NAV points),
|
||||
hooks), `systemIdOfGatesNode(nodeId)` (recover the system id a gates
|
||||
node id encodes — the node object has no `id` field, so the scene passes
|
||||
the id in; the pure core of the activation wiring),
|
||||
`navPoints(content)` (the chart's NAV points + their kind),
|
||||
`navPointIds(content)` (the chart's NAV points, ids only),
|
||||
`navChart(discovery, systemId, content)` (the live discovered/missing
|
||||
split that gates the jumpgate tech — the pure core of the `orbitNav()`
|
||||
console diagnostic in `js/dev/NavDiag.js`),
|
||||
`isNavComplete(discovery, systemId, content)` (every NAV point
|
||||
discovered), `activationKeys(galaxy, systemId)` (the system's gates +
|
||||
the linked systems' return gates, from the jump network alone),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* js/dev/NavDiag.js — in-game diagnostics for the SYSTEM category's NAV chart.
|
||||
*
|
||||
* Installed by js/main.js on every real page load (index.html). Read-only and
|
||||
* defensive: it observes the live game and prints a report — it never mutates
|
||||
* state.
|
||||
*
|
||||
* The jumpgate tech unlocks only when EVERY NAV point of the current system
|
||||
* is discovered (js/research/SystemCategory.js — navChart). This tells you
|
||||
* exactly which ones are still missing, so "I visited everything but the
|
||||
* gates stay locked" resolves in one glance instead of by guesswork.
|
||||
*
|
||||
* Console commands (DevTools → Console, in the RUNNING game — the space view,
|
||||
* not a planet surface):
|
||||
*
|
||||
* orbitNav() → full chart (also logged). Copy-paste the returned
|
||||
* string when asking for help.
|
||||
* orbitNavBrief() → one-line summary.
|
||||
*
|
||||
* If `orbitNav` is NOT DEFINED in your console, the browser served an OLDER
|
||||
* js/main.js (stale module cache) — hard-reload with the cache bypassed
|
||||
* (Ctrl+Shift+R, or DevTools → Network → Disable cache) and check again. The
|
||||
* version marker below changes with each revision.
|
||||
*/
|
||||
import { config } from '../config/Config.js';
|
||||
import { navChart } from '../research/SystemCategory.js';
|
||||
|
||||
export const NAV_DIAG_V = 1;
|
||||
|
||||
const KIND_LABEL = {
|
||||
home: 'home world',
|
||||
planet: 'planet',
|
||||
station: 'space station',
|
||||
gate: 'jump gate',
|
||||
};
|
||||
|
||||
function sceneOf(key) {
|
||||
try {
|
||||
return globalThis.window?.game?.scene?.getScene(key) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current system's live NAV chart, with readable names + type labels
|
||||
* (resolved from the scene's discoverable set — the same names the compass
|
||||
* and discovery toasts show).
|
||||
*/
|
||||
function chartOf(gs) {
|
||||
const systemId = gs?.systemRecord?.id ?? null;
|
||||
const system = gs?.systemRecord?.name ?? systemId ?? '?';
|
||||
const chart = navChart(gs?.discovery, systemId, gs?.systemContent);
|
||||
const names = new Map((gs?.discoverableObjects?.() ?? []).map((o) => [o.id, o]));
|
||||
const points = chart.points.map((p) => {
|
||||
const o = names.get(p.id);
|
||||
const name = o?.name ?? (p.id === 'home' ? gs?.homeWorldName ?? p.id : p.id);
|
||||
return { ...p, name, typeLabel: o?.typeLabel ?? KIND_LABEL[p.kind] ?? p.kind };
|
||||
});
|
||||
return { system, systemId, chart, points };
|
||||
}
|
||||
|
||||
const ABSENT = `[orbit-nav v${NAV_DIAG_V}] GameScene absent — run this from the space view (not a planet surface)`;
|
||||
|
||||
/** One-line summary — the same facts a support request needs. */
|
||||
export function brief() {
|
||||
const gs = sceneOf('GameScene');
|
||||
if (!gs) {
|
||||
console.info(ABSENT);
|
||||
return ABSENT;
|
||||
}
|
||||
const { system, systemId, chart, points } = chartOf(gs);
|
||||
const missing = points.filter((p) => !p.discovered).map((p) => p.name);
|
||||
const state = chart.complete
|
||||
? 'COMPLETE → jumpgates RESEARCHABLE'
|
||||
: 'INCOMPLETE → jumpgates LOCKED';
|
||||
const line =
|
||||
`[orbit-nav v${NAV_DIAG_V}] ` +
|
||||
`system=${system}[${systemId ?? '?'}] ` +
|
||||
`chart=${chart.discovered}/${chart.total} ${state} ` +
|
||||
`missing=${JSON.stringify(missing)}`;
|
||||
console.info(line);
|
||||
return line;
|
||||
}
|
||||
|
||||
/** Full report. Logs it, returns the string (copy-paste friendly). */
|
||||
export function full() {
|
||||
const gs = sceneOf('GameScene');
|
||||
if (!gs) {
|
||||
console.info(ABSENT);
|
||||
return ABSENT;
|
||||
}
|
||||
const { system, systemId, chart, points } = chartOf(gs);
|
||||
const dist = config.get('game.discovery.distance', 540);
|
||||
const state = chart.complete
|
||||
? 'COMPLETE — the jumpgate tech should be RESEARCHABLE'
|
||||
: 'INCOMPLETE — the jumpgate tech is LOCKED';
|
||||
const missing = points.filter((p) => !p.discovered);
|
||||
const lines = [
|
||||
`ORBIT NAV CHART v${NAV_DIAG_V}`,
|
||||
`[system] ${system} [${systemId ?? '?'}]`,
|
||||
`[chart] ${chart.discovered} of ${chart.total} NAV points discovered → ${state}`,
|
||||
];
|
||||
for (const p of points) {
|
||||
lines.push(
|
||||
` ${p.discovered ? '✓' : '✗'} ${p.kind.padEnd(7)} ${String(p.name).padEnd(22)} ` +
|
||||
`(${p.typeLabel})${p.discovered ? '' : ' ← undiscovered'}`,
|
||||
);
|
||||
}
|
||||
if (missing.length) {
|
||||
lines.push(`[missing] ${missing.map((p) => p.name).join(', ')}`);
|
||||
lines.push(
|
||||
`[tip] fly within ${dist} px of each missing object to discover it — ` +
|
||||
'jump gates and free-space stations are the ones easy to fly past',
|
||||
);
|
||||
} else {
|
||||
lines.push(`[ok] every NAV point is discovered — research the jumpgate tech in the console`);
|
||||
}
|
||||
const text = lines.join('\n');
|
||||
console.info(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
/** Install the console commands. Idempotent. */
|
||||
export function installNavDiag() {
|
||||
if (typeof globalThis === 'undefined') return;
|
||||
if (!globalThis.addEventListener) return;
|
||||
globalThis.orbitNav = () => full();
|
||||
globalThis.orbitNavBrief = () => brief();
|
||||
console.info(`[orbit-nav v${NAV_DIAG_V}] installed — type orbitNav() in the console to see this system's NAV chart`);
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { MenuScene } from './scenes/MenuScene.js';
|
|||
import { GameScene } from './scenes/GameScene.js';
|
||||
import { SurfaceScene } from './scenes/SurfaceScene.js';
|
||||
import { installBuildDiag } from './dev/BuildDiag.js';
|
||||
import { installNavDiag } from './dev/NavDiag.js';
|
||||
|
||||
/**
|
||||
* Orbit — entry point.
|
||||
|
|
@ -43,6 +44,11 @@ async function boot() {
|
|||
// the Build console opens.
|
||||
installBuildDiag();
|
||||
|
||||
// The SYSTEM category's NAV chart (orbitNav / orbitNavBrief). See
|
||||
// js/dev/NavDiag.js — which NAV points of the current system are still
|
||||
// undiscovered, i.e. why the jumpgate tech may still be locked.
|
||||
installNavDiag();
|
||||
|
||||
await awaitFonts();
|
||||
console.info('orbit — fonts ready (or timed out)');
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,20 @@ export function systemNodeId(systemId, kind) {
|
|||
return `${systemId}_${kind}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The inverse of systemNodeId() for the gates node: recover the system id a
|
||||
* research node id encodes ("" when `nodeId` is nullish). A node OBJECT
|
||||
* carries no `id` field — its id is the KEY in the tree's `nodes` map — so
|
||||
* callers must pass the id in (from the window / ResearchState), not read
|
||||
* `node.id`. That was the "researched but the gates stay dark" bug.
|
||||
*
|
||||
* @param {string} nodeId — e.g. `S000137_gates` → `S000137`
|
||||
* @returns {string}
|
||||
*/
|
||||
export function systemIdOfGatesNode(nodeId) {
|
||||
return String(nodeId ?? '').replace(/_gates$/, '');
|
||||
}
|
||||
|
||||
const fillTemplate = (template, systemName) =>
|
||||
String(template ?? '').split('{system}').join(systemName);
|
||||
|
||||
|
|
@ -137,15 +151,57 @@ export function buildSystemTree(o = {}) {
|
|||
* @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);
|
||||
/**
|
||||
* The system's NAV points (with their KIND), in a stable order: the central
|
||||
* body ('home'), then the planets (by name), the free-space stations
|
||||
* (settlement id), and the jump gates (gate id). Asteroid clusters are NOT
|
||||
* NAV points — they're objects you can mine, not chart waypoints.
|
||||
*
|
||||
* @param {object} content — a generated system content (ensureContent)
|
||||
* @returns {Array<{id:string, kind:'home'|'planet'|'station'|'gate'>}>
|
||||
*/
|
||||
export function navPoints(content) {
|
||||
const out = [{ id: 'home', kind: 'home' }]; // the central body — discoverable in every system
|
||||
for (const p of content?.planets ?? []) if (p && typeof p.name === 'string') out.push({ id: p.name, kind: 'planet' });
|
||||
for (const s of content?.settlements ?? []) {
|
||||
if (s && s.anchor?.type === 'space' && typeof s.id === 'string') ids.push(s.id);
|
||||
if (s && s.anchor?.type === 'space' && typeof s.id === 'string') out.push({ id: s.id, kind: 'station' });
|
||||
}
|
||||
for (const j of content?.jumps ?? []) if (j && typeof j.id === 'string') ids.push(j.id);
|
||||
return ids;
|
||||
for (const j of content?.jumps ?? []) if (j && typeof j.id === 'string') out.push({ id: j.id, kind: 'gate' });
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The NAV-point discovery ids, in a stable order (see navPoints). */
|
||||
export function navPointIds(content) {
|
||||
return navPoints(content).map((p) => p.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The live NAV chart for one system — which NAV points are discovered and
|
||||
* which are still missing (the exact set that gates the jumpgate tech).
|
||||
* Pure + Node-testable; the console diagnostic `orbitNav()` builds on it.
|
||||
*
|
||||
* @param {object} discovery — the scene's Discovery state (isDiscovered)
|
||||
* @param {string} systemId — the system to chart
|
||||
* @param {object} content — that system's generated content
|
||||
* @returns {{
|
||||
* total:number, discovered:number, complete:boolean, missing:string[],
|
||||
* points:Array<{id:string, kind:string, discovered:boolean}>
|
||||
* }}
|
||||
*/
|
||||
export function navChart(discovery, systemId, content) {
|
||||
const canQuery = typeof discovery?.isDiscovered === 'function';
|
||||
const points = navPoints(content).map((p) => ({
|
||||
...p,
|
||||
discovered: canQuery ? !!discovery.isDiscovered(systemId, p.id) : false,
|
||||
}));
|
||||
const missing = points.filter((p) => !p.discovered);
|
||||
return {
|
||||
total: points.length,
|
||||
discovered: points.length - missing.length,
|
||||
complete: missing.length === 0,
|
||||
missing: missing.map((p) => p.id),
|
||||
points,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ import { categories, loadCategory, isAvailable, buildDefs } from '../research/Re
|
|||
import {
|
||||
SYSTEM_CATEGORY,
|
||||
MAP_NODE,
|
||||
GATES_NODE,
|
||||
systemNodeId,
|
||||
systemIdOfGatesNode,
|
||||
buildSystemTree,
|
||||
isNavComplete,
|
||||
activationKeys,
|
||||
|
|
@ -1928,7 +1930,7 @@ export class GameScene extends Phaser.Scene {
|
|||
if (durMs <= 0) {
|
||||
// Instant tech (duration 0) — applies the moment it's requested.
|
||||
state.unlock(catId, id);
|
||||
this._applyResearchEffects(node.effects, node);
|
||||
this._applyResearchEffects(node.effects, node, id);
|
||||
this._completeResearchFx(catId, id, node);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1948,7 +1950,7 @@ export class GameScene extends Phaser.Scene {
|
|||
if (!state) return;
|
||||
state.unlock(catId, id);
|
||||
const node = this._treeFor(catId)?.nodes?.[id];
|
||||
if (node?.effects) this._applyResearchEffects(node.effects, node);
|
||||
if (node?.effects) this._applyResearchEffects(node.effects, node, id);
|
||||
this._completeResearchFx(catId, id, node);
|
||||
}
|
||||
|
||||
|
|
@ -1967,6 +1969,14 @@ export class GameScene extends Phaser.Scene {
|
|||
_onEnterSystem() {
|
||||
if (!this.researchState || !this.systemRecord) return;
|
||||
this.researchState.unlock(SYSTEM_CATEGORY, systemNodeId(this.systemRecord.id, MAP_NODE));
|
||||
// Invariant: if this system's gates tech is researched, its gates are
|
||||
// active. Reconcile on entry — idempotent (silent when already online),
|
||||
// and it recovers any system whose activation was missed at completion
|
||||
// without a re-research (the node.id bug) or a save round-trip.
|
||||
const gatesId = systemNodeId(this.systemRecord.id, GATES_NODE);
|
||||
if (this.researchState.isUnlocked(SYSTEM_CATEGORY, gatesId)) {
|
||||
this._activateSystemJumpgates(this.systemRecord.id);
|
||||
}
|
||||
this.researchWindow?.refresh();
|
||||
}
|
||||
|
||||
|
|
@ -1994,7 +2004,12 @@ export class GameScene extends Phaser.Scene {
|
|||
_activateSystemJumpgates(sysId) {
|
||||
const level = Math.max(1, Math.floor(Number(config.get('gates.activation.tetherLevel', 1)) || 1));
|
||||
const keys = activationKeys(this.galaxy, sysId);
|
||||
// Idempotent: only the NOT-yet-active keys count as new. Re-entering a
|
||||
// system (a jump, a load, a refresh) re-runs this — the early return
|
||||
// keeps it silent when everything is already online.
|
||||
const fresh = keys.filter((k) => !this.activatedGates.has(k));
|
||||
for (const k of keys) this.activatedGates.add(k);
|
||||
if (fresh.length === 0) return;
|
||||
|
||||
// Current system — the gate entities wake + anchor their tethers.
|
||||
for (const gt of this.systemGates ?? []) {
|
||||
|
|
@ -2026,14 +2041,16 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
/**
|
||||
* Apply a tech's effects (node.effects, data/research/<cat>.json — or
|
||||
* the SYSTEM category's live tree). Known shapes:
|
||||
* the SYSTEM category's live tree). `id` is the node's id (the key in the
|
||||
* tree's `nodes` map — the node object itself carries no `id` field).
|
||||
* Known shapes:
|
||||
* tether { level: N } → the home world's tether field strengthens
|
||||
* capability "flag" → a scene capability set (future systems read it)
|
||||
* activateGates true → the SYSTEM category: the system's jump gates
|
||||
* + the linked systems' return gates go ACTIVE
|
||||
* Unknown shapes are logged and skipped — data can lead code a step.
|
||||
*/
|
||||
_applyResearchEffects(effects, node) {
|
||||
_applyResearchEffects(effects, node, id) {
|
||||
if (!effects || typeof effects !== 'object') return;
|
||||
for (const [type, spec] of Object.entries(effects)) {
|
||||
if (type === 'tether' && spec && Number.isFinite(Number(spec.level))) {
|
||||
|
|
@ -2048,11 +2065,13 @@ export class GameScene extends Phaser.Scene {
|
|||
this.researchCapabilities = this.researchCapabilities ?? new Set();
|
||||
this.researchCapabilities.add(spec);
|
||||
} else if (type === 'activateGates' && spec) {
|
||||
// SYSTEM category — the node id is "<systemId>_gates".
|
||||
const sysId = String(node?.id ?? '').replace(/_gates$/, '');
|
||||
// SYSTEM category — the node id is "<systemId>_gates". The node object
|
||||
// carries no `id` field (it's the key in the tree's `nodes` map), so
|
||||
// `id` (passed in) is the source of truth — NOT `node.id`.
|
||||
const sysId = systemIdOfGatesNode(id);
|
||||
if (sysId) this._activateSystemJumpgates(sysId);
|
||||
} else {
|
||||
console.warn(`[orbit] research: unknown effect ${type}`, spec, node?.id);
|
||||
console.warn(`[orbit] research: unknown effect ${type}`, spec, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue