Go to file
Brian Fertig 4312177c3d Add BOUNTY HUNT side quest with elite combat seam and verification
- Enable the `bounty` category in data/sidequests.json with level-2 gate,
  Neutral-only contexts, encounter chain odds, and 20 written leader dossiers
- Add pure bounty model to SideQuests.js: plan derivation, wave scripts, XP,
  sequential waypoint rule, and completion only via the mark's kill
- Extend combat seam (Waves/CombatSystem/EnemyShip) for a named elite unit
  deployed with the opening group at scaled hull/damage, plus haltWaves() so
  killing the mark stops deployments while survivors stay hostile
- Wire GameScene/SurfaceScene/JobBoardWindow for locked rows, dossier readout,
  sequential check-ins, engagement chain, and single idempotent claim
- Add dev/bounty-hunts.test.mjs (591 checks) and in-browser probe with JSON/PNG
  evidence; update patrol/sidequests tests for the new live category
2026-09-20 20:10:07 -06:00
.pi Add BOUNTY HUNT side quest with elite combat seam and verification 2026-09-20 20:10:07 -06:00
assets
…
data Add BOUNTY HUNT side quest with elite combat seam and verification 2026-09-20 20:10:07 -06:00
dev Add BOUNTY HUNT side quest with elite combat seam and verification 2026-09-20 20:10:07 -06:00
docs Add BOUNTY HUNT side quest with elite combat seam and verification 2026-09-20 20:10:07 -06:00
js Add BOUNTY HUNT side quest with elite combat seam and verification 2026-09-20 20:10:07 -06:00
lib
…
README.md
…
combat.html
…
index.html
…

README.md

Orbit

A procedurally generated space RPG in the spirit of Privateer, in top-down 2D. Built with Phaser 4 as plain ES6 modules — no build step, no package managers required to run.

Run it

Any static file server works (Python, Node, Caddy, nginx, …):

cd orbit
node dev/server.mjs 8080
# → http://localhost:8080

