246 lines
14 KiB
Markdown
246 lines
14 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`).
|
|
- 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)
|
|
- [ ] 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)
|