9.8 KiB
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 commityourself. 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, seelib/PHASER_LICENSE.md). - Must be served over http(s), not
file://(ES modules +fetchof 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.jsonlists which files to load. Add a config file = drop it indata/+ 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
configsingleton (js/config/Config.js):
Always supply a sensible fallback so code never depends on a missing key.import { config } from '../config/Config.js'; const thrust = config.get('ship.thrust', 900); const menu = config.section('menu', {}); - 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), anddata/naming.json(name pools); next up might bedata/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 configjs/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, definepreUpdate(the Phaser v4 hook — v4 does not auto-callupdate). - 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).
- 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. - Contents — planets/moons/belts/settlements/hazards are
generated lazily on first arrival (
galaxy.ensureContent(id)), then cached. Each system's draw stream isRng.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). NeverMath.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.coloretc. for UI; - attribute-driven —
attributessteer the SystemGenerator (star classes, binary chance, planet count spread, planet class weights, moon/belt chances, habitability, hazard). New attribute key = JSON + a few lines inSystemGenerator.js; - distributed —
distribution.weight(how common) anddistribution.radiusBand(first proximity rule: e.g.voidsystems live in the outer rim). Richer galaxy-level rules (clustering, faction borders, adjacency affinity) will slot intodata/galaxy.json→distribution.rules[], read inGalaxy._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.settlementsindata/systems.json:chance+ optionalneeds(planet classes) per kind. Same attribute-driven pattern as everything else. - Core→rim gradient —
galaxy.settlements.gradientindata/galaxy.json: the settled heart is denser (factor 1.0), the rim is thinner (clamped tofloor). Each record carriesrNorm(0 = center, 1 = rim) so density is per-system, not global. - Reserved seam:
owner— every settlement hasowner: 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)). - To upgrade: replace the vendored file + note the version here.
Roadmap (working list, intentionally rough)
- v0.1 foundation — menu → New Game → click-to-fly ship
- Galaxy seed on the main menu (displayed, editable, rerollable; same seed → same galaxy, shown before you commit)
- Two-level worldgen: seeded galaxy roster (40k systems) + lazy, order-independent system contents; system archetypes in JSON (theme + attributes + distribution weight/radius band)
- 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
ownerseam 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 inGalaxy._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)