From b1ae21e95e899b392eb6dadbe41a5681fa68aa10 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Mon, 7 Sep 2026 23:10:47 -0600 Subject: [PATCH] Changes to Research Areas --- README.md | 4 ++-- data/game.json | 2 +- data/research/exploration.json | 24 +++--------------------- dev/compass-far.mjs | 6 +++--- dev/discovery.test.mjs | 4 ++-- dev/research-builds.test.mjs | 16 +++++++--------- dev/research-shot.mjs | 2 +- docs/PROJECT_NOTES.md | 2 +- js/ui/DiscoveryCompass.js | 2 +- 9 files changed, 21 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 2e0b2f1..74fbff2 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,8 @@ node dev/server.mjs 8080 `maxConcurrent: 1`). A deck progress bar tracks the run; completion fires the tech's `effects` (e.g. `tether: {level: N}` grows the home tether's range) and a toast. Each category's tree is its own JSON file under - `data/research/` (**Exploration**: Tether Level 1–3, Tether Anchoring, - Signal Amplification; **Mining**: Improved Mining Arm, Advanced Mining + `data/research/` (**Exploration**: Tether Level 1–3; + **Mining**: Improved Mining Arm, Advanced Mining Arm, Improved Mining Storage — all three unlock the matching build in the console's MINING tab). Unlocks and in-progress work persist in the save bank — and resume correctly on load diff --git a/data/game.json b/data/game.json index 9825ec2..41e8e5c 100644 --- a/data/game.json +++ b/data/game.json @@ -31,7 +31,7 @@ "smallBox": 26, "smallHitSlack": 16, "expandDelay": 300, - "collapseDelay": 400 + "collapseDelay": 2000 } } } diff --git a/data/research/exploration.json b/data/research/exploration.json index 692181b..d5d4b73 100644 --- a/data/research/exploration.json +++ b/data/research/exploration.json @@ -1,5 +1,5 @@ { - "_comment": "EXPLORATION — the player's reach. Tether tech is the BLUEPRINT only (effects: {}) — the player installs it as a build on a world (data/builds.json, the build carries the effect): Tether Level 2 → a TETHER - LEVEL 2 build on a world holding a level-1 tether (→ tether-l2), Tether Level 3 → a TETHER - LEVEL 3 build on a world holding a level-2 tether (→ tether-l3). Radius = 5,120 × 2^(level−1) (data/tether.json). 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.", + "_comment": "EXPLORATION — the player's reach. Tether tech is the BLUEPRINT only (effects: {}) — the player installs it as a build on a world (data/builds.json, the build carries the effect): Tether Level 2 → a TETHER - LEVEL 2 build on a world holding a level-1 tether (→ tether-l2), Tether Level 3 → a TETHER - LEVEL 3 build on a world holding a level-2 tether (→ tether-l3). Radius = 5,120 × 2^(level−1) (data/tether.json). 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": { @@ -8,7 +8,7 @@ "icon": "tether", "duration": 0, "requires": [], - "unlocks": { "builds": [], "research": ["tether_l2", "signal_amp"] }, + "unlocks": { "builds": [], "research": ["tether_l2"] }, "effects": {} }, "tether_l2": { @@ -17,7 +17,7 @@ "icon": "tether", "duration": 60, "requires": ["tether_l1"], - "unlocks": { "builds": ["tether-l2"], "research": ["tether_l3", "tether_anchors"] }, + "unlocks": { "builds": ["tether-l2"], "research": ["tether_l3"] }, "effects": {} }, "tether_l3": { @@ -28,24 +28,6 @@ "requires": ["tether_l2"], "unlocks": { "builds": ["tether-l3"], "research": [] }, "effects": {} - }, - "tether_anchors": { - "label": "Tether Anchoring", - "description": "Tether relays that bolt to worlds and stations you hold. Their zones stitch into your own — the union of every ring you anchor becomes open space, with no wall between them.", - "icon": "anchor", - "duration": 120, - "requires": ["tether_l2"], - "unlocks": { "builds": [], "research": [] }, - "effects": { "capability": "tether-anchors" } - }, - "signal_amp": { - "label": "Signal Amplification", - "description": "Booster coils for the ship's long-range array. Faint emissions — distant beacons, far stations, half-dead transponders — resolve at range, and scans return cleaner hits.", - "icon": "signal", - "duration": 60, - "requires": ["tether_l1"], - "unlocks": { "builds": [], "research": [] }, - "effects": { "capability": "signal-amplification" } } } } diff --git a/dev/compass-far.mjs b/dev/compass-far.mjs index 9e46e59..e942488 100644 --- a/dev/compass-far.mjs +++ b/dev/compass-far.mjs @@ -156,8 +156,8 @@ const run = async () => { scene.compass.onChipOut(e); // the chip's pointerout handler check('hover OUT: a stray exit doesn\u2019t yank it down (grace armed)', e.mode === 'full' && e.collapseAt != null); - // Grace (400 ms) + the fold fade + headroom. - await wait(900); + // Grace (2000 ms) + the fold fade + headroom. + await wait(2600); check('hover OUT (grace runs out): folds back to the compact display (still far)', e.mode === 'small' && e.w === cfg.smallBox && e.typeText.alpha < 0.1); @@ -171,7 +171,7 @@ const run = async () => { (() => { scene.compass.onChipOver(e); return e.collapseAt === null && e.mode === 'full'; })()); // Leave it for good: folded back to the small box before the guard tests. scene.compass.onChipOut(e); - await wait(900); + await wait(2600); check('final leave: back to the compact display', e.mode === 'small' && e.w === cfg.smallBox); // --- The scene's click guard around the small box -------------------- diff --git a/dev/discovery.test.mjs b/dev/discovery.test.mjs index b4f1c74..0afe73b 100644 --- a/dev/discovery.test.mjs +++ b/dev/discovery.test.mjs @@ -281,7 +281,7 @@ check('lerpAngle eases the short way (3→−3, no long-way sweep)', Math.abs(le check( 'far display is configured (farDistance 5120 / arrow 0.6 / smallBox 26 / intent delays)', fc.farDistance === 5120 && fc.farArrowScale === 0.6 && fc.smallBox === 26 && fc.smallHitSlack === 16 - && fc.expandDelay === 300 && fc.collapseDelay === 400, + && fc.expandDelay === 300 && fc.collapseDelay === 2000, ); check('isFarTarget: beyond farDistance ⇒ compact', isFarTarget({ x: 6000, y: 0 }, { x: 0, y: 0 }, 5120) === true); check('isFarTarget: exactly at farDistance ⇒ full', isFarTarget({ x: 5120, y: 0 }, { x: 0, y: 0 }, 5120) === false); @@ -299,7 +299,7 @@ check('lerpAngle eases the short way (3→−3, no long-way sweep)', Math.abs(le const compass = new DiscoveryCompass(fakeScene); check('compass far defaults match the config (incl. intent delays)', compass.farDistance === 5120 && compass.smallBox === 26 && compass.farArrowScale === 0.6 - && compass.expandDelay === 300 && compass.collapseDelay === 400); + && compass.expandDelay === 300 && compass.collapseDelay === 2000); const noop = () => {}; const entry = { mode: 'full', diff --git a/dev/research-builds.test.mjs b/dev/research-builds.test.mjs index b5a0896..eb9fd69 100644 --- a/dev/research-builds.test.mjs +++ b/dev/research-builds.test.mjs @@ -170,17 +170,16 @@ check('model: issues(tree) is clean', Array.isArray(issues(tree)) && issues(tree // 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 normalizes {builds, research}', JSON.stringify(unlocksOf(tree, 'tether_l2')) === JSON.stringify({ builds: ['tether-l2'], research: ['tether_l3'] })); check('model: unlocksOf(tether_l3) declares the level-3 build', JSON.stringify(unlocksOf(tree, 'tether_l3')) === JSON.stringify({ builds: ['tether-l3'], research: [] })); -check('model: unlocksOf is empty for a node without unlocks', JSON.stringify(unlocksOf(tree, 'signal_amp')) === JSON.stringify({ builds: [], research: [] })); +check('model: unlocksOf normalizes a node with no builds', JSON.stringify(unlocksOf(tree, 'tether_l1')) === JSON.stringify({ builds: [], research: ['tether_l2'] })); 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 = 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); +check('model: layout rows = 3 (l1 → l2 → l3)', layout.rows === 3); +check('model: layout levels — l1 root, l2 tier 1, l3 tier 2', + layout.level.tether_l1 === 0 && layout.level.tether_l2 === 1 && layout.level.tether_l3 === 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 @@ -188,17 +187,16 @@ const state = new ResearchState(); for (const id of tree.starting) state.unlock(tree.id, id); check('state: a fresh run already owns Tether Level 1', state.isUnlocked('exploration', 'tether_l1')); check('state: Tether Level 2 is the FIRST thing to research', isAvailable(tree, state, 'tether_l2')); -check('state: Signal Amplification is also available (requires only L1)', isAvailable(tree, state, 'signal_amp')); check('state: Tether Level 3 waits on L2 (locked)', !isAvailable(tree, state, 'tether_l3')); check('state: missingRequires names the parents (L3 ← L2)', JSON.stringify(missingRequires(tree, state, 'tether_l3')) === JSON.stringify(['tether_l2'])); // one project at a time — the in-flight slot check('state: start() claims the slot', state.start('exploration', 'tether_l2', 60_000, 0) === true); -check('state: a second start() is refused while one runs', state.start('exploration', 'signal_amp', 60_000, 0) === false); +check('state: a second start() is refused while one runs', state.start('exploration', 'tether_l3', 60_000, 0) === false); check('state: progress runs 0→1 over the duration', state.progress(0).fraction === 0 && state.progress(60_000).fraction === 1); check('state: tick() reports completion at the deadline', state.tick(59_999).length === 0 && state.tick(60_000).length === 1); check('state: L2 is researched after completion', state.isUnlocked('exploration', 'tether_l2')); -check('state: L3 + Tether Anchoring open up next', isAvailable(tree, state, 'tether_l3') && isAvailable(tree, state, 'tether_anchors')); +check('state: L3 opens up next', isAvailable(tree, state, 'tether_l3')); // save/restore round-trip — the in-flight project keeps its remaining time const state2 = new ResearchState(); diff --git a/dev/research-shot.mjs b/dev/research-shot.mjs index 3ebcc93..95cd64e 100644 --- a/dev/research-shot.mjs +++ b/dev/research-shot.mjs @@ -2,7 +2,7 @@ * Dev-only: boot GameScene and open the RESEARCH console (depth 80) so a * headless screenshot shows the whole feature — left video feed (muted * loop), category tabs, the top→down tech tree (Tether Level 1 owned → - * Level 2 + Signal Amplification available), the detail readout with the + * Level 2 available), the detail readout with the * RESEARCH button, and a project started in flight (deck bar + status). * * node dev/slow-server.mjs 8081 # serves / + /sleep?ms=N diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index 21dc8fe..14fa738 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -1183,7 +1183,7 @@ 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–3, Tether Anchoring, Signal Amplification); second category + Level 1–3); second category **Mining** is live (Improved Mining Arm → Advanced Mining Arm + Improved Mining Storage — all 60 s blueprints, each unlocking its ship-upgrade build in the console's Mining tab); add a category = diff --git a/js/ui/DiscoveryCompass.js b/js/ui/DiscoveryCompass.js index 6e212d5..d82a592 100644 --- a/js/ui/DiscoveryCompass.js +++ b/js/ui/DiscoveryCompass.js @@ -82,7 +82,7 @@ export class DiscoveryCompass extends Phaser.GameObjects.Container { // after pointer-enter, the fold-back waits collapseDelay after // pointer-leave (hover back within the grace and it stays expanded). this.expandDelay = Math.max(0, cfg.expandDelay ?? 300); - this.collapseDelay = Math.max(0, cfg.collapseDelay ?? 400); + this.collapseDelay = Math.max(0, cfg.collapseDelay ?? 2000); /** @type {Map} target id → entry (arrow, chip, angle…) */ this.entries = new Map(); // Autopilot seam: clicking a chip calls this with the target's id —