Changes to Research Areas
This commit is contained in:
parent
1c5767b095
commit
b1ae21e95e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"smallBox": 26,
|
||||
"smallHitSlack": 16,
|
||||
"expandDelay": 300,
|
||||
"collapseDelay": 400
|
||||
"collapseDelay": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 --------------------
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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<string, object>} target id → entry (arrow, chip, angle…) */
|
||||
this.entries = new Map();
|
||||
// Autopilot seam: clicking a chip calls this with the target's id —
|
||||
|
|
|
|||
Loading…
Reference in New Issue