Why not python3 -m http.server? It sends Last-Modified with no Cache-Control, so browsers apply heuristic caching to the ES modules. While iterating, the browser then keeps running an old js/*.js module graph (e.g. a GameScene.js from before a feature landed) long after the files on disk changed — while the JSON data is fresh. That mix (new data + old JS) is the classic "it still behaves like the old version" bug, and clearing the JSON cache or hard-resetting the game does not fix it. The dev server above sends Cache-Control: no-store on every response, so a normal reload always runs what's on disk.

Must be served over http(s) — opening index.html via file:// won't work, because the game uses ES modules and fetches its JSON config.

Combat (tuned before it ships in the real game) has two fronts, and every combat number — thrust, speeds, hulls, shields, the lance, rams, respawns, the bar — lives in data/combat.json:

  • The training range (a standalone arena — where the feel is tuned): the dev server maps /combat → combat.html (any server can too). One fixed arena: you (WASD/arrows + SPACE) against a pack of the enemy craft (assets/images/ships-enemy.png). R resets the range, P freezes it (positions hold while you poke window.__COMBAT__ = { reset, spawnEnemy, player, kills } in DevTools).

  • In the main game (the integration stand-in — what players will eventually feel): index.html?combat boots the normal game, and when the player starts, a pack of hostiles spawns around the ship. The mouse steers; while a hostile is in range the ship auto-fires at the nearest one (the cannon tracks — js/combat/CombatSystem.js). While combat is live the action deck swaps for the combat bar (js/ui/CombatBar.js) — Diablo 4 style: vertical shields + hull on the left, the ENERGY gauge on the right, eight skill seats wearing keys 1–8 in the middle (the skills land later — for now they're empty seats; a press lands on the system's cast() stub). Wipe the pack and the deck comes back. Kills also earn combat XP (the run's level ledger — js/combat/XpManager.js; the curve is data/progression.json, each type's value is its own xp in data/combat.json) — a level-up bumps the ship's armor (the placeholder toughness), fires a burst + a distinct voice, and lands a line in the kill feed. Poke the live fight in DevTools via window.__ORBIT_COMBAT__ = { system, bar, player, kills, spawn, damagePlayer, cast, xp, grantXp }.

  • Feel — the handling is what the range tunes. Every feel number (thrust, top speed, drag, turn rate + turn response, throttle ramp/ decay, brake) lives in data/combat.json under combat.player, and both fronts run the same pure handleFeel integration (js/combat/Combat.js) — so what you feel in the range is what the main game ships. Thrust ramps in and decays out, turns ease on, and the brake kills the velocity instantly.

  • Feedback — VFX — every combat event has a distinct visual cue, all procedural (js/combat/Fx.js — canvas dots / additive glows, no new art): a tracer + muzzle flash on firing, a spark burst + damage number on impact, a cyan flash + damage number on a shield hit, an orange flash + damage number on a hull hit, a big burst + double ring on an enemy kill, a DOWN burst when the player goes down, and a REFORMED bloom on respawn. Shield reads cyan and hull reads orange in both the flash and the number, so a glance tells you which layer took the hit.

  • Feedback — SFX — the same ten events each get a distinct voice, through one shared event→voice map + playback path (js/combat/CombatSfx.js) so the two fronts can't drift: the player's cannon = mass_cannon (assets/fx/combat-mass-cannon.mp3), a hostile taking the player's shot = mass_cannon_hit (assets/fx/combat-mass-cannon-hit.mp3), the enemy's laser = laser (assets/fx/combat-laser.mp3), impact = ui_click, shield = solar_buzz, hull = mining_split, kill = thunder_01..04 (one at random), down = deconstruct, respawn = discovery, level-up = solar_flare_01.

  • Readability layer — a shared HUD (js/combat/CombatHud.js, pure rules in js/combat/Combat.js) makes the fight readable in both fronts: a rotating lock reticle on the current target (the same target the cannon auto-fires — one rule, one target), pulsing threat markers over every live hostile, off-screen arrows at hostiles outside the camera (the main game's camera follows the ship), a kill feed (newest-first, top-right), and a gauge flash — the combat bar's shields/hull gauge lights the moment it takes the hit.

  • Combat levels (XP) — kills grant the hostile's type's own xp (the roster sets XP-per-threat: the base craft 10, the bomber 25 — data/combat.json → enemy.xp), banked against a curve that's easy at first and hard later, never a grind (data/progression.json — base × n^power, shipped 50 × n², capped at level 80; five base kills buy level 1, the whole run to the cap is 8.7M XP). A crossed level fires a levelup EVENT (one per level — the armor bump (progression.reward: +3 hull / +2 shields, full heal), the burst + voice, the feed's line all ride it; the skill layer — active and passive, the post-cap progression — will subscribe to that same event later). At the cap the level holds and the overflow is harmlessly banked. The ledger is part of the save as { level, xp } (old saves load — a fresh run starts at level 0), and the bar's XP row (below the orbs — the gold summary strip) shows the level, the run toward the next, and the numbers (banked over the next cost; at the cap the banked total). node dev/xp.test.mjs covers the curve, the grants, the cap, the events, and the save round-trip.

  • Dev hooks + checkers — besides the two pokes above, every SFX call is logged to window.__COMBAT_SFX__ = { log, last } (capped), so the wiring is verifiable headless in both fronts. CDP checkers drive the real game and read state back: node dev/combat-flycheck.mjs (flight feel — t60/t30, spin, stop distance, top speed), node dev/combat.sfxcheck.mjs (all the SFX events fire through the shared path, in both the range and the main game), and node dev/combat-shot.mjs [outDir] (captures a labeled feedback frame per context through the production paths into dev/shots/ — the committed combat-range.png + combat-main.png are that evidence).

Current state — v0.2: a seedable galaxy

  • Main menu with Continue (resumes the newest save — grayed out while the save bank is empty), New Game, Load Game (the full slot bank), and a Galaxy Seed panel: the seed is displayed, editable (click it and type), and rerollable — and the menu shows what that seed builds (the galaxy's name, system count, archetype count) before you commit. Same seed ⇒ same galaxy.
  • Procedural galaxy: 90 star systems in a seeded wide (2:1, plate-shaped) field — even, organic spacing (Poisson disk, data/galaxy.json), the player's home in the lower-right corner, and a home→far difficulty axis split into near/middle/far zones — typed into six themed archetypes (data/systems.json) with per-type weights and per-zone mixing (galaxy.distribution.zoneMix) — the galaxy lays itself out on a progression the trade/combat economy can lean on.
  • Two-level generation: the whole galaxy roster is generated at New Game (~15 ms); each system's planets/moons/belts/settlements are generated lazily on arrival, deterministically (seed + system id), so lazy and eager give identical results.
  • A lived-in galaxy: the galaxy was settled long before you arrive. Every planet is settled (for now): colonies on habitable worlds, mining stations over the rest, cloud bases riding gas giants — plus stations adrift in open space and beacons, whose odds are per-archetype (data/systems.json) and thin out from the settled core to the wilder rim (data/galaxy.json). Barren systems (~10%, the objectCount → 0 stops) are the DEAD-END LEAVES of the jump-gate maze: they hold nothing but their single jump gate (and 1–2 asteroid clusters drifting inside that gate's tether, their only payload) and report charted · unclaimed; you enter and exit through the same gate. Non-home systems hold 0/2/3/4/5 objects (planets + free-space stations, data/systems.json → objectCount); every non-barren system holds at least one planet, and the gate's tether anchors on one of them (so the player can always build and expand out from the gate); the starting system always holds the home world, a gas giant, and a rocky world. Each settlement has a name, population, and an owner seam reserved for the factions/pirates to come. The current system's dossier (name, identity, what's there) shows top-left in the game scene.
  • Names with a point of view (data/naming.json): planets draw from a deep curated bank — colonial "New Denver" worlds beside alien "Klaxoria" worlds — and stations from a bank of official designations ("Deep Space SC-145") and smuggler hangouts ("Hell's Hideout"). Names are dealt out randomly per system without repeats until the bank is exhausted (a seeded shuffle, so the first N draws are N distinct names). Stars and the galaxy itself are still synthesised from syllable pools, so you never run out of those. Within a system a planet never shares a name with another planet and a station never with another station.
  • Game screen with a top-down ship (real spritesheet art — frame 0 of assets/images/ships-player.png, see data/ship.json): click anywhere to fly there in the current system's open space. Every system also holds 1–3 JUMP GATES — solid, discoverable exits that face their destination star — wired into a PURE SPANNING TREE gate network (data/gates.json, no shortcuts, no closed loops: exactly one route between any two systems — a maze of dead ends and long hauls); every gate is active: false for now — DORMANT (dimmed, field still) — and activation is the seam for the tether mechanic (an activated gate anchors a level-1 tether); the jump drive itself comes next. Discovered worlds get a screen-edge arrow + a name tag showing the world's name (e.g. HOME WORLD · ESHKAELURA); clicking the name tag autopilots the ship there (it arrives on the keep-out rim, facing the world)
  • The home planet — the player's world — in the system you start in: a 1024 px disc rendered 1:1 from the assets/images/planets.png spritesheet, with a proper name drawn from the planet name bank (the first name off the starting system's deck, so it never clashes with one of the other worlds). Which Terran face it shows (frames 0–2) is a seed-deterministic pick, so the same galaxy always yields the same home world. The ship spawns ~150 px (edge-to-edge) off its rim in a seed-derived direction, may fly in as close as 50 px from the rim, and can never cross it — a planet is solid (tuning in data/planets.json)
  • The tether — your range. You start with a level-1 tether anchored on the home world: a circular zone, 5120 px from the planet's center (data/tether.json). The ship cannot leave it — anywhere in the union of its tethers' zones is open space (where zones overlap there is no line and no wall), and the rim is a hard barrier rendered as a thick, glitchy, marching dotted line (neon core + RGB fringe + ambient glitch bursts, contact shudder where the ship bumps it). Click-to-fly and autopilot targets are clamped to the union boundary, so a world outside your range is where the ship parks — on the line — until a tether grows. Upgrades and extra anchors (planets/stations) plug into TetherField.add/setLevel (tuning in data/tether.json)
  • The Research console — the RESEARCH deck button opens a full-screen window: the left pane loops the muted assets/videos/research-computer.mp4 archive feed (scanlines, sweep band, REC pulse, glitch bursts), the right pane holds the category tabs and the branching tech tree for the selected category — it starts at the top and unlocks downward, one branch at a time. Selecting a tech shows its dossier (icon + description + duration) and a RESEARCH button, which appears only when the tech is researchable and nothing else is in progress (one project at a time, 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; 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
  • The Build console — the BUILD deck button (on a planet surface, where the deck's SHOP/BUILD/TAKE OFF live) opens a full-screen window: the left pane loops the muted 2:3 assets/videos/build.mp4 build feed (scanlines, sweep band, REC pulse), the right pane holds the category tabs (Planet / Cargo, data/builds.json) and a list of the category's buildable items — not a tree: builds are one-off installs on the planet. Locked items are grayed out with their missing requirements (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). 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 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 the save bank — and resume with their remaining time on load
  • The Map console — the MAP deck button opens a full-screen cartography window (data/map.json, js/ui/MapWindow.js), the ResearchWindow's bones: the left pane loops the muted 2:3 assets/videos/map.mp4 feed (scanlines, sweep band, REC pulse, glitch bursts). Two tabs on the chart plate:
    • CURRENT SYSTEM — the whole solar system on a padded frame (discovered objects, the tether range as a glitch-dotted union boundary, and a fog-of-war dim over what the tether doesn't reach yet), the system's discovery + resources readout, hover tooltips, and click-to-ENGAGE AUTOPILOT (the same autopilot as a world click); wheel zoom / drag pan (the chart repaints crisp at the new scale)
    • GALAXY — the whole galaxy: a glowing star per system, each colored by its archetype and pulsing on that archetype's heartbeat (binary stars beat fast with a twin core), the jump-lane web as thin lanes — traveled lanes glow brighter with a flow packet drifting the route, frontier lanes (one end charted) brighter still, unexplored lanes faint threads — plus the charted region (the hull of the visited systems, soft fill + subtle outline), HOME/SHIP markers, and a reveal ripple out of home. Hover a star for its readout + its link state to your current system; click a star on a live lane → CONFIRM JUMP (the same gate transport as the world's gates). The run's footprint — visited systems + traveled lanes — is part of the save (old saves load, defaulting to “your system is charted, nothing traveled”). All of it tuned in data/map.json → galaxy
  • Camera gently trails the ship; the parallax starfield streams past while it flies and the view slowly recenters (≈1.5 s) once the ship comes to rest
  • Config-driven setup: every tunable value lives in data/*.json

Project layout

orbit/
├── index.html              # boots the game
├── data/                   # ← ALL tunable config (edit these freely)
│   ├── manifest.json       #   which config files exist
│   ├── game.json           #   dimensions, colors, starfield, …
│   ├── menu.json           #   menu text, colors, button layout, seed panel
│   ├── theme.json          #   shared UI theme: fonts, palette, CRT/glitch tuning
│   ├── ship.json           #   the ship: art, feel (thrust, drag, maxSpeed…), base stats (hull, shields, cargo, minerals)
│   ├── combat.json         #   COMBAT: the range (player feel, enemy craft + AI + lance, rams, respawns, arena) + the main-game demo (game.*, energy, the bar)
│   ├── progression.json    #   COMBAT LEVELS: the XP curve (base × n^power, the cap) + the per-level armor reward
│   ├── planets.json        #   home world + system layout + solid-disc rules
│   ├── map.json            #   the MAP console: tabs, the system chart, the GALAXY tab (stars/pulse/lanes/region), stats, zoom
│   ├── tether.json         #   the tether (your range): level radii, barrier line, glitch, contact
│   ├── galaxy.json         #   galaxy scale & shape (count, 2:1 field, zones, corner home…)
│   ├── systems.json        #   system archetypes: theme, attributes, distribution
│   ├── settlements.json    #   the lived-in layer: settlement kinds & populations
│   ├── gates.json          #   JUMP GATES: network (1–3 gates, local jumps, pure spanning tree — maze, no shortcuts) + placement (tether anchor, facing, radii, gaps)
│   ├── research.json       #   RESEARCH: global rules (time unit, one at a time) + category registry
│   │                       #   trees live one-per-file below (section name = file basename)
│   ├── research/
│   │   ├── exploration.json#   the Exploration tree: tether levels, anchoring, signal amp
│   │   └── mining.json     #   the Mining tree: improved/advanced mining arm, improved storage (blueprints)
│   ├── builds.json         #   BUILDING: the build console's categories/resources + the buildable items
│   ├── actionbar.json      #   the command deck: 6 slots (Research, Scan, Ship, ·, ·, Menu)
│   └── naming.json         #   names: star/galaxy syllable pools + the curated PLANET & STATION name banks
├── assets/images/          # art: planets.png (1024×1024 frames), ships-player.png (256×256 frames), ships-enemy.png (128×128 frames)
├── assets/fonts/           # UI typefaces: Ethnocentric (headers), Centauri (body)
├── lib/                    # vendored third-party libs (Phaser 4.2.1)
├── js/
│   ├── main.js             # entry point: load config → boot Phaser (?combat boots the normal game with the combat demo live)
│   ├── config/             # Config singleton, ConfigLoader, game config
│   ├── scenes/             # MenuScene, GameScene (thin, orchestration)
│   ├── combat/             # COMBAT: CombatSystem (the main-game pack — auto-fire, tracers, rams, armor, energy), the XP ledger (XpManager — levels from kills, data/progression.json), the range (CombatScene, js/combat/main.js boots it), EnemyShip, Bullet, Fx, StarDrift, pure rules in Combat.js
│   ├── entities/           # Ship (own behavior), Planet (home world, solid), Station, JumpGate (solids)
│   ├── galaxy/             # Galaxy (seeded world model), SystemGenerator, SystemReport, JumpNetwork (the gate network)
│   ├── tether/             # Tether (pure range math) + TetherField (constraint + barrier line)
│   ├── research/           # ResearchModel (pure tree rules/layout), ResearchState (unlocks + active run), ResearchIcons
│   ├── build/              # BuildModel (pure build rules), BuildState (installed + in-progress records)
│   ├── ui/                 # MenuButton, GlitchText, CyberShape, ActionBar, CombatBar (the combat-mode deck), DiscoveryCompass (reusable), ResearchWindow, BuildWindow
│   ├── visuals/            # Starfield, CyberOverlay (CRT/glitch, shared)
│   ├── utils/              # small pure helpers (Color, Rng, NameGenerator)
│   └── vendor/             # shim to the vendored Phaser
└── docs/PROJECT_NOTES.md   # ← project conventions: read this

Conventions (short version)

  • Config in JSON. If a value might change, it goes in data/, not code. Add a file = one line in data/manifest.json.
  • One class per file, ES modules, scenes stay thin, entities own their behavior. Details in docs/PROJECT_NOTES.md.
  • Phaser is imported only via js/vendor/phaser.js (one-file version swap).

The player's loop: research + building

Beyond flying, Orbit is built around two progression verbs:

  • Research (data/research.json + data/research/<category>.json) — time-based. A project takes a fixed duration (timeUnit: seconds); the player researches one thing at a time (maxConcurrent: 1). Research is the gate: it unlocks builds and further research (each category tree is a DAG — a node's requires must be unlocked first). The RESEARCH deck button opens the console window (js/ui/ResearchWindow.js): looping archive feed, category tabs, the branching tree, per-tech dossier + RESEARCH button, and a deck progress bar. The rules are pure (js/research/ResearchModel.js, ResearchState.js — Node-testable); the window is a passive view that asks GameScene to start a run. effects on a finished node carry the payload (e.g. { tether: { level: 2 } } → TetherField.setLevel), so new effect kinds plug in without touching the tree data.
  • Building (data/builds.json) — cost-based, paid in minerals (and credits, when they land; resources). A build is a one-off install (category: planet | cargo | mining): planet builds land on the world's surface (targets: ["planet"]); the Mining tab's builds are ship upgrades (targets: ["ship"] — the improved/advanced mining arm set the ship's mining rate to 1.5 / 2.0 minerals per second, the improved storage grows the hold to 350): built from any planet's console, the effect is ship-wide, and it counts as built wherever the ship lands (no re-buy). Each build names its cost, its duration (seconds), the research that unlocks it (requires, mirroring the research tree's unlocks.builds), optional world-state gates (planetRequires, e.g. the tether level on that world), and its effects (e.g. tether: {level: 2} → the world's tether field strengthens; mining: {rate: 2.0} / mining: {capacity: 350} → the ship's stats, MAX wins). The BUILD deck button (on a planet surface) opens the console window (js/ui/BuildWindow.js): looping build feed, category tabs, the build LIST (locked items grayed with their missing gates), per-build dossier + highlighted cost + BUILD button. While a build runs the deck is locked (one at a time) and the state ticks on the game-loop clock — so a build started on the surface keeps running if the player takes off. The rules are pure (js/build/BuildModel.js, BuildState.js — Node-tested in dev/builds.test.mjs); the window is a passive view that asks GameScene.beginBuild() to run the gates, charge the cost, and start the clock, and the scene applies effects on completion (ship upgrades re-derive from the build records on load, GameScene._restoreMiningUpgrades).
  • The command deck (data/actionbar.json, rendered by js/ui/ActionBar.js) — the cyberpunk bar across the bottom of the screen. Six evenly spaced slots: Research, Scan, Ship, ·, ·, Menu in space; on a planet surface the same bar re-deals itself — Shop, Build, Ship, ·, Take Off, Menu (surface.buttons). The slots fire onAction (GameScene / SurfaceScene own the behavior); two slots are reserved. Dressed with the menu's CRT language — scanlines over the strip and the GlitchText RGB pull-apart on labels and the panel outline during bursts. All layout, palette, and motion (boot flicker, rail comet, glitch bursts, RGB split, scanlines) live in the config.

_-prefixed keys (_comment, _template, …) are documentation, not runtime data — loaders and tests ignore them.

Dev tools

node dev/run-all.mjs             # the whole suite (dev/*.test.mjs) — every test runs with plain `node`
node dev/ship-behavior.test.mjs   # runs the real Ship.update() loop in Node
node dev/starfield.test.mjs       # runs the real Starfield.create() in Node
node dev/galaxy.test.mjs          # galaxy determinism, composition, lazy vs eager, gradient + report
node dev/frames.test.mjs          # the galaxy-wide (class, frame) pass: in-pool, spread, determinism
node dev/jumps.test.mjs           # the gate network: 1–3 gates, locality, strong connectivity + placement invariants
node dev/jumpgate.test.mjs        # the JumpGate entity: discovery fields, dormant/active, the solid contract (Node)
node dev/discovery.test.mjs       # discovery rules + compass geometry + chip hit test
node dev/tether.test.mjs          # tether range math: union, clamp, visible arcs (no line in overlaps)
node dev/research-builds.test.mjs # data contract: research/builds/actionbar shapes + manifest
node dev/builds.test.mjs          # the build console's pure rules + state machine (Node)
node dev/world-names.test.mjs    # world-name casing: display casing must never leak into the data
node dev/saves.test.mjs           # the save bank + capture/restore hand-off (incl. builds)
node dev/decode.test.mjs          # the shared decode scramble (menu seed + system dossier)
node dev/system-hud.test.mjs      # real GameScene dossier: layout + staggered decode to final report
node dev/quest-tracker.test.mjs  # the quest tracker HUD: slide in/out, live checklist, priority, SHOW MORE, the popup contract
node dev/perimeter-shimmer.test.mjs # the READY row's dual-shimmer rim: perimeter arc math + the counter-streaming comets
node dev/quests.test.mjs         # the quest ledger: grant/claim + the ONE priority (set/clear/sticks/auto-assign) + save round-trip
node dev/sfx.test.mjs             # the shared SFX voice: guards + sfx.json keys/files
node dev/music.test.mjs           # the shared music voice: guards + music.json contract
node dev/combat.test.mjs          # the combat RULES: armor damage, the enemy's ring AI + lance decision, bullet life, CombatSystem (pack, auto-fire, hits, rams, armor, respawn), ship FEEL (handleFeel), the SFX event→voice map, and the READABILITY rules (pickTarget, feed, off-screen arrows, the shared CombatHud) — real data/combat.json

dev/test-game.html boots straight into the GameScene (no menu click), handy for manual testing of the flight feel. dev/research-shot.html does the same but opens the Research console and starts a run, and dev/cdp-shot.mjs waits for a readiness expression before screenshotting through CDP (any Chromium-CDP browser on 127.0.0.1:9333):

node dev/cdp-shot.mjs "http://127.0.0.1:8081/dev/research-shot.html" shot.png \
  "window.__RESEARCH_SHOT && window.__RESEARCH_SHOT.ready" 90000

dev/build-check.html plays the real Build flow (menu → New Game → home world → build console) and prints a plain-text report of the L1/L2 row states. Every real page (index.html) also carries a read-only console diagnostic: opening the Build console logs a one-line [orbit-diag v…] summary (planet, home, built records, tether level, L1/L2 row states, stale-JS probe), and orbitDiag() in DevTools prints the full report (data contract, save bank, scene wiring, errors since boot). If orbitDiag is undefined, the browser served a cached older js/main.js — run the dev server above, or hard-reload with the cache disabled (DevTools → Network → Disable cache).

Phaser

Phaser 4.2.1 is vendored at lib/phaser.min.js (UMD build, MIT license — see lib/PHASER_LICENSE.md). No internet or npm needed at runtime.