Auto-install level-1 tether on first landing of any world

- ensureLandingTether runs before SurfaceScene launch so the surface HUD
  already reads Tether Level 1; free + instant, no console step
- Extend tether lookups (_applyBuildEffects, tetherLevelFor,
  _rematerializeBuiltTethers, applyRestore) to include systemStations so
  station tethers anchor, level up, and re-anchor like planets'
- Idempotent: home seed, existing build record, or an already-hosted
  tether all short-circuit; adopt rather than double-anchor
- tether-l1 targets now ["planet", "station"]; update descriptions in
  builds.json, README, PROJECT_NOTES
- Reorder MapWindow tabs to CURRENT SYSTEM / GALAXY / SYSTEM (galaxy is
  the live middle tab) and keep SYSTEM as the armed standby target
This commit is contained in:
Brian Fertig 2026-09-09 13:11:38 -06:00
parent 0e83021542
commit a4fa37ca46
6 changed files with 86 additions and 29 deletions

View File

@ -136,8 +136,10 @@ node dev/server.mjs 8080
(research, tether level); installed ones read **BUILT ✓**. Selecting a
build shows its dossier (glyph + description) with the **highlighted
cost** and a **BUILD** button that appears only when the build is
available and affordable (one build at a time, `maxConcurrent: 1`). The
home world starts with **Tether Level 1** already installed; **Tether
available and affordable (one build at a time, `maxConcurrent: 1`).
Every world — planet or deep-space station — gets **Tether Level 1**
installed automatically the moment you first land (free + instant, no
console step); the home world ships with it from the start. **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 10240 px (world state that outlives

View File

@ -46,10 +46,10 @@
"builds": {
"tether-l1": {
"label": "Tether - Level 1",
"description": "The world's first tether: a level-1 field (range 5120 px) anchoring the ship's travel rim around the planet. The home world ships with it already in place; on every other world this is the build that installs it.",
"description": "The world's first tether: a level-1 field (range 5120 px) anchoring the ship's travel rim around the world. Installed automatically the moment you first land — free + instant, on planets and deep-space stations alike (GameScene.ensureLandingTether); the home world ships with it already in place.",
"icon": "tether",
"category": "planet",
"targets": ["planet"],
"targets": ["planet", "station"],
"cost": {},
"duration": 0,
"requires": [],
@ -187,7 +187,7 @@
"icon": "tether | tether2 | tether3 | anchor | signal | mining | mining2 | storage | diamond — procedural glyph (js/research/ResearchIcons.js)",
"category": "planet | cargo | mining — which tab of the console (registry above)",
"targets": ["planet"],
"_targets": "[\"planet\"] = installed on the planet's surface; [\"ship\"] = a SHIP upgrade (mining arms/storage): built from any planet's console, the effect is ship-wide and it counts as BUILT wherever the ship lands (never re-bought)",
"_targets": "[\"planet\", \"station\"] = installed on the world's surface (a planet / a deep-space station); [\"ship\"] = a SHIP upgrade (mining arms/storage): built from any planet's console, the effect is ship-wide and it counts as BUILT wherever the ship lands (never re-bought)",
"cost": {
"credits": 0,
"minerals": 200

View File

@ -1,5 +1,5 @@
{
"_comment": "MAP — the deck's MAP button (to the right of SHIP): the full-screen cartography console, js/ui/MapWindow.js (depth 80, the ResearchWindow idiom). LEFT = the cartography feed (assets/videos/map.mp4, a muted 2:3 loop, the same recipe as the research archive feed). RIGHT = tabs on top (CURRENT SYSTEM — the live system chart, SYSTEM — a CHARTED star's chart, picked from the GALAXY tab; armed until then, and GALAXY — the whole-galaxy chart: glowing, pulsing stars per archetype, the jump-lane web with traveled lanes bright + flow packets, the charted region's outline — js/ui/GalaxyView.js), and the readout on the bottom (per tab: the system's discovery/resources, or the galaxy's charted stats). THE CHART (the magic, drawn to a canvas texture): the whole solar system — every object's extent (planets, stations, gates AND asteroid clusters, discovered or not) + `bounds.padding` px of padding on each edge — fitted to the plate. Only DISCOVERED objects are drawn (planets / stations / gates / rock fields, small representations). The TETHER RANGE is drawn as the union boundary (Tether.visibleArcs) with a soft zone glow inside, and everything OUTSIDE the tether union is grayed out (a fog-of-war dim pass with feathered rim) — so the chart reads as 'here is what you have charted, and here is the room to expand'. A live SHIP marker + hover tooltips + click-to-autopilot (GameScene.autopilotTo) sit on top. All colors/motion are steered here.",
"_comment": "MAP — the deck's MAP button (to the right of SHIP): the full-screen cartography console, js/ui/MapWindow.js (depth 80, the ResearchWindow idiom). LEFT = the cartography feed (assets/videos/map.mp4, a muted 2:3 loop, the same recipe as the research archive feed). RIGHT = tabs on top (CURRENT SYSTEM — the live system chart, GALAXY — the whole-galaxy chart: glowing, pulsing stars per archetype, the jump-lane web with traveled lanes bright + flow packets, the charted region's outline — js/ui/GalaxyView.js, and SYSTEM — a CHARTED star's chart, picked from the GALAXY tab; armed until then), and the readout on the bottom (per tab: the system's discovery/resources, or the galaxy's charted stats). THE CHART (the magic, drawn to a canvas texture): the whole solar system — every object's extent (planets, stations, gates AND asteroid clusters, discovered or not) + `bounds.padding` px of padding on each edge — fitted to the plate. Only DISCOVERED objects are drawn (planets / stations / gates / rock fields, small representations). The TETHER RANGE is drawn as the union boundary (Tether.visibleArcs) with a soft zone glow inside, and everything OUTSIDE the tether union is grayed out (a fog-of-war dim pass with feathered rim) — so the chart reads as 'here is what you have charted, and here is the room to expand'. A live SHIP marker + hover tooltips + click-to-autopilot (GameScene.autopilotTo) sit on top. All colors/motion are steered here.",
"enabled": true,
"title": "STELLAR CARTOGRAPHY",
"meta": "NAV DATA // LIVE FEED",
@ -12,25 +12,25 @@
]
},
"tabs": {
"_comment": "The sockets above the chart (top right), equal widths. `currentSystem` is the live system chart (default). `system` is a CHARTED star's chart (the same painter, no ship/tether) — it wakes from standby the first time the player picks that star on the GALAXY tab; until then it's the armed killswitch. `galaxy` is the whole-galaxy chart (js/ui/GalaxyView.js). `standby: true` on ANY socket is the killswitch — the tab renders dimmed with its standbyTag and a press fires the scene's onLocked (a console toast) instead of switching.",
"_comment": "The sockets above the chart (top right), equal widths, in this order. `currentSystem` is the live system chart (default). `galaxy` is the whole-galaxy chart (js/ui/GalaxyView.js). `system` is a CHARTED star's chart (the same painter, no ship/tether) — it wakes from standby the first time the player picks that star on the GALAXY tab; until then it's the armed killswitch. `standby: true` on ANY socket is the killswitch — the tab renders dimmed with its standbyTag and a press fires the scene's onLocked (a console toast) instead of switching.",
"currentSystem": {
"id": "currentSystem",
"label": "Current System",
"accent": "#00e5ff"
},
"system": {
"id": "system",
"label": "System",
"accent": "#a8ffc4",
"standby": true,
"standbyTag": "NO TARGET"
},
"galaxy": {
"id": "galaxy",
"label": "Galaxy",
"accent": "#ffc94d",
"standby": false,
"standbyTag": "OFFLINE"
},
"system": {
"id": "system",
"label": "System",
"accent": "#a8ffc4",
"standby": true,
"standbyTag": "NO TARGET"
}
},
"galaxyLockedToast": "GALAXY MAP OFFLINE — SECTOR DATA NOT ACQUIRED",

View File

@ -396,12 +396,6 @@ the plate:
- **CURRENT SYSTEM** — the system chart (canvas-painted: discovered
objects, the tether union, the fog of war), hover + ENGAGE AUTOPILOT,
wheel zoom / drag pan. Unchanged by the galaxy work.
- **SYSTEM** — the same painter applied to a CHARTED star's system
(no ship/tether): pick a charted star on the GALAXY tab to arm it, then
click an object on its chart to **SET DESTINATION** (see the Route
section below). Locked ("NO SYSTEM TARGET") until a charted star is
picked. The shared `ConfirmOverlay` is label-sized to fit the dialog
(`baseWidth` + measured width, per-show `width`/`height` overrides).
- **GALAXY** (live — `tabs.galaxy.standby` is the killswitch; `true`
returns it to the old "OFFLINE" toast) — the whole-galaxy chart,
`js/ui/GalaxyView.js`, fed by `GameScene.galaxySnapshot()`
@ -431,6 +425,12 @@ the plate:
`jumpThroughGate`); dormant lane / no lane → the readouts. The
window is a pure view: the galaxy tab polls `getGalaxy()` on the
same 500 ms cycle the system chart is.
- **SYSTEM** — the same painter applied to a CHARTED star's system
(no ship/tether): pick a charted star on the GALAXY tab to arm it, then
click an object on its chart to **SET DESTINATION** (see the Route
section below). Locked ("NO SYSTEM TARGET") until a charted star is
picked. The shared `ConfirmOverlay` is label-sized to fit the dialog
(`baseWidth` + measured width, per-show `width`/`height` overrides).
**The run's footprint (save-ready):** `GameScene` keeps two
registry-backed sets (the `activatedGates` pattern) — `visitedSystems`
@ -905,6 +905,22 @@ must not un-install the home world's level-1 tether. A player resuming a
run always has Tether - Level 1 built on home (pinned in
`dev/builds.test.mjs`).
**First-landing auto-install (tether-l1):** `GameScene.ensureLandingTether`
runs in `startLanding` — BEFORE the SurfaceScene launch, so the surface
HUD's `tetherLevel` snapshot already reads `Tether Level 1`: the world
(planet or deep-space station) hosts its level-1 tether the moment the
player first lands, with no build-console step (the build is free +
instant — `cost: {}`, `duration: 0`). It applies the `tether-l1`
effects (anchor + toast) and marks the build installed (the console
reads BUILT; the L2 `planetRequires.tetherLevel: 1` gate is met). It is
idempotent — the home seed, an installed record, or an existing tether
all short-circuit; a world that already hosts a tether without a record
is adopted, never anchored twice. The world lookups it feeds
(`_applyBuildEffects`, `tetherLevelFor`, `_rematerializeBuiltTethers`)
include `systemStations`, so station tethers anchor, level up, and
re-anchor across jump cuts like planets' do, and `applyRestore`'s
re-key list knows station names too.
**Ship-scoped builds (the Mining tab) — one-off installs, ship-wide
effect:** `targets: ["ship"]` (the mining arms + storage, data/builds.json
→ the Mining category) are installed from any planet's console but the

View File

@ -2667,6 +2667,12 @@ export class GameScene extends Phaser.Scene {
// the video/music lookups need the number). `station` tells the
// surface deck which clip set the frame indexes into (data/landing.json
// → videos for planets, stationVideos for station variants).
// FIRST LANDING (data/builds.json → tether-l1): the world hosts its
// level-1 tether the moment the player lands — free + instant, so no
// build-console step (planets and deep-space stations alike). Before
// the launch, so the surface HUD's tetherLevel snapshot already
// reads 1.
this.ensureLandingTether(name);
this.scene.launch('SurfaceScene', {
frame: Number(target.frame ?? target.sheetFrame ?? 0),
name,
@ -2688,6 +2694,7 @@ export class GameScene extends Phaser.Scene {
if (!name || !this.tetherField) return 0;
const obj =
this.systemPlanets.find((p) => p.discoveryName === name) ??
this.systemStations.find((s) => s.discoveryName === name) ??
(name === this.planet?.discoveryName ? this.planet : null);
if (!obj) return 0;
const t = this.tetherField.tethers.find(
@ -2696,6 +2703,36 @@ export class GameScene extends Phaser.Scene {
return t ? t.level : 0;
}
/**
* FIRST-LANDING INSTALL (data/builds.json tether-l1): the moment the
* player lands on a world a planet or a deep-space station the
* world hosts its level-1 tether (5120 px, data/tether.json), with no
* build-console step: the build is free + instant (cost {}, duration
* 0), so the landing simply completes it. The record then reads BUILT
* in the console, and the level-2 build's planet gate (a world holding
* a level-1 tether) is met. Idempotent the home world's starting
* seed (tether-l1.starting) already records it, and a world that
* already hosts a tether is adopted, never anchored twice.
*/
ensureLandingTether(worldName) {
const state = this.buildState;
if (!worldName || !state) return;
if (state.isBuilt(worldName, 'tether-l1')) return;
if (this.tetherLevelFor(worldName) >= 1) {
// The world already hosts a tether without a build record — adopt
// it (the console reads BUILT, the L2 gate is met) instead of
// anchoring a second field.
state.markBuilt(worldName, 'tether-l1');
return;
}
const def = defById('tether-l1');
if (!def?.effects) return; // build system off / the data is gone
this._applyBuildEffects(worldName, def.effects); // anchors + the toast
state.markBuilt(worldName, 'tether-l1');
this.playSfx('construct'); // the power-up tick — the manual build's voice
this._questRefresh(); // a tether requirement may have just landed
}
/**
* Mining phase changes (Mining onPhase): the scene's share of the
* sequence the ship's STATE, the ship lock, the console calls, the sfx.
@ -3976,6 +4013,7 @@ export class GameScene extends Phaser.Scene {
// home world's name).
const obj =
this.systemPlanets.find((p) => p.discoveryName === planetName) ??
this.systemStations.find((s) => s.discoveryName === planetName) ??
(planetName === this.planet?.discoveryName ? this.planet : null);
const t = this.tetherField?.tethers.find(
(tt) => tt.label === planetName ||
@ -4402,6 +4440,7 @@ export class GameScene extends Phaser.Scene {
for (const [planetName, builds] of this.buildState.built.entries()) {
const world =
this.systemPlanets.find((p) => p.discoveryName === planetName) ??
this.systemStations.find((s) => s.discoveryName === planetName) ??
(planetName === this.planet?.discoveryName ? this.planet : null);
if (!world) continue; // another system's world — re-anchors on the way back
let level = 0;
@ -4467,6 +4506,7 @@ export class GameScene extends Phaser.Scene {
const known = [
this.planet?.discoveryName,
...(this.systemPlanets ?? []).map((p) => p.discoveryName),
...(this.systemStations ?? []).map((s) => s.discoveryName),
...(this.tetherField?.tethers ?? []).map((t) => t.label),
];
for (const [k, v] of [...fresh.built.entries()]) {

View File

@ -6,17 +6,16 @@
* contract.
*
* Right column, top to bottom:
* tabs CURRENT SYSTEM (the live system chart) / SYSTEM (a CHARTED
* star's chart the same painter; armed until the player picks that
* star on the GALAXY tab) / GALAXY (the whole-galaxy chart,
* js/ui/GalaxyView.js): glowing, pulsing stars per system (colored
* tabs CURRENT SYSTEM (the live system chart) / GALAXY (the whole-galaxy
* chart, js/ui/GalaxyView.js): glowing, pulsing stars per system (colored
* by archetype), the jump-lane web (traveled lanes bright with flow
* packets, frontier lanes mid, unexplored faint), the charted region
* (convex hull of visited systems, inflated), HOME + SHIP markers.
* Hover = the star's readout + its link state to the current system;
* click a CHARTED star = its chart in the SYSTEM tab (onStarOpen);
* uncharted stars are readouts only. Clicking a `standby` socket
* fires `onLocked` (toast).
* uncharted stars are readouts only) / SYSTEM (a CHARTED star's chart
* the same painter; armed until the player picks that star on the
* GALAXY tab). Clicking a `standby` socket fires `onLocked` (toast).
* the CHART the current system drawn on an offscreen canvas and
* shown as a Phaser image:
* - frame = the furthest objects (planets, stations, jump gates and
@ -1299,8 +1298,8 @@ export class MapWindow extends Phaser.GameObjects.Container {
: [];
if (!tabs.length) {
tabs.push({ id: 'currentSystem', label: 'Current System', accent: '#00e5ff' });
tabs.push({ id: 'system', label: 'System', accent: '#a8ffc4', standby: true, standbyTag: 'NO TARGET' });
tabs.push({ id: 'galaxy', label: 'Galaxy', accent: '#ffc94d', standby: true, standbyTag: 'OFFLINE' });
tabs.push({ id: 'system', label: 'System', accent: '#a8ffc4', standby: true, standbyTag: 'NO TARGET' });
}
let x = rightX;
const avail = rightW;
@ -1394,11 +1393,11 @@ export class MapWindow extends Phaser.GameObjects.Container {
/**
* The tab switch the plate has three lives:
* CURRENT SYSTEM the live system chart (hover, ENGAGE, zoom, pan)
* GALAXY the whole-galaxy chart (GalaxyView: stars, lanes,
* the charted region)
* SYSTEM a CHARTED star's chart (the same painter; the ship
* marker + course line are hidden the ship isn't
* in that system)
* GALAXY the whole-galaxy chart (GalaxyView: stars, lanes,
* the charted region)
* The plate's input surface moves between mapImg and the galaxy plate
* one at a time (Phaser 4's topOnly input: both catching would
* double-fire). Each life is driven from the same 500 ms poll in