orbit/README.md

273 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, …):
```sh
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 `fetch`es its JSON config.
## 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**: 40,000 star systems in a seeded disk + core +
spiral arms (`data/galaxy.json`), typed into six themed archetypes
(`data/systems.json`) with per-type distribution weights and radial
bands — the first "how does the galaxy lay itself out" rules.
- **Two-level generation**: the whole galaxy roster is generated at New
Game (~70 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.
Systems host colonies on habitable worlds, mining stations over resource
worlds, cloud bases riding gas giants, stations adrift in open space, and
beacons — or report *charted · unclaimed* when nobody's there. Rates are
per-archetype (`data/systems.json`) and thin out from the settled core to
the wilder rim (`data/galaxy.json`). 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 (system boundaries/jumps come 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 02) 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/` (the first is **Exploration**: Tether Level 14,
Tether Anchoring, Signal Amplification). 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`). The
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
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
- 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)
│ ├── planets.json # home world + system layout + solid-disc rules
│ ├── tether.json # the tether (your range): level radii, barrier line, glitch, contact
│ ├── galaxy.json # galaxy scale & shape (count, radius, spiral…)
│ ├── systems.json # system archetypes: theme, attributes, distribution
│ ├── settlements.json # the lived-in layer: settlement kinds & populations
│ ├── 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
│ ├── 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)
├── 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
│ ├── config/ # Config singleton, ConfigLoader, game config
│ ├── scenes/ # MenuScene, GameScene (thin, orchestration)
│ ├── entities/ # Ship (own behavior), Planet (home world, solid)
│ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator, SystemReport
│ ├── 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, 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`](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 on a planet** (`category: planet | cargo`): it 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). 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.
- **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
```sh
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, distribution, lazy vs eager
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/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
```
`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`):
```sh
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.