Bump tether radius growth to 2× and refresh HUD on build completion
- Change radiusGrowth from 1.25 to 2.0 so level N doubles the previous level (L2 = 10240, L3 = 20480, L4 = 40960 px) - Update all data descriptions, docs, and test defaults to match - Add refreshHudTetherLevel() in SurfaceScene so the HUD line re-decodes when a tether build completes while on-surface - Rename rocky-planet-02 asset (typo fix), add 03-a/03-b variants, point rocky frames at indices 6–8 in planets.json - Add dev/tether-flow harness to exercise the full L2 build flow end-to-end
This commit is contained in:
parent
1d1cb2c150
commit
4fd1f05359
|
|
@ -117,7 +117,7 @@ node dev/server.mjs 8080
|
|||
home world starts with **Tether – Level 1** already installed; **Tether
|
||||
– Level 2** needs the level-2 tether research plus a world holding a
|
||||
level-1 tether, and installs in **20 s for 200 minerals** — the planet's
|
||||
tether range then grows from 5120 to 6400 px (world state that outlives
|
||||
tether range then grows from 5120 to 10240 px (world state that outlives
|
||||
the surface stay). While a build runs the whole deck is locked (BUILD
|
||||
stays open to watch the progress); completion fires the build's
|
||||
`effects` and a toast. Build records + the in-progress build persist in
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
|
|
@ -57,7 +57,7 @@
|
|||
},
|
||||
"tether-l2": {
|
||||
"label": "Tether - Level 2",
|
||||
"description": "Extends the planet's tether to level 2: the field's range grows from 5120 px to 6400 px around the world. Requires the level-2 tether research and a planet already holding a level-1 tether. Installed in 20 seconds for 200 minerals.",
|
||||
"description": "Extends the planet's tether to level 2: the field's range grows from 5120 px to 10240 px around the world. Requires the level-2 tether research and a planet already holding a level-1 tether. Installed in 20 seconds for 200 minerals.",
|
||||
"icon": "tether2",
|
||||
"category": "planet",
|
||||
"targets": ["planet"],
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@
|
|||
},
|
||||
"videos": [
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "terran-land-02.mp4", "surface": "terran-surface-02.mp4", "takeoff": "terran-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "terran-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "terran-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-02.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-01.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-01.mp4", "shop": "terran-shop-01.mp4" }
|
||||
{ "land": "gasgiant-land-02.mp4", "surface": "terran-surface-02.mp4", "takeoff": "gasgiant-takeoff-02.mp4", "shop": "terran-shop-01.mp4" },
|
||||
{ "land": "gasgiant-land-03.mp4", "surface": "terran-surface-01.mp4", "takeoff": "gasgiant-takeoff-03.mp4", "shop": "terran-shop-01.mp4" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"scale": 1.0,
|
||||
"frames": {
|
||||
"terran": [0, 1, 2],
|
||||
"rocky": [0, 1, 2],
|
||||
"rocky": [6, 7, 8],
|
||||
"gas": [3, 4, 5],
|
||||
"ice": [0, 1, 2],
|
||||
"lava": [0, 1, 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) — except Tether Level 2, which is the BLUEPRINT only (effects: {}): the player installs it as a TETHER - LEVEL 2 build on a world holding a level-1 tether (data/builds.json → tether-l2; the build carries the effect). 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 raises the home tether's level (effects.tether.level → TetherField.setLevel; radius = 5,120 × 2^(level−1), see data/tether.json and docs/PROJECT_NOTES.md) — except Tether Level 2, which is the BLUEPRINT only (effects: {}): the player installs it as a TETHER - LEVEL 2 build on a world holding a level-1 tether (data/builds.json → tether-l2; the build carries the effect). 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,7 +13,7 @@
|
|||
},
|
||||
"tether_l2": {
|
||||
"label": "Tether Level 2",
|
||||
"description": "A second stage of field power. The blueprint for a stronger tether: install it with a TETHER - LEVEL 2 build on a world that holds a level-1 tether (200 minerals, 20 s — data/builds.json) and that world's boundary ring pulls out to 6,400 m.",
|
||||
"description": "A second stage of field power. The blueprint for a stronger tether: install it with a TETHER - LEVEL 2 build on a world that holds a level-1 tether (200 minerals, 20 s — data/builds.json) and that world's boundary ring pulls out to 10,240 m.",
|
||||
"icon": "tether",
|
||||
"duration": 60,
|
||||
"requires": ["tether_l1"],
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"tether_l3": {
|
||||
"label": "Tether Level 3",
|
||||
"description": "Third stage: 8,000 m of tether. The outer belt of an average system sits comfortably inside the ring — less time parked on the line, more time working it.",
|
||||
"description": "Third stage: 20,480 m of tether. The outer belt of an average system sits comfortably inside the ring — less time parked on the line, more time working it.",
|
||||
"icon": "tether",
|
||||
"duration": 150,
|
||||
"requires": ["tether_l2"],
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
"tether_l4": {
|
||||
"label": "Tether Level 4",
|
||||
"description": "Fourth stage: 10,000 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.",
|
||||
"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"],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,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": 1.25,
|
||||
"radiusGrowth": 2.0,
|
||||
"maxLevel": 12,
|
||||
"homeId": "home",
|
||||
"homeLevel": 1,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ check('distribution has target/min/max/jitter', Number.isFinite(D.targetObjects)
|
|||
check('starting system minimum ≥ 2', (D.startingSystemMinClusters ?? 2) >= 2);
|
||||
const P = A.placement ?? {};
|
||||
check('placement rules present', P.minObjectSpacing === 1024 && P.minRadius > 0 && P.maxRadius >= P.minRadius);
|
||||
const TETHER = config.get('tether.level1Radius', 5120) * Math.pow(config.get('tether.radiusGrowth', 1.25), Math.max(1, Math.floor(config.get('tether.homeLevel', 1))) - 1);
|
||||
const TETHER = config.get('tether.level1Radius', 5120) * Math.pow(config.get('tether.radiusGrowth', 2.0), Math.max(1, Math.floor(config.get('tether.homeLevel', 1))) - 1);
|
||||
|
||||
// --- Name synthesis ---------------------------------------------------------
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev tether-l2 build flow test</title>
|
||||
<base href="../" />
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/tether-flow.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Dev-only: TETHER-L2 BUILD FLOW — plays the real flow end-to-end
|
||||
* (menu → NEW GAME → land on the home world → build Tether - Level 2 →
|
||||
* the build completes) and reports what the tether field does with it:
|
||||
*
|
||||
* before: home tether id/level/radius
|
||||
* build: beginBuild() result (ok / refusal reason)
|
||||
* after : home tether id/level/radius, tetherLevelFor(home),
|
||||
* the surface HUD's cached level, built records
|
||||
*
|
||||
* node dev/server.mjs 8080
|
||||
* node dev/cdp-firefox.mjs http://127.0.0.1:8080/dev/tether-flow.html \
|
||||
* 'return window.__TETHER_FLOW__;' 40000
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||
import { MenuScene } from '../js/scenes/MenuScene.js';
|
||||
import { GameScene } from '../js/scenes/GameScene.js';
|
||||
import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
|
||||
const errors = [];
|
||||
const origWarn = console.warn.bind(console);
|
||||
console.warn = (...a) => { errors.push(a.map(String).join(' ')); origWarn(...a); };
|
||||
const origErr = console.error.bind(console);
|
||||
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
|
||||
window.addEventListener('error', (e) => errors.push(String(e.message)));
|
||||
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
|
||||
|
||||
const game = new Phaser.Game({ ...createGameConfig(), scene: [MenuScene, GameScene, SurfaceScene] });
|
||||
window.game = game;
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
async function waitFor(fn, tries = 200, everyMs = 100) {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await sleep(everyMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const tetherInfo = (gs) => {
|
||||
const home = gs.planet?.discoveryName;
|
||||
const t = gs.tetherField?.tethers?.find(
|
||||
(tt) => tt.label === home || (tt.x === 0 && tt.y === 0),
|
||||
);
|
||||
return t ? { id: t.id, x: t.x, y: t.y, level: t.level, radius: t.radius } : null;
|
||||
};
|
||||
|
||||
try {
|
||||
// 1) Menu → NEW GAME (the real entry).
|
||||
const menu = await waitFor(() => game.scene.getScene('MenuScene')?.newGameBtn ? game.scene.getScene('MenuScene') : null);
|
||||
if (!menu) throw new Error('menu never booted');
|
||||
menu.startNewGame();
|
||||
const gs = await waitFor(() => { const g = game.scene.getScene('GameScene'); return g?.ship && g?.buildState && g?.tetherField ? g : null; }, 300);
|
||||
if (!gs) throw new Error('GameScene never booted');
|
||||
await sleep(500);
|
||||
const home = gs.planet.discoveryName;
|
||||
const before = tetherInfo(gs);
|
||||
const beforeLevel = gs.tetherLevelFor(home);
|
||||
|
||||
// 2) Research is done (the build's only research gate).
|
||||
gs.researchState.unlock('exploration', 'tether_l2');
|
||||
|
||||
// 3) Land on the home world the game's own way, wait for the surface
|
||||
// stage (the stall guard advances after ~5 s without a clip).
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
const ss = await waitFor(() => { const c = game.scene.getScene('SurfaceScene'); return c?.phase === 'surface' ? c : null; }, 150, 200);
|
||||
if (!ss) throw new Error('surface stage never reached');
|
||||
await sleep(300);
|
||||
|
||||
// 4) Start the Tether - Level 2 build (the scene-side rules apply).
|
||||
// The cost (200 minerals) is covered — a fresh ship holds none.
|
||||
gs.ship.setMinerals(200);
|
||||
const res = gs.beginBuild(ss.planetName, 'tether-l2', game.loop.now);
|
||||
const active = gs.buildState.getActive();
|
||||
|
||||
// 5) Fast-forward the build to completion (the test does not wait out
|
||||
// the 20 s), then let the surface's own tick apply the effects.
|
||||
if (active) active.startedAt -= active.durationMs;
|
||||
// …and let the HUD line finish re-decoding to the new level.
|
||||
await sleep(2500);
|
||||
|
||||
const after = tetherInfo(gs);
|
||||
const out = {
|
||||
home,
|
||||
before,
|
||||
beforeLevel,
|
||||
build: { res, active: active ?? null },
|
||||
after,
|
||||
afterLevel: gs.tetherLevelFor(home),
|
||||
hudLevel: ss.tetherLevel,
|
||||
hudLine: ss.hudLine ? ss.hudLine.text : null,
|
||||
built: gs.buildState.isBuilt(ss.planetName, 'tether-l2'),
|
||||
arcs: after ? gs.tetherField._arcs.get(after.id).length : null,
|
||||
errors: errors.slice(0, 5),
|
||||
};
|
||||
window.__TETHER_FLOW__ = out;
|
||||
console.info('tether-flow:', JSON.stringify(out));
|
||||
} catch (err) {
|
||||
window.__TETHER_FLOW__ = { fatal: String(err.message), errors: errors.slice(0, 5) };
|
||||
console.error('tether-flow: fatal', err);
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ const dist = (x, y, t) => Math.hypot(x - t.x, y - t.y);
|
|||
{
|
||||
const r1 = Tether.radiusForLevel(1);
|
||||
check(`level 1 radius is 5120 px (got ${r1})`, r1 === 5120);
|
||||
const growth = config.get('tether.radiusGrowth', 1.25);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -247,10 +247,10 @@ The ship starts with **one level-1 tether** anchored on the home world
|
|||
`maxLevel`), the starting tether (`homeId`/`homeLevel`), and every
|
||||
visual/feel parameter.
|
||||
|
||||
**Upgrade economics (planned):** radius(level) = 5120 × 1.25^(level−1) —
|
||||
level 4 = 10000 px, which reaches every object of a ≤ 10-object system
|
||||
**Upgrade economics (planned):** radius(level) = 5120 × 2^(level−1) —
|
||||
level 2 = 10240 px, which reaches every object of a ≤ 10-object system
|
||||
(all within 10240 px of the home world); the far 8-ring of the rare
|
||||
11-object system (up to ~13380 px out) wants one level more. Extra
|
||||
11-object system (up to ~13380 px out) fits inside level 3 (20480 px). Extra
|
||||
tethers anchored on planets/stations will be the "expand your reach" verb;
|
||||
the seams are in place.
|
||||
|
||||
|
|
@ -705,7 +705,7 @@ The player holds a REPUTATION (standing) on each planet and space station:
|
|||
- [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 (6400 px), effect and all, outliving the stay. Levels 3–4
|
||||
to level 2 (10240 px), effect and all, outliving the stay. Levels 3–4
|
||||
are 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)
|
||||
|
|
|
|||
|
|
@ -562,7 +562,7 @@ function relaxRockGaps(members, gapFactor, maxIter = 96) {
|
|||
function homeTetherRadius() {
|
||||
const level = Math.max(1, Math.floor(config.get('tether.homeLevel', 1)));
|
||||
const base = config.get('tether.level1Radius', 5120);
|
||||
const growth = config.get('tether.radiusGrowth', 1.25);
|
||||
const growth = config.get('tether.radiusGrowth', 2.0);
|
||||
return base * Math.pow(growth, level - 1);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,34 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
if (done) this.hudLines = null; // the text holds — stop the per-frame churn
|
||||
}
|
||||
|
||||
/**
|
||||
* The world's tether level changed under us (a tether build completed
|
||||
* on this world): the HUD's second line was a snapshot from landing,
|
||||
* so refresh it — "Tether Level N" re-decodes to the new level. A
|
||||
* no-op when the level is unchanged or the HUD is not up (take-off
|
||||
* stage, no surface clip).
|
||||
*/
|
||||
refreshHudTetherLevel(time) {
|
||||
const level = Math.max(0, Math.round(Number(this.gameScene?.tetherLevelFor(this.planetName) ?? 0)));
|
||||
if (level === this.tetherLevel) return;
|
||||
this.tetherLevel = level;
|
||||
if (!this.hudLine) return;
|
||||
const parts = [];
|
||||
if (this.planetType) parts.push(this.planetType);
|
||||
parts.push(`Tether Level ${this.tetherLevel}`);
|
||||
const line = parts.join(', ');
|
||||
const idx = this.hudLines?.findIndex((h) => h.text === this.hudLine) ?? -1;
|
||||
if (idx >= 0) {
|
||||
// Still decoding: retarget the in-flight scramble at the new line.
|
||||
const ln = this.hudLines[idx];
|
||||
ln.value = line;
|
||||
ln.dec = new ScrambleDecode(line, time, ln.dur);
|
||||
ln.settled = false;
|
||||
} else {
|
||||
this.hudLine.setText(line); // settled — swap the held text in place
|
||||
}
|
||||
}
|
||||
|
||||
/** The HUD belongs to the surface stage — the take-off clip owns the screen. */
|
||||
destroyHud() {
|
||||
this.hudName?.destroy();
|
||||
|
|
@ -804,6 +832,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
const def = buildDefs()[done[0].build];
|
||||
this.consoleNote(`BUILD COMPLETE — ${String(def?.label ?? done[0].build).toUpperCase()}`);
|
||||
this.playSfx('discovery'); // the 'something new is here' voice
|
||||
this.refreshHudTetherLevel(time); // the HUD's level line is a landing snapshot
|
||||
}
|
||||
}
|
||||
this.buildWindow?.update(time);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export class Tether {
|
|||
/** radius(level) = level1Radius × radiusGrowth^(level−1) (data/tether.json). */
|
||||
static radiusForLevel(level) {
|
||||
const l1 = config.get('tether.level1Radius', 5120);
|
||||
const g = config.get('tether.radiusGrowth', 1.25);
|
||||
const g = config.get('tether.radiusGrowth', 2.0);
|
||||
const n = Math.max(1, Math.round(level ?? 1));
|
||||
return l1 * Math.pow(g, n - 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue