Cap tether progression at level 3 and remove tether_l4 research node

- Reduce maxLevel from 12 to 3 in data/tether.json
- Remove the tether_l4 entry from exploration.json; tether_l3 now has empty unlocks
- Update research-builds and tether tests to reflect the shorter chain (3 layout rows, level checks for 1–3 only)
- Revise PROJECT_NOTES.md to describe the 3-level progression
This commit is contained in:
Brian Fertig 2026-09-06 15:20:31 -06:00
parent cb82da4935
commit d9fc9df829
5 changed files with 16 additions and 24 deletions

View File

@ -26,17 +26,8 @@
"icon": "tether",
"duration": 150,
"requires": ["tether_l2"],
"unlocks": { "builds": [], "research": ["tether_l4"] },
"effects": { "tether": { "level": 3 } }
},
"tether_l4": {
"label": "Tether Level 4",
"description": "Fourth stage: 40,960 m. At this level every object of a ten-body system — all the worlds and stations — lies inside one ring around your anchor, and the barrier stops meaning anything out here.",
"icon": "tether",
"duration": 300,
"requires": ["tether_l3"],
"unlocks": { "builds": [], "research": [] },
"effects": { "tether": { "level": 4 } }
"effects": { "tether": { "level": 3 } }
},
"tether_anchors": {
"label": "Tether Anchoring",

View File

@ -2,7 +2,7 @@
"_comment": "TETHER — the player's range. A tether anchors on a world or station and defines a circular zone: radius = distance from the anchor's center. Level 1 = 5120 px (design rule); radius(level) = level1Radius × radiusGrowth^(level1). The ship may fly anywhere in the UNION of its tethers' zones — where zones overlap there is no line and no wall; outside all of them is a hard barrier, drawn as a thick cyberpunk glitch dotted line along the union's boundary. homeId/homeLevel/homeLabel = the tether the player starts with (anchored on the home world, which sits at the system origin). line = the barrier's look & motion (dash/gap px along the rim, thicknesses, march speed = how fast dashes flow, colors/alphas = the neon passes, glitch = ambient bursts: dash flicker, radial displacement, sparks). contact = feedback when the ship bumps the barrier (pulse = the local shudder of the line; toastCooldown = console warning throttle, ms).",
"level1Radius": 5120,
"radiusGrowth": 2.0,
"maxLevel": 12,
"maxLevel": 3,
"homeId": "home",
"homeLevel": 1,
"homeLabel": null,

View File

@ -116,15 +116,16 @@ for (const id of Object.keys(nodes)) {
check('exploration: the tree is a DAG (no cycles)', !cyclic);
check('exploration: has at least one root (a node with no requires)', Object.values(nodes).some((n) => !(n.requires ?? []).length));
// tether level chain: L2 is a BUILD (research unlocks it, the build
// applies it — data/builds.json → tether-l2), so it carries no immediate
// effect; L3/L4 still apply their tether raise on research completion.
// tether level chain (3 levels total): L2 is a BUILD (research unlocks
// it, the build applies it — data/builds.json → tether-l2), so it carries
// no immediate effect; L3 still applies its tether raise on research
// completion.
const l2NoEffect = !nodes.tether_l2 || !nodes.tether_l2.effects?.tether;
const levelsOk = [3, 4].every((lvl) => {
const levelsOk = [3].every((lvl) => {
const n = nodes[`tether_l${lvl}`];
return n && n.effects?.tether?.level === lvl;
});
check('exploration: tether_l3/l4 each raise the tether to their level', levelsOk);
check('exploration: tether_l3 raises the tether to level 3', levelsOk);
check('exploration: tether_l2 carries no immediate tether effect (the build applies it)', l2NoEffect);
// ----------------------------------------------------------------------
@ -145,15 +146,15 @@ check('model: issues(tree) is clean', Array.isArray(issues(tree)) && issues(tree
// 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: unlocksOf is empty for a node without unlocks', JSON.stringify(unlocksOf(tree, 'signal_amp')) === 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',
check('model: layout rows = 3 (l1 → l2/anchor/signal → l3)', layout.rows === 3);
check('model: layout levels — l1 root, l2/signal tier 1, l3/anchors tier 2',
layout.level.tether_l1 === 0 && layout.level.tether_l2 === 1 && layout.level.signal_amp === 1 &&
layout.level.tether_l3 === 2 && layout.level.tether_anchors === 2 && layout.level.tether_l4 === 3);
layout.level.tether_l3 === 2 && layout.level.tether_anchors === 2);
check('model: layout is deterministic (same tree → same columns)', JSON.stringify(layout.col) === JSON.stringify(layoutTree(tree).col));
// a fresh run: starting techs pre-unlocked, the rest follow the rules

View File

@ -69,7 +69,7 @@ const dist = (x, y, t) => Math.hypot(x - t.x, y - t.y);
const growth = config.get('tether.radiusGrowth', 2.0);
const r2 = Tether.radiusForLevel(2);
check(`level 2 = 5120 × growth (${growth}) (got ${r2})`, Math.abs(r2 - 5120 * growth) < 1e-9);
const maxLevel = config.get('tether.maxLevel', 12);
const maxLevel = config.get('tether.maxLevel', 3);
let mono = true;
let prev = 0;
for (let l = 1; l <= maxLevel; l++) {

View File

@ -789,15 +789,15 @@ The player holds a REPUTATION (standing) on each planet and space station:
Pure rules/state in `js/research/` (Node-tested), the window is a
passive view, `effects` carry the payload (e.g. `tether.level` grows
the home tether). First category **Exploration** is live (Tether
Level 14, Tether Anchoring, Signal Amplification); add a category =
Level 13, Tether Anchoring, Signal Amplification); add a category =
one JSON file + one line in the registry + one line in the manifest
(`js/ui/ResearchWindow.js`, `js/research/*`, `data/research.json`,
`data/research/exploration.json`, dev/research-builds.test.mjs)
- [x] Tether progression: level 2 is a **build** — research
`tether_l2` (blueprint, no effect) → BUILD slot on a world holding a
level-1 tether → 200 minerals / 20 s → the world's tether strengthens
to level 2 (10240 px), effect and all, outliving the stay. Levels 34
are still research-direct (`effects.tether.level` on the home
to level 2 (10240 px), effect and all, outliving the stay. Level 3
is still research-direct (`effects.tether.level` on the home
tether); anchoring extra rings on planets/stations remains
(the `add`/`setLevel`/`onChange` seams are in place)
(`data/builds.json → tether-l2`, `js/build/*`,