orbit/docs/PROJECT_NOTES.md

202 lines
11 KiB
Markdown

# Orbit — Project Notes
Working agreements and conventions for building this game. Read this before
adding new systems — these are the rules that keep the project scalable.
## Commits (important)
- **Brian makes the commits, manually.** After finishing a change, leave the
work staged/unstaged in the working tree with a suggested commit message
(in the chat reply, or a note here), and do NOT run `git commit` yourself.
This gives Brian the chance to review the code before it enters history.
- Amends/reverts are fine if asked explicitly.
## What we're building
A procedurally generated space RPG in the spirit of **Privateer**, but in
**top-down 2D**: stars/sectors you can jump between, ships to fly, crew,
trading/economy, stations, quests — to be scoped as we go.
**Hard technical constraints:**
- Plain **ES6 modules**, no transpilation, no build step.
- **No package managers required to run** — the game must work from any
static HTTP server (the browser fetches everything).
- Third-party libraries are **vendored** into `lib/` (right now:
`lib/phaser.min.js`, Phaser 4.2.1 UMD build, see `lib/PHASER_LICENSE.md`).
- Must be served over http(s), not `file://` (ES modules + `fetch` of JSON).
## Config lives in JSON (important)
- Tunable data lives in `data/*.json`: dimensions, colors, text, physics,
balance, spawn tables, and anything a non-programmer might want to tweak.
- `data/manifest.json` lists which files to load. **Add a config file =
drop it in `data/` + one line in the manifest.** It is then available as
a section named after the file (`ship.json` → `config.get('ship.thrust')`).
- Code reads config through the `config` singleton (`js/config/Config.js`):
```js
import { config } from '../config/Config.js';
const thrust = config.get('ship.thrust', 900);
const menu = config.section('menu', {});
```
Always supply a sensible fallback so code never depends on a missing key.
- **Rule of thumb:** if a value might ever change (balance, layout, copy,
colors), it belongs in JSON, not code. Code owns *behavior*, JSON owns
*parameters*.
- Split config by concern as the game grows: world generation already has
`data/galaxy.json` (shape/scale), `data/systems.json` (archetypes), and
`data/naming.json` (name pools); next up might be `data/economy.json`,
`data/ships.json`, `data/crew.json`, etc. One file per system beats one
giant file.
## Code is modular & class-based (important)
- One class per file, ES module exports, no globals (except the deliberate
singletons: `config`).
- Layering:
- `js/scenes/` — Phaser scenes: state + orchestration only (thin classes)
- `js/entities/` — in-world objects that own their behavior (Ship, NPC, …)
- `js/ui/` — reusable UI components (MenuButton, panels, HUD)
- `js/visuals/` — decorative, non-interactive visuals (Starfield, …)
- `js/galaxy/` — the world model: seeded Galaxy, system archetypes,
lazy content generation (Galaxy, SystemGenerator)
- `js/utils/` — small pure helpers (Color, Rng, NameGenerator)
- `js/config/` — config loading + Phaser game config
- `js/vendor/` — shims to pinned third-party libraries
- Scenes stay thin: they compose entities/UI and wire input. They don't hold
balance numbers or game rules.
- Every class should be standalone-constructible: it takes what it needs
(`scene`, config values) in its constructor rather than reaching for
globals — that keeps things testable and reusable.
- Entities drive themselves from their own `update(time, delta)` method,
called by the owning scene (e.g. `GameScene.update`). If an entity ever
needs the engine's auto-update instead, define `preUpdate` (the Phaser v4
hook — v4 does not auto-call `update`).
- Import Phaser only through `js/vendor/phaser.js` — the single place that
changes if we swap framework versions.
## World model — the galaxy (important)
The galaxy is **seeded and two-level**. The seed is chosen on the main
menu (displayed, editable, rerollable; same seed ⇒ same galaxy).
1. **Roster**`Galaxy.create(seed)` builds every system's *identity*
(id, name, type, x, y) up front. 40,000 systems is ~70 ms, so the whole
galaxy is always known: the player can never "discover" a layout that
wasn't already implied by the seed.
2. **Contents** — planets/moons/belts/**settlements**/hazards are
generated **lazily** on first arrival (`galaxy.ensureContent(id)`),
then cached. Each system's draw stream is `Rng.derive(seed, 'system', id)`
— independent of generation order — so lazy and eager
(`galaxy.generateAll()`) results are identical. Pick whichever is
cheaper at runtime; correctness never depends on it.
**Determinism rules (keep them!):**
- All generation draws go through `Rng` (`js/utils/Rng.js`). Never
`Math.random()` in a generation path.
- Anything that must be order-independent (names, contents) draws from a
`Rng.derive(seed, <purpose>, id)` stream — never from the galaxy-level
sequence. The roster loop is allowed to use its own sequence because
the loop order itself is part of the seed contract.
- Seeds are trimmed; seed → hash → stream is one-way.
- Dev tools that assert determinism: `dev/galaxy.test.mjs`.
**System archetypes** live in `data/systems.json`. Each type is:
- **themable** — `theme.color` etc. for UI;
- **attribute-driven** — `attributes` steer the SystemGenerator (star
classes, binary chance, planet count spread, planet class weights,
moon/belt chances, habitability, hazard). New attribute key = JSON +
a few lines in `SystemGenerator.js`;
- **distributed** — `distribution.weight` (how common) and
`distribution.radiusBand` (first proximity rule: e.g. `void` systems
live in the outer rim). Richer galaxy-level rules (clustering,
faction borders, adjacency affinity) will slot into
`data/galaxy.json``distribution.rules[]`, read in
`Galaxy._generate()` — the hook is marked in code.
The player's **current system** starts at `galaxy.currentSystem()`
(`startingSystem.policy`: `center` or `random`). Jumping between systems
(the eventual star map / jump drives) will use `galaxy.neighborsOf(id)`
and the spatial hash already built for it.
**The galaxy is already lived in.** It was settled long before the
player arrives. Every system's content can include **settlements**:
`colony` (on a habitable world), `miningStation` (over a resource
world), `cloudBase` (riding a gas giant), `deepSpaceStation` (adrift in
open space), `waypoint` (a small beacon — the faint trace of a crossed
galaxy). Not everything is inhabited: many systems report "charted ·
unclaimed". Model & seams:
- **Kinds vocabulary** — `data/settlements.json` (label, description,
theme color, population range, anchor type). Add a kind = JSON + naming
pool; the generator picks it up by name.
- **Per-type rates** — `types.<id>.attributes.settlements` in
`data/systems.json`: `chance` + optional `needs` (planet classes) per
kind. Same attribute-driven pattern as everything else.
- **Core→rim gradient** — `galaxy.settlements.gradient` in
`data/galaxy.json`: the settled heart is denser (factor 1.0), the rim
is thinner (clamped to `floor`). Each record carries `rNorm` (0 =
center, 1 = rim) so density is per-system, not global.
- **Reserved seam: `owner`** — every settlement has `owner: null`. That's
where **factions and pirates** will plug in later (claim, flag,
relations). Deliberately absent for now — no factions yet.
- **Pure report formatter** — `js/galaxy/SystemReport.js`
(`formatSystemReport(content)` → title/subtitle/settlements/summary).
The GameScene HUD renders it; future star map / terminal UI reuse it.
- Landing/exploration (a future feature) will treat settlements as points
of interest: the data already says what's there and where (anchor =
planet ordinal or open space).
## Phaser version
- Pinned: **Phaser 4.2.1** ("Giedi"), vendored in `lib/phaser.min.js`.
- App code is v4-specific where v4 changed things (e.g.
`Phaser.Math.Angle.RotateTo`, `banner: false`, `Clamp(value, min, max)`).
- **v4 quirks that bite (verified against this build, Sept 2026):**
- The engine calls a scene's `update(time, delta)` directly but does **not**
step the scene's `TimeClock` or `TweenManager` (the v3 scene-events
PRE_UPDATE/UPDATE plumbing is not fired in the v4 loop). Each scene must
call `this.time.update(time, delta)` and `this.tweens.update()` at the top
of `update()`, or `delayedCall`/`addEvent`/tweens silently never run.
(Done in `MenuScene.update` / `GameScene.update`.)
- A GameObject constructed with `new` (e.g. our `Container` subclasses in
`js/ui/`) is **not** added to the scene display list — the `scene.add.*`
factories do that. Call `scene.add.existing(this)` in the constructor
(Ship/Planet already do; GlitchText/MenuButton now do too).
- Text colors go straight to the canvas: v4 writes `fillStyle =
style.color`, so a **numeric** color is an invalid fillStyle and the text
silently renders **black**. Text styles and `setColor()` must get CSS
strings — use `toCss()` from `js/utils/Color.js` (keep `toColor()` for
Graphics/shape APIs).
- To upgrade: replace the vendored file + note the version here (and re-check
the quirks above — they may go away).
## Roadmap (working list, intentionally rough)
- [x] v0.1 foundation — menu → New Game → click-to-fly ship
- [x] Galaxy seed on the main menu (displayed, editable, rerollable;
same seed → same galaxy, shown before you commit)
- [x] Two-level worldgen: seeded galaxy roster (40k systems) + lazy,
order-independent system contents; system archetypes in JSON
(theme + attributes + distribution weight/radius band)
- [x] The lived-in layer: settlements (colonies, mining stations, cloud
bases, deep-space stations, beacons) with per-type rates, a
core→rim density gradient, populations, and a `owner` seam reserved
for the factions/pirates to come; readable as a HUD dossier
(`SystemReport`)
- [ ] Factions & pirates: claim settlements (`owner`), flags, borders,
and the player's place in a populated galaxy
- [ ] Landing & exploration: settlements become points of interest you
can approach (the data — kind, anchor, population — is already there)
- [ ] Richer galaxy distribution rules (`galaxy.distribution.rules[]`:
clustering by type, borders, adjacency affinity) — hook marked in
`Galaxy._generate()`
- [ ] World model in play: the ship still flies unbounded open space;
wire in current-system boundaries, jumps between systems (use
`galaxy.neighborsOf`), and a star map scene
- [ ] Ship input beyond click-to-fly (throttle/brake keys, manual rotation)
- [ ] HUD (speed, fuel/crew) — the current system's dossier (name,
identity, settlements) is already shown top-left
- [ ] Save/load (the `config` + entity split should make this tractable;
a save = seed + player state, since the galaxy regenerates)
- [ ] Economy/trading loop (the Privateer heart)