Wire research→build unlocks and add first tether-l2 build entry
- Define the two-sided unlock contract: research nodes declare `unlocks.builds`/`unlocks.research`, builds declare `requires` as "category/node id" (authoritative gate); ResearchModel exposes `unlocksOf`, `buildDefs`, `unlockIssues`, `buildIssues` - Add `tether-l2` build entry (planet/station targets, credits+minerals cost, level-2 tether effect) and link it from `exploration/tether_l2` - Show an UNLOCKS line in the research dossier so the console surfaces what a tech opens - Extend dev/research-builds.test.mjs to validate both sides of the gate and the new model helpers - Update data/builds.json, research.json, exploration.json `_comment`s and PROJECT_NOTES to document the contract
This commit is contained in:
parent
7138d153fa
commit
70f4954c69
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "BUILDABLE ITEMS — cost-based improvements. A build costs the `cost` below (in `resources`) and takes effect when purchased; it is only AVAILABLE once the research it `requires` is complete (data/research.json → `projects.<id>`). `category` says where it applies: `ship` = improve the player's ship, `planet` = improve things on planets, `station` = improve space stations, `general` = new capabilities for the player. `repeatable` builds can be bought more than once (treat as false when absent). Add a build = one entry under `builds` (the key is the build's id). Keys starting with `_` (like `_template`) are documentation, not data. The build state/UI is not implemented yet — this file is the data layer that will drive it.",
|
||||
"_comment": "BUILDABLE ITEMS — cost-based improvements. A build costs the `cost` below (in `resources`) and takes effect when purchased; it is AVAILABLE once every research id in its `requires` is complete — ids are \"<category>/<node id>\" pointing at data/research/<category>.json → nodes (e.g. \"exploration/tether_l2\"). The research side declares the same relationship in its node's `unlocks.builds`; dev/research-builds.test.mjs keeps the two in lock-step. `category` says where it applies: `ship` = improve the player's ship, `planet` = improve things on planets, `station` = improve space stations, `general` = new capabilities for the player; `targets` (optional) lists the surfaces a build can be placed on (defaults to `[category]` when absent). `repeatable` builds can be bought more than once (treat as false when absent). Add a build = one entry under `builds` (the key is the build's id). Keys starting with `_` (like `_template`) are documentation, not data. The build state/UI is not implemented yet — this file is the data layer that will drive it.",
|
||||
"resources": {
|
||||
"credits": {
|
||||
"label": "Credits",
|
||||
|
|
@ -10,16 +10,37 @@
|
|||
"description": "Raw materials — mined from resource worlds."
|
||||
}
|
||||
},
|
||||
"builds": {},
|
||||
"builds": {
|
||||
"tether-l2": {
|
||||
"_comment": "Placeholder entry — proves the research→build unlock wiring (exploration/tether_l2 `unlocks.builds` ↔ this `requires`). Cost/effect are first guesses: tune when the build panel lands.",
|
||||
"label": "Tether Ring · Level 2",
|
||||
"description": "Anchor a level-2 tether ring on a planet or space station you hold. Its 6,400 m field stitches into your home tether — open space where there was a wall.",
|
||||
"category": "general",
|
||||
"targets": ["planet", "station"],
|
||||
"cost": {
|
||||
"credits": 400,
|
||||
"minerals": 60
|
||||
},
|
||||
"requires": ["exploration/tether_l2"],
|
||||
"repeatable": false,
|
||||
"effects": {
|
||||
"tether": { "level": 2, "anchor": "target" }
|
||||
},
|
||||
"theme": {
|
||||
"color": "#00e5ff"
|
||||
}
|
||||
}
|
||||
},
|
||||
"_template": {
|
||||
"label": "Buildable Item",
|
||||
"description": "What this is and what it does.",
|
||||
"category": "ship",
|
||||
"targets": ["ship"],
|
||||
"cost": {
|
||||
"credits": 250,
|
||||
"minerals": 40
|
||||
},
|
||||
"requires": [],
|
||||
"requires": ["exploration/<node id>"],
|
||||
"repeatable": false,
|
||||
"effects": {},
|
||||
"theme": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "RESEARCH — time-based tech, one project at a time (maxConcurrent). The command deck's RESEARCH button opens the full-screen research console (js/ui/ResearchWindow.js). Categories are the tab row; each category's TECH TREE lives in its own file under data/research/ — the file basename is the config section AND the category id (data/research/exploration.json → section 'exploration'). A tree is a branching DAG drawn top→down: `nodes` is a flat map (data order matters — roots/children lay out left-to-right in this order), each node's `requires` names the parents that must be RESEARCHED before it is. Research rules: start an available node → its `duration` (timeUnit) runs; when it completes the node is unlocked, its `effects` apply (the scene applies them — the seam), and its children become available. `unlocks` is the documented forward reference (builds.json ids + follow-on research) for the build system that comes later. `starting` names nodes a fresh run already owns. Keys starting with `_` are documentation, not data.",
|
||||
"_comment": "RESEARCH — time-based tech, one project at a time (maxConcurrent). The command deck's RESEARCH button opens the full-screen research console (js/ui/ResearchWindow.js). Categories are the tab row; each category's TECH TREE lives in its own file under data/research/ — the file basename is the config section AND the category id (data/research/exploration.json → section 'exploration'). A tree is a branching DAG drawn top→down: `nodes` is a flat map (data order matters — roots/children lay out left-to-right in this order), each node's `requires` names the parents that must be RESEARCHED before it is (the DAG edges — authoritative). Research rules: start an available node → its `duration` (timeUnit) runs; when it completes the node is unlocked, its `effects` apply (the scene applies them — the seam), and its children become available. `unlocks` is what the tech OPENS beyond follow-on tech: `builds` = ids in data/builds.json the tech makes available (the build's own `requires` — \"category/node id\" — is the authoritative gate; the declaration here is what the console's UNLOCKS readout and the test validate), `research` = readable mirror of the children's `requires` edges. `starting` names nodes a fresh run already owns. Keys starting with `_` are documentation, not data.",
|
||||
"enabled": true,
|
||||
"timeUnit": "seconds",
|
||||
"maxConcurrent": 1,
|
||||
|
|
@ -23,7 +23,10 @@
|
|||
"icon": "tether",
|
||||
"duration": 120,
|
||||
"requires": ["parent-tech-id"],
|
||||
"unlocks": { "builds": [], "research": [] },
|
||||
"unlocks": {
|
||||
"builds": ["build-id-in-builds-json"],
|
||||
"research": ["child-tech-id"]
|
||||
},
|
||||
"effects": { "tether": { "level": 2 } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "EXPLORATION — the player's reach. Tether tech raises the home tether's level (effects.tether.level → TetherField.setLevel; radius = 5,120 × 1.25^(level−1), see data/tether.json and docs/PROJECT_NOTES.md). Tether Anchoring and Signal Amplification grant capability flags (seam) that future mechanics read. `starting` = already researched on a fresh run, so its children are available the first time the console opens. duration is in research.timeUnit (seconds); 0 = granted, never researched.",
|
||||
"_comment": "EXPLORATION — the player's reach. Tether tech raises the home tether's level (effects.tether.level → TetherField.setLevel; radius = 5,120 × 1.25^(level−1), see data/tether.json and docs/PROJECT_NOTES.md). Tether Anchoring and Signal Amplification grant capability flags (seam) that future mechanics read. Node contract: `requires` = parents that must be RESEARCHED first (the DAG edges — authoritative); `unlocks` = what completing the tech opens: `builds` (ids in data/builds.json the tech makes available — the build's own `requires` is the authoritative gate, this list is the declaration, and the test keeps the two in lock-step) and `research` (a readable mirror of the children's `requires` edges); `effects` = what the scene applies on completion. `starting` = already researched on a fresh run, so its children are available the first time the console opens. duration is in research.timeUnit (seconds); 0 = granted, never researched.",
|
||||
"starting": ["tether_l1"],
|
||||
"nodes": {
|
||||
"tether_l1": {
|
||||
|
|
@ -13,11 +13,11 @@
|
|||
},
|
||||
"tether_l2": {
|
||||
"label": "Tether Level 2",
|
||||
"description": "A second stage of field power. The boundary ring pulls out to 6,400 m — the far reaches of a compact system drop inside your reach, and the keep-out wall moves with you.",
|
||||
"description": "A second stage of field power. The boundary ring pulls out to 6,400 m — the far reaches of a compact system drop inside your reach, and the keep-out wall moves with you. Also unlocks anchoring level-2 tether rings on worlds and stations you hold (build: tether-l2).",
|
||||
"icon": "tether",
|
||||
"duration": 60,
|
||||
"requires": ["tether_l1"],
|
||||
"unlocks": { "builds": [], "research": ["tether_l3", "tether_anchors"] },
|
||||
"unlocks": { "builds": ["tether-l2"], "research": ["tether_l3", "tether_anchors"] },
|
||||
"effects": { "tether": { "level": 2 } }
|
||||
},
|
||||
"tether_l3": {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,15 @@ const tree = loadCategory('exploration');
|
|||
check('model: loadCategory returns the exploration tree', !!tree && tree.nodes.tether_l1 && tree.order.length === Object.keys(nodes).length);
|
||||
check('model: issues(tree) is clean', Array.isArray(issues(tree)) && issues(tree).length === 0);
|
||||
|
||||
// the UNLOCKS space — research declares what it opens (builds + follow-on
|
||||
// research); the build side is the authoritative gate; the test locks the
|
||||
// two declarations together.
|
||||
const { unlocksOf, buildDefs, unlockIssues, buildIssues } = await import('../js/research/ResearchModel.js');
|
||||
check('model: unlocksOf normalizes {builds, research}', JSON.stringify(unlocksOf(tree, 'tether_l2')) === JSON.stringify({ builds: ['tether-l2'], research: ['tether_l3', 'tether_anchors'] }));
|
||||
check('model: unlocksOf is empty for a node without unlocks', JSON.stringify(unlocksOf(tree, 'tether_l4')) === JSON.stringify({ builds: [], research: [] }));
|
||||
check('model: unlockIssues(tree) is clean (mirrors + build wiring agree)', unlockIssues(tree).length === 0);
|
||||
check('model: buildIssues() is clean (every build require resolves)', buildIssues().length === 0);
|
||||
|
||||
const layout = layoutTree(tree);
|
||||
check('model: layout rows = 4 (l1 → l2/anchor/signal → l3 → l4)', layout.rows === 4);
|
||||
check('model: layout levels — l1 root, l2/signal tier 1, l3/anchors tier 2, l4 tier 3',
|
||||
|
|
@ -179,10 +188,18 @@ check('state: an old save without research restores as a fresh start', (() => {
|
|||
check('builds: credits resource defined', typeof builds.resources?.credits?.label === 'string');
|
||||
check('builds: minerals resource defined', typeof builds.resources?.minerals?.label === 'string');
|
||||
check('builds: builds is a map', !!builds.builds && typeof builds.builds === 'object' && !Array.isArray(builds.builds));
|
||||
check('builds: no builds yet (empty map)', Object.keys(builds.builds ?? {}).filter((k) => !k.startsWith('_')).length === 0);
|
||||
|
||||
// the tether-l2 build — the first research→build unlock, wired both ways
|
||||
const tl2 = builds.builds['tether-l2'] ?? {};
|
||||
check('builds: tether-l2 entry exists (the level-2 tether build)', !!tl2);
|
||||
check('builds: tether-l2 is available on a planet OR a station', JSON.stringify(tl2.targets ?? []) === JSON.stringify(['planet', 'station']));
|
||||
check('builds: tether-l2 is gated on exploration/tether_l2 (authoritative side)', JSON.stringify(tl2.requires ?? []) === JSON.stringify(['exploration/tether_l2']));
|
||||
check('builds: tether-l2 is a one-off with a credits+minerals cost', tl2.repeatable === false && typeof tl2.cost?.credits === 'number' && typeof tl2.cost?.minerals === 'number');
|
||||
check('builds: tether_l2 declares the build in its unlocks (declaration side)', (nodes.tether_l2?.unlocks?.builds ?? []).includes('tether-l2'));
|
||||
check('builds: tether-l2 effect raises a level-2 tether on the target', tl2.effects?.tether?.level === 2 && tl2.effects?.tether?.anchor === 'target');
|
||||
|
||||
const bt = builds._template ?? {};
|
||||
for (const k of ['label', 'description', 'category', 'cost', 'requires', 'repeatable', 'effects', 'theme']) {
|
||||
for (const k of ['label', 'description', 'category', 'targets', 'cost', 'requires', 'repeatable', 'effects', 'theme']) {
|
||||
check(`builds._template documents "${k}"`, k in bt);
|
||||
}
|
||||
check('builds._template.category is a known kind', ['ship', 'planet', 'station', 'general'].includes(bt.category));
|
||||
|
|
|
|||
|
|
@ -275,17 +275,45 @@ only present when the tech is researchable and nothing is in progress.
|
|||
label, icon, accent), the `video` (file + aspect), and `fx` timing.
|
||||
- `data/research/<category>.json` — the tree: a flat `nodes` map where each
|
||||
node has `label`, `description`, `duration` (in `timeUnit`), `requires`
|
||||
(parent ids — a DAG), and optional `effects`. `starting` lists the
|
||||
pre-unlocked roots. Section name = file basename
|
||||
(parent ids — a DAG), `unlocks` (what the tech OPENS, below), and
|
||||
optional `effects`. `starting` lists the pre-unlocked roots. Section name
|
||||
= file basename
|
||||
(`research/exploration.json` → `config.section('exploration')`).
|
||||
**Add a category = one file + one line in `research.json → categories`
|
||||
+ one line in `data/manifest.json`.**
|
||||
|
||||
**The UNLOCKS space (research → everything else):** a tech unlocks more
|
||||
than follow-on tech. Each node's `unlocks` is the declaration side:
|
||||
- `unlocks.research` — the readable mirror of the children's `requires`
|
||||
edges (the DAG itself stays authoritative — `requires` only).
|
||||
- `unlocks.builds` — ids in `data/builds.json` the tech makes available.
|
||||
The **authoritative gate is the build's own `requires`** (a list of
|
||||
`"<category>/<node id>"` research ids — e.g.
|
||||
`tether-l2.requires: ["exploration/tether_l2"]`): a build is available
|
||||
once every id it names is researched. `dev/research-builds.test.mjs`
|
||||
keeps both sides in lock-step (the test fails if one names the other and
|
||||
the other doesn't name back, or an id dangles).
|
||||
- Builds also carry `category` (ship / planet / station / general),
|
||||
optional `targets` (the surfaces it can be placed on — defaults to
|
||||
`[category]`; the tether ring targets `planet` + `station`), `cost`
|
||||
(credits + minerals), `repeatable`, `effects`, `theme`.
|
||||
- The first build entry, `tether-l2` (level-2 tether ring, anchored on a
|
||||
world or station), is the placeholder proving the wiring — cost/effect
|
||||
are first guesses; tune them when the build panel lands.
|
||||
- The console's dossier shows the line (`UNLOCKS: TETHER LEVEL 3 · TETHER
|
||||
ANCHORING · BUILD · TETHER RING · LEVEL 2`), read through
|
||||
`ResearchModel.unlocksOf(tree, id)` + `buildDefs()` — the one read
|
||||
point the build UI will use too.
|
||||
|
||||
**Code layering (same rules as the tether):**
|
||||
- `js/research/ResearchModel.js` — PURE (no Phaser): `roots`, `issues`
|
||||
(DAG validation), `levels` (longest-path level), `layoutTree`
|
||||
(deterministic column/row layout: DFS leaf-slot assignment, parent =
|
||||
mean of children), `isAvailable`, `missingRequires`. Node-tested.
|
||||
mean of children), `isAvailable`, `missingRequires`, and the unlocks
|
||||
contract: `unlocksOf` (normalize `{builds, research}`), `buildDefs`
|
||||
(builds.json → map, `_`-keys excluded), `unlockIssues(tree)` (mirror +
|
||||
build wiring), `buildIssues()` (every build `requires` id resolves).
|
||||
Node-tested.
|
||||
- `js/research/ResearchState.js` — PURE: `unlock`, `isUnlocked`, `getActive`,
|
||||
`start`, `progress(time)`, `tick(time)` (→ array of completions),
|
||||
`restoreActive`, `toJSON(now)`/`fromJSON`. Node-tested.
|
||||
|
|
@ -318,9 +346,11 @@ keys). Open/close → `ui_window`/`ui_close`.
|
|||
**Verified:** `dev/research-builds.test.mjs` (manifest registration,
|
||||
research.json globals, the exploration tree's raw-JSON contract — DAG, node
|
||||
fields, effects — the real code path: ResearchModel layout/levels/determinism
|
||||
+ ResearchState start/tick/complete/restore round-trip, builds.json,
|
||||
actionbar.json). `dev/research-shot.html` + `dev/cdp-shot.mjs` open the
|
||||
window and start a run through CDP for a screenshot.
|
||||
+ the unlocks contract (unlocksOf/unlockIssues/buildIssues) +
|
||||
ResearchState start/tick/complete/restore round-trip, builds.json (the
|
||||
tether-l2 build, both sides of the gate, the template), actionbar.json).
|
||||
`dev/research-shot.html` + `dev/cdp-shot.mjs` open the window and start a
|
||||
run through CDP for a screenshot.
|
||||
|
||||
## Reputation — standing on planets & space stations (data layer; factions later)
|
||||
|
||||
|
|
@ -545,7 +575,9 @@ The player holds a REPUTATION (standing) on each planet and space station:
|
|||
- [ ] Tether progression (research side is done; the build side is next):
|
||||
anchor tethers on planets/stations and upgrade levels beyond the
|
||||
Exploration tree (the `add`/`setLevel`/`onChange` seams are in place;
|
||||
the costs + build panel come next)
|
||||
the build entry `tether-l2` + the research→build unlock wiring exist
|
||||
in `data/builds.json` — the costs panel and the apply-effect seam
|
||||
come next)
|
||||
- [ ] Factions & pirates: claim settlements (`owner`), flags, borders,
|
||||
and the player's place in a populated galaxy (the reputation layer
|
||||
already resolves standing through `owner` — `Reputation.
|
||||
|
|
|
|||
|
|
@ -177,3 +177,89 @@ export function missingRequires(tree, state, id) {
|
|||
(r) => !tree.nodes[r] || !state.isUnlocked(tree.id, r),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* What completing this tech OPENS, normalized (both arrays always present,
|
||||
* strings only). `unlocks` is optional on a node; absent = unlocks nothing.
|
||||
* One read point for the console's UNLOCKS line and the build system:
|
||||
* builds → ids in data/builds.json the tech makes available
|
||||
* (the build's own `requires` is the authoritative gate)
|
||||
* research → readable mirror of the children's `requires` edges
|
||||
*/
|
||||
export function unlocksOf(tree, id) {
|
||||
const u = tree.nodes[id]?.unlocks ?? {};
|
||||
const str = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === 'string') : []);
|
||||
return { builds: str(u.builds), research: str(u.research) };
|
||||
}
|
||||
|
||||
/** Map of build id → build definition (data/builds.json → builds, `_`-keys excluded). */
|
||||
export function buildDefs() {
|
||||
const raw = config.get('builds.builds', {}) ?? {};
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(raw)) {
|
||||
if (!k.startsWith('_') && v && typeof v === 'object') out[k] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-file unlock contract for one tree (empty array = clean):
|
||||
* - unlocks.research entries name real nodes that require THIS node
|
||||
* (the mirror of the DAG's child edges — kept honest by the test),
|
||||
* - unlocks.builds entries exist in data/builds.json AND that build
|
||||
* names this node ("<tree.id>/<id>", bare id tolerated) in its `requires`.
|
||||
*/
|
||||
export function unlockIssues(tree) {
|
||||
const out = [];
|
||||
const defs = buildDefs();
|
||||
for (const id of tree.order) {
|
||||
const { builds: ub, research: ur } = unlocksOf(tree, id);
|
||||
for (const r of ur) {
|
||||
if (!tree.nodes[r]) out.push(`${id} unlocks unknown research node "${r}"`);
|
||||
else if (!(tree.nodes[r].requires ?? []).includes(id))
|
||||
out.push(`${id}.unlocks.research lists "${r}", but ${r}.requires does not name ${id}`);
|
||||
}
|
||||
for (const b of ub) {
|
||||
const def = defs[b];
|
||||
if (!def) {
|
||||
out.push(`${id} unlocks unknown build "${b}" (data/builds.json → builds)`);
|
||||
continue;
|
||||
}
|
||||
const req = Array.isArray(def.requires) ? def.requires : [];
|
||||
if (!req.includes(`${tree.id}/${id}`) && !req.includes(id))
|
||||
out.push(`build "${b}" does not require ${tree.id}/${id} (its requires: [${req.join(', ')}])`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds → research direction (empty array = clean): every entry of a
|
||||
* build's `requires` must be a well-formed "category/node id" that
|
||||
* resolves to a real node in a registered category's tree.
|
||||
*/
|
||||
export function buildIssues() {
|
||||
const out = [];
|
||||
for (const [bid, def] of Object.entries(buildDefs())) {
|
||||
const req = def.requires ?? [];
|
||||
if (!Array.isArray(req)) {
|
||||
out.push(`build "${bid}".requires must be an array of "category/node id"`);
|
||||
continue;
|
||||
}
|
||||
for (const rid of req) {
|
||||
const m = typeof rid === 'string' ? rid.split('/') : null;
|
||||
if (!m || m.length !== 2 || !m[0] || !m[1]) {
|
||||
out.push(`build "${bid}" requires malformed research id "${rid}" (want "category/node id")`);
|
||||
continue;
|
||||
}
|
||||
const catId = m[0];
|
||||
if (!categories().some((c) => c.id === catId)) {
|
||||
out.push(`build "${bid}" requires unknown category "${catId}"`);
|
||||
continue;
|
||||
}
|
||||
const t = loadCategory(catId);
|
||||
if (!t || !t.nodes[m[1]]) out.push(`build "${bid}" requires unknown node "${m[1]}" in ${catId}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ import {
|
|||
layoutTree,
|
||||
isAvailable,
|
||||
missingRequires,
|
||||
unlocksOf,
|
||||
buildDefs,
|
||||
} from '../research/ResearchModel.js';
|
||||
import { ensureIcon } from '../research/ResearchIcons.js';
|
||||
|
||||
|
|
@ -824,15 +826,24 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
|
|||
})
|
||||
.setScrollFactor(0);
|
||||
cont.add(this.dMeta);
|
||||
this.dUnlocks = s
|
||||
.text(tx, detailY + 58, '', {
|
||||
fontFamily: BODY,
|
||||
fontSize: '9px',
|
||||
color: toCss(C.faint),
|
||||
letterSpacing: 1.5,
|
||||
})
|
||||
.setScrollFactor(0);
|
||||
cont.add(this.dUnlocks);
|
||||
const btnW = 152;
|
||||
const wrapW = Math.max(120, rightX + rightW - 12 - btnW - 18 - tx);
|
||||
this.dDesc = s
|
||||
.text(tx, detailY + 64, '', {
|
||||
.text(tx, detailY + 76, '', {
|
||||
fontFamily: BODY,
|
||||
fontSize: '12px',
|
||||
color: toCss(C.dim),
|
||||
letterSpacing: 0.4,
|
||||
lineSpacing: 5,
|
||||
lineSpacing: 4,
|
||||
wordWrap: { width: wrapW },
|
||||
})
|
||||
.setScrollFactor(0);
|
||||
|
|
@ -1142,6 +1153,7 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
|
|||
this.dIcon.setVisible(false);
|
||||
this.dLabel.setText('');
|
||||
this.dMeta.setText('');
|
||||
this.dUnlocks.setText('');
|
||||
this.dDesc.setText('');
|
||||
this._paintDetailBtn();
|
||||
return;
|
||||
|
|
@ -1196,6 +1208,16 @@ export class ResearchWindow extends Phaser.GameObjects.Container {
|
|||
}
|
||||
this.dMeta.setText(meta);
|
||||
this.dMeta.setColor(toCss(metaColor));
|
||||
|
||||
// unlocks line: what completing this tech OPENS — follow-on research
|
||||
// (mirror of the DAG edges) and builds (data/builds.json, gated by the
|
||||
// build's own `requires`; this is the declaration side).
|
||||
const { builds: ub, research: ur } = unlocksOf(entry.tree, sel.id);
|
||||
const defs = buildDefs();
|
||||
const parts = [];
|
||||
for (const r of ur) parts.push(String(entry.tree.nodes[r]?.label ?? r).toUpperCase());
|
||||
for (const b of ub) parts.push(`BUILD · ${String(defs[b]?.label ?? b).toUpperCase()}`);
|
||||
this.dUnlocks.setText(`UNLOCKS: ${parts.length ? parts.join(' · ') : '—'}`);
|
||||
this._paintDetailBtn();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue