diff --git a/README.md b/README.md index fbe9a57..e21af65 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/images/originals/rocky-planet-01.png b/assets/images/originals/rocky-planet-01.png index 5d08da0..ca0aed3 100644 Binary files a/assets/images/originals/rocky-planet-01.png and b/assets/images/originals/rocky-planet-01.png differ diff --git a/assets/images/originals/rockey-planet-02.png b/assets/images/originals/rocky-planet-02.png similarity index 100% rename from assets/images/originals/rockey-planet-02.png rename to assets/images/originals/rocky-planet-02.png diff --git a/assets/images/originals/rocky-planet-03-a.png b/assets/images/originals/rocky-planet-03-a.png new file mode 100644 index 0000000..4713265 Binary files /dev/null and b/assets/images/originals/rocky-planet-03-a.png differ diff --git a/assets/images/originals/rocky-planet-03-b.png b/assets/images/originals/rocky-planet-03-b.png new file mode 100644 index 0000000..288ac7e Binary files /dev/null and b/assets/images/originals/rocky-planet-03-b.png differ diff --git a/data/builds.json b/data/builds.json index f9e4c74..1ef4f15 100644 --- a/data/builds.json +++ b/data/builds.json @@ -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"], diff --git a/data/landing.json b/data/landing.json index 7230eee..7701fb0 100644 --- a/data/landing.json +++ b/data/landing.json @@ -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" } ] } diff --git a/data/planets.json b/data/planets.json index 292ed54..bd20ebf 100644 --- a/data/planets.json +++ b/data/planets.json @@ -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] diff --git a/data/research/exploration.json b/data/research/exploration.json index 5e38188..74ec45e 100644 --- a/data/research/exploration.json +++ b/data/research/exploration.json @@ -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"], diff --git a/data/tether.json b/data/tether.json index dfdaded..7362d2c 100644 --- a/data/tether.json +++ b/data/tether.json @@ -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, diff --git a/dev/asteroids.test.mjs b/dev/asteroids.test.mjs index b4a9be7..714d2e1 100644 --- a/dev/asteroids.test.mjs +++ b/dev/asteroids.test.mjs @@ -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 --------------------------------------------------------- { diff --git a/dev/tether-flow.html b/dev/tether-flow.html new file mode 100644 index 0000000..889fb6a --- /dev/null +++ b/dev/tether-flow.html @@ -0,0 +1,17 @@ + + + + + Orbit — dev tether-l2 build flow test + + + + + +
+ + + diff --git a/dev/tether-flow.mjs b/dev/tether-flow.mjs new file mode 100644 index 0000000..5adb6f7 --- /dev/null +++ b/dev/tether-flow.mjs @@ -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); +} diff --git a/dev/tether.test.mjs b/dev/tether.test.mjs index 30fdf48..1bfa22d 100644 --- a/dev/tether.test.mjs +++ b/dev/tether.test.mjs @@ -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); diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md index 542a2ed..5d18f78 100644 --- a/docs/PROJECT_NOTES.md +++ b/docs/PROJECT_NOTES.md @@ -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) diff --git a/js/galaxy/SystemGenerator.js b/js/galaxy/SystemGenerator.js index 5f28f99..a3f116e 100644 --- a/js/galaxy/SystemGenerator.js +++ b/js/galaxy/SystemGenerator.js @@ -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); } diff --git a/js/scenes/SurfaceScene.js b/js/scenes/SurfaceScene.js index 78749db..7913536 100644 --- a/js/scenes/SurfaceScene.js +++ b/js/scenes/SurfaceScene.js @@ -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); diff --git a/js/tether/Tether.js b/js/tether/Tether.js index 480493a..7ce1cef 100644 --- a/js/tether/Tether.js +++ b/js/tether/Tether.js @@ -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); }