Add Cargo research category with quest and build-completion gates
- Introduce data/research/cargo.json: a 3-node chain (8 → 12 → 16 bays) where each node is a blueprint that unlocks the matching ship-hold build - Add `build` gate field to ResearchModel.js (attachBuildGates) so a node can require a named build to be completed before it becomes researchable, composing with the existing quest gate - Wire both gates in GameScene and ResearchWindow so node availability, lock notes, and the RESEARCH button reflect quest + build state - Gate cargo_bay_8 on the special_delivery quest; cargo_bay_12/16 on Improved/Advanced Mining Storage builds - Update builds.json to require the corresponding research nodes for cargo-bay-8/-12/-16 - Add comprehensive tests covering the cargo tree structure, pure DAG availability, live gate composition, and build-side lock-step
This commit is contained in:
parent
8810ec1b3d
commit
138096cd2b
|
|
@ -230,7 +230,7 @@
|
|||
"credits": 300
|
||||
},
|
||||
"duration": 20,
|
||||
"requires": [],
|
||||
"requires": ["cargo/cargo_bay_8"],
|
||||
"repeatable": false,
|
||||
"effects": {
|
||||
"cargo": {
|
||||
|
|
@ -251,7 +251,7 @@
|
|||
"credits": 1200
|
||||
},
|
||||
"duration": 30,
|
||||
"requires": [],
|
||||
"requires": ["cargo/cargo_bay_12"],
|
||||
"repeatable": false,
|
||||
"effects": {
|
||||
"cargo": {
|
||||
|
|
@ -272,7 +272,7 @@
|
|||
"credits": 3000
|
||||
},
|
||||
"duration": 40,
|
||||
"requires": [],
|
||||
"requires": ["cargo/cargo_bay_16"],
|
||||
"repeatable": false,
|
||||
"effects": {
|
||||
"cargo": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"research.json",
|
||||
"research/exploration.json",
|
||||
"research/mining.json",
|
||||
"research/cargo.json",
|
||||
"builds.json",
|
||||
"actionbar.json",
|
||||
"landing.json",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"categories": [
|
||||
{ "id": "system", "label": "System", "accent": "#5fd4ff", "dynamic": true },
|
||||
{ "id": "mining", "label": "Mining", "accent": "#ffc94d" },
|
||||
{ "id": "cargo", "label": "Cargo", "accent": "#7ce8a4" },
|
||||
{ "id": "exploration", "label": "Exploration", "accent": "#00e5ff" }
|
||||
],
|
||||
"_dynamic": "A category flagged `dynamic: true` has NO data/research/<id>.json — its tree is built at runtime per situation (js/research/SystemCategory.js: one tech pair per solar system, named after the system the player is in — its Map tech is granted on entry, its jumpgate tech researchable once every NAV point — planets, space stations, jump gates — is discovered, and activation flips the gates on, here and in the linked systems' return gates). Static categories keep the one-file-per-category rule; the tests exempt dynamic ones from the tree-file check.",
|
||||
|
|
@ -42,5 +43,6 @@
|
|||
},
|
||||
"effects": { "tether": { "level": 2 } }
|
||||
},
|
||||
"_questGate": "Optional node field — `quest: <quest id>`: a STORY-QUEST gate on top of `requires`. The named quest (data/quests.json) must have its reward CLAIMED (the dossier's CLAIM action — the same paid-out witness the run's chain uses to grant the next quest) before the node is researchable. The model stays pure: the scene supplies the live predicate, and js/research/ResearchModel.js → attachQuestGates wires it into the tree's optional `available`/`lockNote` hooks (isAvailable consults the hook like any extra gate — the SYSTEM category's chart gate is the same seam; the console's LOCKED readout shows the quest's title via lockNote). In use: mining/premium_arm + mining/advanced_storage wait on 'special_delivery' (data/research/mining.json)."
|
||||
"_questGate": "Optional node field — `quest: <quest id>`: a STORY-QUEST gate on top of `requires`. The named quest (data/quests.json) must have its reward CLAIMED (the dossier's CLAIM action — the same paid-out witness the run's chain uses to grant the next quest) before the node is researchable. The model stays pure: the scene supplies the live predicate, and js/research/ResearchModel.js → attachQuestGates wires it into the tree's optional `available`/`lockNote` hooks (isAvailable consults the hook like any extra gate — the SYSTEM category's chart gate is the same seam; the console's LOCKED readout shows the quest's title via lockNote). In use: mining/premium_arm + mining/advanced_storage (data/research/mining.json) and cargo/cargo_bay_8 (data/research/cargo.json) wait on 'special_delivery'.",
|
||||
"_buildGate": "Optional node field — `build: <build id>`: a BUILD-COMPLETION gate on top of `requires`. The named build (data/builds.json) must be BUILT before the node is researchable (for a ship upgrade, built anywhere — the scene's live predicate reads its BuildState built set). Same pure seam as the quest gate: js/research/ResearchModel.js → attachBuildGates wires it into the same optional `available`/`lockNote` hooks (the console's LOCKED readout shows `<BUILD LABEL> BUILD REQUIRED` via lockNote). The two gates compose (each is checked first in the order it was attached). In use: cargo/cargo_bay_12 waits on the mining-storage-improved build, cargo/cargo_bay_16 on mining-storage-advanced (data/research/cargo.json)."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"_comment": "CARGO — the player's trade capacity: the ship's hold, bay for bay (the 16-slot grid, data/economy.json → cargo.maxSlots). All three techs are BLUEPRINTS (effects: {}): the node's `unlocks.builds` declares the matching ship-hold build (data/builds.json → cargo-bay-8 / -12 / -16) and the build's `requires: [\"cargo/<node id>\"]` is the authoritative gate (dev/research-builds.test.mjs keeps both sides in lock-step). The hold's life is 0 at the run's start (data/ship.json), 4 by the bar scene (data/vignettes.json → effects.cargoHold), then this chain: 8 → 12 → 16. One node per bay count, a strict chain — cargo_bay_8 is the root; cargo_bay_12 requires it; cargo_bay_16 requires cargo_bay_12 (MAX wins — the biggest hold always supersedes, the build's effect never downgrades). On top of the DAG, the three tiers carry gates in the style of the mining Tier-3 techs (node fields, the scene wires the live predicates via attachQuestGates + attachBuildGates — js/research/ResearchModel.js): cargo_bay_8 waits on `quest: \"special_delivery\"` (the 'Special Delivery' reward must be CLAIMED — you learn to move cargo before you grow the hold), cargo_bay_12 on `build: \"mining-storage-improved\"` (the Improved Mining Storage build must be BUILT), cargo_bay_16 on `build: \"mining-storage-advanced\"` (the Advanced Mining Storage build BUILT). Node contract is the same as data/research/mining.json: `requires` = parents that must be RESEARCHED first (the DAG edges — authoritative); `unlocks` = { builds, research } declaration; `effects` = what the scene applies on completion (empty here — the build carries it); `starting` = already researched on a fresh run (none — everything is researched, never granted). duration is in research.timeUnit (seconds).",
|
||||
"starting": [],
|
||||
"nodes": {
|
||||
"cargo_bay_8": {
|
||||
"label": "Cargo - 8 Bays",
|
||||
"description": "The blueprint for the ship's first trade upgrade: the 8-bay hold. Once researched, the 8-bay bay can be built from any planet's console (300 credits, 20 s) — roughly 2× the goods per run, and the trade income to match.",
|
||||
"icon": "storage",
|
||||
"duration": 60,
|
||||
"requires": [],
|
||||
"quest": "special_delivery",
|
||||
"unlocks": { "builds": ["cargo-bay-8"], "research": ["cargo_bay_12"] },
|
||||
"effects": {}
|
||||
},
|
||||
"cargo_bay_12": {
|
||||
"label": "Cargo - 12 Bays",
|
||||
"description": "The blueprint for the 12-bay hold. Once researched, the 12-bay bay can be built (1200 credits, 30 s) — three quarters of a full hold in a single run, supersedes the 8-bay bay.",
|
||||
"icon": "storage",
|
||||
"duration": 60,
|
||||
"requires": ["cargo_bay_8"],
|
||||
"build": "mining-storage-improved",
|
||||
"unlocks": { "builds": ["cargo-bay-12"], "research": ["cargo_bay_16"] },
|
||||
"effects": {}
|
||||
},
|
||||
"cargo_bay_16": {
|
||||
"label": "Cargo - 16 Bays",
|
||||
"description": "The blueprint for the full 16-bay hold — a freighter's worth of goods in one run, the biggest the ship can carry. Once researched, the 16-bay bay can be built (3000 credits, 40 s), superseding every smaller bay.",
|
||||
"icon": "storage",
|
||||
"duration": 60,
|
||||
"requires": ["cargo_bay_12"],
|
||||
"build": "mining-storage-advanced",
|
||||
"unlocks": { "builds": ["cargo-bay-16"], "research": [] },
|
||||
"effects": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ const manifest = read('manifest.json');
|
|||
const research = read('research.json');
|
||||
const exploration = read('research/exploration.json');
|
||||
const mining = read('research/mining.json');
|
||||
const cargo = read('research/cargo.json');
|
||||
const quests = read('quests.json');
|
||||
const builds = read('builds.json');
|
||||
const actionbar = read('actionbar.json');
|
||||
|
|
@ -42,7 +43,7 @@ const hex = /^#[0-9a-fA-F]{6}$/;
|
|||
// ----------------------------------------------------------------------
|
||||
// 1. Manifest registration (section name = file basename)
|
||||
// ----------------------------------------------------------------------
|
||||
for (const f of ['research.json', 'research/exploration.json', 'research/mining.json', 'builds.json', 'actionbar.json']) {
|
||||
for (const f of ['research.json', 'research/exploration.json', 'research/mining.json', 'research/cargo.json', 'builds.json', 'actionbar.json']) {
|
||||
check(`manifest registers ${f}`, manifest.files.includes(f));
|
||||
}
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ check('research: one project at a time (maxConcurrent === 1)', research.maxConcu
|
|||
check('research: categories is a non-empty array', Array.isArray(research.categories) && research.categories.length > 0);
|
||||
check('research: every category has id/label/accent', research.categories.every((c) => typeof c.id === 'string' && typeof c.label === 'string' && hex.test(c.accent ?? '')));
|
||||
check('research: defaultCategory names a real category', (research.categories ?? []).some((c) => c.id === research.defaultCategory));
|
||||
check('research: tab order is System, Mining, Cargo, Exploration (Cargo between Mining and Exploration)', JSON.stringify(research.categories.map((c) => c.id)) === JSON.stringify(['system', 'mining', 'cargo', 'exploration']));
|
||||
|
||||
// every STATIC category in the registry has its own tree file in the
|
||||
// manifest. A category flagged `dynamic: true` (currently: `system`)
|
||||
|
|
@ -173,7 +175,7 @@ check('mining: the Tier-1 + Tier-2 techs carry no quest gate',
|
|||
// 4. The REAL code path — ResearchModel + ResearchState (pure modules)
|
||||
// ----------------------------------------------------------------------
|
||||
const { config } = await import('../js/config/Config.js');
|
||||
config.init({ research, exploration, mining, builds, actionbar, quests });
|
||||
config.init({ research, exploration, mining, cargo, builds, actionbar, quests });
|
||||
const { categories, loadCategory, issues, layoutTree, isAvailable, missingRequires } = await import('../js/research/ResearchModel.js');
|
||||
const { ResearchState } = await import('../js/research/ResearchState.js');
|
||||
const { questDef } = await import('../js/quests/QuestState.js');
|
||||
|
|
@ -294,6 +296,102 @@ const { attachQuestGates } = await import('../js/research/ResearchModel.js');
|
|||
})());
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 4c. The cargo tree — data/research/cargo.json (the Cargo tab, between
|
||||
// Mining and Exploration): 8 Bays → 12 Bays → 16 Bays, each a
|
||||
// blueprint for its ship-hold build (data/builds.json → cargo-bay-*).
|
||||
// ----------------------------------------------------------------------
|
||||
const cnodes = cargo.nodes ?? {};
|
||||
check('cargo: nodes is a non-empty map (3 techs — the 8 → 12 → 16 chain)', Object.keys(cnodes).length === 3);
|
||||
check('cargo: nothing is starting (all three must be researched)', (cargo.starting ?? []).length === 0);
|
||||
let cfieldsOk = true;
|
||||
for (const [id, n] of Object.entries(cnodes)) {
|
||||
if (!n || typeof n !== 'object') { cfieldsOk = false; continue; }
|
||||
if (typeof n.label !== 'string' || typeof n.description !== 'string' || !ICON_NAMES.includes(n.icon ?? 'diamond') || typeof n.duration !== 'number' || n.duration < 0) cfieldsOk = false;
|
||||
if (!Array.isArray(n.requires) || !n.requires.every((r) => typeof r === 'string' && cnodes[r])) cfieldsOk = false;
|
||||
if (!Array.isArray(n.unlocks?.builds) || !Array.isArray(n.unlocks?.research)) cfieldsOk = false;
|
||||
}
|
||||
check('cargo: every node has label/description/icon/duration/requires/unlocks', cfieldsOk);
|
||||
check('cargo: labels read Cargo - 8 / 12 / 16 Bays', cnodes.cargo_bay_8?.label === 'Cargo - 8 Bays' && cnodes.cargo_bay_12?.label === 'Cargo - 12 Bays' && cnodes.cargo_bay_16?.label === 'Cargo - 16 Bays');
|
||||
check('cargo: cargo_bay_8 is the root (no requires — the chain opens here)', (cnodes.cargo_bay_8?.requires ?? []).length === 0);
|
||||
check('cargo: cargo_bay_12 waits on cargo_bay_8', JSON.stringify(cnodes.cargo_bay_12?.requires ?? []) === JSON.stringify(['cargo_bay_8']));
|
||||
check('cargo: cargo_bay_16 waits on cargo_bay_12', JSON.stringify(cnodes.cargo_bay_16?.requires ?? []) === JSON.stringify(['cargo_bay_12']));
|
||||
check('cargo: 8 Bays declares its build (cargo-bay-8) + child (cargo_bay_12)', JSON.stringify(cnodes.cargo_bay_8?.unlocks ?? null) === JSON.stringify({ builds: ['cargo-bay-8'], research: ['cargo_bay_12'] }));
|
||||
check('cargo: 12 Bays declares its build (cargo-bay-12) + child (cargo_bay_16)', JSON.stringify(cnodes.cargo_bay_12?.unlocks ?? null) === JSON.stringify({ builds: ['cargo-bay-12'], research: ['cargo_bay_16'] }));
|
||||
check('cargo: 16 Bays declares its build (cargo-bay-16) and no child', JSON.stringify(cnodes.cargo_bay_16?.unlocks ?? null) === JSON.stringify({ builds: ['cargo-bay-16'], research: [] }));
|
||||
check('cargo: cargo_bay_8 gates on the special_delivery quest (reward CLAIMED first)', cnodes.cargo_bay_8?.quest === 'special_delivery');
|
||||
check('cargo: cargo_bay_12 gates on the mining-storage-improved build (BUILT first)', cnodes.cargo_bay_12?.build === 'mining-storage-improved');
|
||||
check('cargo: cargo_bay_16 gates on the mining-storage-advanced build (BUILT first)', cnodes.cargo_bay_16?.build === 'mining-storage-advanced');
|
||||
check('cargo: the gated builds are REAL builds (a typo would be a gate that never lifts)', !!builds.builds['mining-storage-improved'] && !!builds.builds['mining-storage-advanced']);
|
||||
check('cargo: the gated quest is DEFINED (a typo would be a gate that never lifts)', questDef('special_delivery') !== null);
|
||||
check('cargo: all three are blueprints (no effects — the build carries the hold)', Object.values(cnodes).every((n) => Object.keys(n.effects ?? {}).length === 0));
|
||||
|
||||
const ctree = loadCategory('cargo');
|
||||
check('model: loadCategory returns the cargo tree', !!ctree && !!ctree.nodes.cargo_bay_8 && ctree.order.length === 3);
|
||||
check('model: cargo: issues(tree) is clean', Array.isArray(issues(ctree)) && issues(ctree).length === 0);
|
||||
check('model: cargo: unlockIssues is clean (mirror + build wiring agree)', unlockIssues(ctree).length === 0);
|
||||
check('model: cargo layout — a 3-row chain (8 → 12 → 16)', (() => { const l = layoutTree(ctree); return l.rows === 3 && l.level.cargo_bay_8 === 0 && l.level.cargo_bay_12 === 1 && l.level.cargo_bay_16 === 2; })());
|
||||
|
||||
const cstate = new ResearchState();
|
||||
check('cargo (pure DAG): 8 Bays is the root (available on a fresh run)', isAvailable(ctree, cstate, 'cargo_bay_8'));
|
||||
check('cargo (pure DAG): 12 + 16 Bays wait on their parent (locked)', !isAvailable(ctree, cstate, 'cargo_bay_12') && !isAvailable(ctree, cstate, 'cargo_bay_16'));
|
||||
check('cargo: missingRequires names the parent (12 ← 8)', JSON.stringify(missingRequires(ctree, cstate, 'cargo_bay_12')) === JSON.stringify(['cargo_bay_8']));
|
||||
cstate.unlock('cargo', 'cargo_bay_8');
|
||||
check('cargo (pure DAG): 12 Bays opens once 8 Bays is researched', isAvailable(ctree, cstate, 'cargo_bay_12'));
|
||||
check('cargo (pure DAG): 16 Bays stays locked until its own parent is researched', !isAvailable(ctree, cstate, 'cargo_bay_16'));
|
||||
cstate.unlock('cargo', 'cargo_bay_12');
|
||||
check('cargo (pure DAG): 16 Bays opens once 12 Bays is researched', isAvailable(ctree, cstate, 'cargo_bay_16'));
|
||||
|
||||
// The live gates — the scene's wiring (attachBuildGates + attachQuestGates,
|
||||
// the same seam as the mining Tier-3 quest gate): fresh run = quest NOT
|
||||
// CLAIMED, nothing BUILT → every cargo tech locked; each gate lifts as its
|
||||
// condition lands (claim → 8 Bays; Improved Storage built → 12 Bays;
|
||||
// Advanced Storage built → 16 Bays).
|
||||
const { attachBuildGates } = await import('../js/research/ResearchModel.js');
|
||||
{
|
||||
const gates = (built, claimed) => attachQuestGates(
|
||||
attachBuildGates(loadCategory('cargo'), (bid) => built.includes(bid)),
|
||||
(qid) => (claimed ? qid === 'special_delivery' : false),
|
||||
);
|
||||
const gstate = new ResearchState();
|
||||
check('cargo-gates: fresh run — 8 Bays waits on the quest (SPECIAL DELIVERY REQUIRED)',
|
||||
!isAvailable(gates([], false), gstate, 'cargo_bay_8') && gates([], false).lockNote(gstate, 'cargo_bay_8') === 'SPECIAL DELIVERY REQUIRED');
|
||||
check('cargo-gates: fresh run — 12 + 16 Bays locked (parent + build gates)',
|
||||
!isAvailable(gates([], false), gstate, 'cargo_bay_12') && !isAvailable(gates([], false), gstate, 'cargo_bay_16'));
|
||||
check('cargo-gates: quest CLAIMED — 8 Bays opens (the first cargo research)',
|
||||
isAvailable(gates([], true), gstate, 'cargo_bay_8'));
|
||||
gstate.unlock('cargo', 'cargo_bay_8');
|
||||
check('cargo-gates: Improved Storage NOT built — 12 Bays stays locked (the build gate)',
|
||||
!isAvailable(gates([], true), gstate, 'cargo_bay_12') && gates([], true).lockNote(gstate, 'cargo_bay_12') === 'IMPROVED MINING STORAGE BUILD REQUIRED');
|
||||
check('cargo-gates: Improved Storage NOT built — 16 Bays stays locked too',
|
||||
!isAvailable(gates([], true), gstate, 'cargo_bay_16'));
|
||||
const g2 = new ResearchState();
|
||||
g2.unlock('cargo', 'cargo_bay_8');
|
||||
check('cargo-gates: Improved Storage BUILT — 12 Bays opens (8 Bays researched + the build done)',
|
||||
isAvailable(gates(['mining-storage-improved'], true), g2, 'cargo_bay_12'));
|
||||
g2.unlock('cargo', 'cargo_bay_12');
|
||||
check('cargo-gates: Advanced Storage NOT built — 16 Bays stays locked (the build gate)',
|
||||
!isAvailable(gates(['mining-storage-improved'], true), g2, 'cargo_bay_16') &&
|
||||
gates(['mining-storage-improved'], true).lockNote(g2, 'cargo_bay_16') === 'ADVANCED MINING STORAGE BUILD REQUIRED');
|
||||
check('cargo-gates: both storages BUILT — 16 Bays opens (12 Bays researched + the build done)',
|
||||
isAvailable(gates(['mining-storage-improved', 'mining-storage-advanced'], true), g2, 'cargo_bay_16'));
|
||||
check('cargo-gates: lockNote reports the right gate per node (quest on 8 Bays, the builds on 12 + 16)',
|
||||
(() => {
|
||||
const none = gates([], false);
|
||||
const st = new ResearchState();
|
||||
return none.lockNote(st, 'cargo_bay_8') === 'SPECIAL DELIVERY REQUIRED' &&
|
||||
none.lockNote(st, 'cargo_bay_12') === 'IMPROVED MINING STORAGE BUILD REQUIRED' &&
|
||||
none.lockNote(st, 'cargo_bay_16') === 'ADVANCED MINING STORAGE BUILD REQUIRED';
|
||||
})());
|
||||
}
|
||||
|
||||
// The BUILDS side — the authoritative gate for each hold expansion (the
|
||||
// research node's unlocks.builds is the declaration; the build's requires
|
||||
// is what beginBuild/BuildModel enforce).
|
||||
check('builds: cargo-bay-8 is gated on cargo/cargo_bay_8 (authoritative side)', JSON.stringify(builds.builds['cargo-bay-8']?.requires ?? []) === JSON.stringify(['cargo/cargo_bay_8']));
|
||||
check('builds: cargo-bay-12 is gated on cargo/cargo_bay_12 (authoritative side)', JSON.stringify(builds.builds['cargo-bay-12']?.requires ?? []) === JSON.stringify(['cargo/cargo_bay_12']));
|
||||
check('builds: cargo-bay-16 is gated on cargo/cargo_bay_16 (authoritative side)', JSON.stringify(builds.builds['cargo-bay-16']?.requires ?? []) === JSON.stringify(['cargo/cargo_bay_16']));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 5. Builds — the build console (data/builds.json → js/ui/BuildWindow.js)
|
||||
// ----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* starting: ["root-id"] already-owned nodes (fresh run)
|
||||
* nodes: { id: { label, description, icon, duration, requires: [ids],
|
||||
* quest?: <quest id>,
|
||||
* build?: <build id>,
|
||||
* unlocks: { builds, research }, effects } }
|
||||
* Layout: level(id) = 0 for roots, else 1 + max(level(requires)). Columns:
|
||||
* leaves take sequential slots in DFS order (data order of roots, then
|
||||
|
|
@ -23,7 +24,14 @@
|
|||
* researchable. The model stays pure: attachQuestGates(tree, isQuestClaimed)
|
||||
* wires the caller's live predicate into those same hooks (the scene reads
|
||||
* its quest ledger — the ledger's claimed set; today: mining/premium_arm +
|
||||
* mining/advanced_storage wait on 'special_delivery').
|
||||
* mining/advanced_storage + cargo/cargo_bay_8 wait on 'special_delivery').
|
||||
* Node's optional `build` field (data/builds.json id): a BUILD-COMPLETION
|
||||
* gate on top of `requires` — the named build must be BUILT before the
|
||||
* node is researchable. Same pure seam: attachBuildGates(tree,
|
||||
* isBuildCompleted) wires the caller's live predicate (the scene reads its
|
||||
* BuildState — the built set; today: cargo/cargo_bay_12 waits on the
|
||||
* Improved Mining Storage build, cargo/cargo_bay_16 on the Advanced one).
|
||||
* Both gates compose over each other and over a dynamic tree's own hooks.
|
||||
*/
|
||||
import { config } from '../config/Config.js';
|
||||
import { questDef } from '../quests/QuestState.js';
|
||||
|
|
@ -235,6 +243,50 @@ export function attachQuestGates(tree, isQuestClaimed) {
|
|||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire a node's BUILD-COMPLETION gate (optional `build` field, a
|
||||
* data/builds.json id) into the tree's available/lockNote hooks — the same
|
||||
* seam the SYSTEM category's chart gate and the STORY-QUEST gate use
|
||||
* (isAvailable consults `available`; the console's LOCKED readout
|
||||
* consults `lockNote`). The named build must be BUILT before the node is
|
||||
* researchable — for a ship upgrade (targets: ["ship"]) that means built
|
||||
* anywhere (the caller's predicate decides — the scene reads its BuildState
|
||||
* built set). Pure: `isBuildCompleted(buildId)` is the caller's LIVE
|
||||
* predicate; absent it, the tree is returned untouched and the gate is not
|
||||
* enforced (the pure tree has no run state to consult).
|
||||
*
|
||||
* Gating semantics: a node with `build` is unavailable while
|
||||
* !isBuildCompleted(build) — regardless of its `requires` — and its lock
|
||||
* reason reads "<BUILD LABEL> BUILD REQUIRED" (the build id, upper-cased,
|
||||
* when the label is unknown). Idempotent: an already-wired hook composes
|
||||
* (this gate is checked first). Returns the same tree (mutated) or null for
|
||||
* no tree.
|
||||
*/
|
||||
export function attachBuildGates(tree, isBuildCompleted) {
|
||||
if (!tree || typeof tree !== 'object' || !Array.isArray(tree.order)) return tree;
|
||||
const buildOf = (id) => {
|
||||
const b = tree.nodes[id]?.build;
|
||||
return typeof b === 'string' && b !== '' ? b : null;
|
||||
};
|
||||
if (typeof isBuildCompleted !== 'function' || !tree.order.some((id) => buildOf(id))) return tree;
|
||||
const prevAvailable = tree.available;
|
||||
const prevNote = tree.lockNote;
|
||||
tree.available = (state, id) => {
|
||||
const b = buildOf(id);
|
||||
if (b !== null && !isBuildCompleted(b)) return false;
|
||||
return typeof prevAvailable === 'function' ? prevAvailable(state, id) : true;
|
||||
};
|
||||
tree.lockNote = (state, id) => {
|
||||
const b = buildOf(id);
|
||||
if (b !== null && !isBuildCompleted(b)) {
|
||||
const label = buildDefs()[b]?.label ?? b;
|
||||
return `${String(label).toUpperCase()} BUILD REQUIRED`;
|
||||
}
|
||||
return typeof prevNote === 'function' ? prevNote(state, id) : null;
|
||||
};
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this tree hold ANY tech ready to research right now — an
|
||||
* unresearched node whose requires are all met and whose extra gate
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import { navDiscoveryStats, resourceStats, systemChartSnapshot } from '../galaxy
|
|||
import { buildGalaxySnapshot, edgeKey } from '../galaxy/GalaxyChart.js';
|
||||
import { planRoute } from '../galaxy/Route.js';
|
||||
import { ResearchState } from '../research/ResearchState.js';
|
||||
import { categories, loadCategory, isAvailable, hasAvailable, buildDefs, attachQuestGates } from '../research/ResearchModel.js';
|
||||
import { categories, loadCategory, isAvailable, hasAvailable, buildDefs, attachQuestGates, attachBuildGates } from '../research/ResearchModel.js';
|
||||
import {
|
||||
SYSTEM_CATEGORY,
|
||||
MAP_NODE,
|
||||
|
|
@ -4937,15 +4937,20 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
/** The tree for a category: the SYSTEM category's live per-system tree,
|
||||
* or the static file tree (data/research/<id>.json — null if missing).
|
||||
* The static tree's nodes may carry the STORY-QUEST gate (node.
|
||||
* `quest` — 'Special Delivery' reward CLAIMED before the gated tech is
|
||||
* researchable): attachQuestGates wires this scene's live quest check
|
||||
* into the tree's available/lockNote hooks, so beginResearch's
|
||||
* isAvailable pass and the READY-RESEARCH cues see the gate. */
|
||||
* The static tree's nodes may carry gates on top of `requires`: a
|
||||
* STORY-QUEST gate (node. `quest` — 'Special Delivery' reward CLAIMED;
|
||||
* attachQuestGates wires this scene's live quest check) and a
|
||||
* BUILD-COMPLETION gate (node. `build` — the named build BUILT;
|
||||
* attachBuildGates wires this scene's BuildState built set — today the
|
||||
* cargo 12/16 Bays techs wait on the Improved/Advanced Mining Storage
|
||||
* builds). Both land in the tree's available/lockNote hooks, so
|
||||
* beginResearch's isAvailable pass and the READY-RESEARCH cues see them. */
|
||||
_treeFor(catId) {
|
||||
if (this.systemTree && catId === this.systemTree.id) return this.systemTree;
|
||||
const tree = loadCategory(catId);
|
||||
return tree ? attachQuestGates(tree, (qid) => this.questClaimed(qid)) : tree;
|
||||
if (!tree) return tree;
|
||||
attachBuildGates(tree, (bid) => (this.buildState?.isBuiltAnywhere(bid) ?? false) === true);
|
||||
return attachQuestGates(tree, (qid) => this.questClaimed(qid));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import {
|
|||
unlocksOf,
|
||||
buildDefs,
|
||||
attachQuestGates,
|
||||
attachBuildGates,
|
||||
} from '../research/ResearchModel.js';
|
||||
import { gatesReady } from '../research/SystemCategory.js';
|
||||
import { shimmerPhase, drawShimmer } from './ShimmerCue.js';
|
||||
|
|
@ -746,16 +747,22 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
|
|||
// ── right: the tech trees ─────────────────────────────────────────────────
|
||||
/** A category's tree: the DYNAMIC one (SystemCategory) is the scene's
|
||||
* live per-system tree — no data file; the rest read data/research/<id>.json.
|
||||
* The static trees' STORY-QUEST gates (node. `quest` — the 'Special
|
||||
* Delivery' reward-claim bar on the mining Tier-3 techs) are wired in
|
||||
* here: attachQuestGates points the tree's available/lockNote hooks at
|
||||
* the scene's live quest check (this.scene.questClaimed), so the node
|
||||
* states, the LOCKED readout and the RESEARCH button all see the gate. */
|
||||
* The static trees' gates are wired in here: attachQuestGates points the
|
||||
* tree's available/lockNote hooks at the scene's live quest check
|
||||
* (node. `quest` — the 'Special Delivery' reward-claim bar on the mining
|
||||
* Tier-3 techs + cargo 8 Bays), and attachBuildGates at the scene's
|
||||
* BuildState built set (node. `build` — the named build must be BUILT;
|
||||
* today: cargo 12 Bays on Improved Mining Storage, 16 Bays on Advanced).
|
||||
* Node states, the LOCKED readout and the RESEARCH button all see both. */
|
||||
_treeFor(catId) {
|
||||
if (this.systemTree && catId === this.systemTree.id) return this.systemTree;
|
||||
const tree = loadCategory(catId);
|
||||
if (tree && typeof this.scene?.questClaimed === 'function') {
|
||||
return attachQuestGates(tree, (qid) => this.scene.questClaimed(qid));
|
||||
if (!tree) return tree;
|
||||
if (this.scene?.buildState && typeof this.scene.buildState.isBuiltAnywhere === 'function') {
|
||||
attachBuildGates(tree, (bid) => this.scene.buildState.isBuiltAnywhere(bid));
|
||||
}
|
||||
if (typeof this.scene?.questClaimed === 'function') {
|
||||
attachQuestGates(tree, (qid) => this.scene.questClaimed(qid));
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue