From d9fc9df829d3880c3a6a0f046a459fe3da637437 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 6 Sep 2026 15:20:31 -0600 Subject: [PATCH] Cap tether progression at level 3 and remove tether_l4 research node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- data/research/exploration.json | 11 +---------- data/tether.json | 2 +- dev/research-builds.test.mjs | 19 ++++++++++--------- dev/tether.test.mjs | 2 +- docs/PROJECT_NOTES.md | 6 +++--- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/data/research/exploration.json b/data/research/exploration.json index 74ec45e..b9ab848 100644 --- a/data/research/exploration.json +++ b/data/research/exploration.json @@ -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", diff --git a/data/tether.json b/data/tether.json index 7362d2c..c565dee 100644 --- a/data/tether.json +++ b/data/tether.json @@ -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^(level−1). 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, diff --git a/dev/research-builds.test.mjs b/dev/research-builds.test.mjs index eadaccf..1f64973 100644 --- a/dev/research-builds.test.mjs +++ b/dev/research-builds.test.mjs @@ -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 diff --git a/dev/tether.test.mjs b/dev/tether.test.mjs index 1bfa22d..31ffdb6 100644 --- a/dev/tether.test.mjs +++ b/dev/tether.test.mjs @@ -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++) { diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index 7c7e7d5..d635d9f 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -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 1–4, Tether Anchoring, Signal Amplification); add a category = + Level 1–3, 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 3–4 - 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/*`,