orbit/docs/PROJECT_NOTES.md

270 lines
15 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).
## The current system is a place, not just a dossier (discovery + compass)
The player starts in a solar system, and the other worlds **exist in the
world** — solid, rendered, flyable-to. Rules and seams:
- **World layout** — `SystemGenerator.layoutSystemPlanets(seed, systemId,
planets)` (pure, `js/galaxy/SystemGenerator.js`) places each generated
world in an annular band around the home world (origin), enforcing
edge-to-edge separation from every other disc. Draws come from the
dedicated fork `Rng.derive(seed, 'system', id, 'layout')` — layout
results are seed-deterministic *and* don't perturb the content stream
(lazy === eager is preserved). Band params live in
`data/planets.json → solarSystem` (enabled, minOrbit, maxOrbit,
minEdgeGap); class sizes/tints in `classScale`/`classTint`.
- **Discovery** — `js/galaxy/Discovery.js` (pure, no Phaser — Node-
testable, save-ready: `toJSON()`/`fromJSON()`). Rule: the ship within
`game.discovery.distance` (data/game.json, default 540 px) of an
object's *edge* (center distance ≤ radius + distance) discovers it,
once, per system. Home world is discovered at spawn (the ship starts
beside it). Feedback: rim ping + "DISCOVERED — NAME · TYPE" toast
(GameScene.celebrateDiscovery).
- **Compass** — `js/ui/DiscoveryCompass.js` (screen-space Container,
scrollFactor 0). Every frame it refreshes the set of **discovered**
objects that are **off-screen**, drawing a themed chevron arrow on the
screen edge with a type/name chip, separated by angle when rays crowd.
Edge-anchor geometry (`edgeAnchor`, `circleInView`, `lerpAngle`) is
exported pure for tests. Arrow texture is generated procedurally on
first use (`__compass_arrow`).
- **World solids** — the ship collides with *every* system planet
(`GameScene.solidPlanets`), not just home: you can approach a rim,
never pass through.
- Verified: `dev/discovery.test.mjs` (rule, boundaries, per-system
state, JSON round-trip, layout bounds + determinism, compass geometry).
## 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).
- An `Image` created **before** its texture key exists can stay bound to
the `__MISSING` texture forever — even after the key is generated later
in the same session. Generate the texture **first** (see
`DiscoveryCompass.ensureArrowTexture`: texture, then `scene.add.image`).
- **Input on screen-fixed containers is per-child.** Hit-testing uses
each object's *own* scrollFactor (`InputManager`: `g = worldX +
scrollX*sf - scrollX`), while a child of a `scrollFactor(0)` container
renders screen-fixed. So a child whose scrollFactor is left at the
default (1) is drawn pinned but hit-tested in world space — clicks miss
it whenever the camera has scrolled. (MenuButton never bit by this:
the menu camera doesn't move.) Set `scrollFactor(0)` on **every** child
of a screen-fixed UI container — done in `ActionBar.buildSlots`.
- 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`)
- [x] The system as a place: generated worlds laid out in the world
(deterministic band), solid to the ship; discovery (within 540 px
of an edge, once, per system) with rim ping + toast; off-screen
compass — themed screen-edge arrows with type/name chips pointing
at discovered worlds; pure, save-ready discovery state
(`js/galaxy/Discovery.js`)
- [ ] 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)
- [x] The player's loop, laid down as data + seams: research (time-based,
one at a time, gates builds/research) and building (credits + minerals,
ship/planet/station) data layers (`data/research.json`,
`data/builds.json`) with templates, and the command deck
(`js/ui/ActionBar.js` + `data/actionbar.json`) — Research, Build,
Ship, ·, ·, Menu. The deck reuses the menu's CRT language: the same
scanline tile recipe as `CyberOverlay` (clipped to the bar) and the
`GlitchText` RGB pull-apart (icons + labels at all times, panel outline
during bursts) — see `actionbar.animation.rgb` / `actionbar.scanline`
- [ ] Research rules + panel: start a project (one at a time), tick its
duration, award `unlocks`; the Research slot on the deck opens it
- [ ] Build panel: pay credits/minerals, apply `effects`, respect
`requires`; the Build slot on the deck opens it; a credits/minerals
readout in the HUD
- [ ] Ship screen (the Ship slot) — inspect & upgrade the ship
(ship-category builds) from one place
- [ ] 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)