# 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, , 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..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). ## The tether — the player's range (important) The ship starts with **one level-1 tether** anchored on the home world (system origin): a circular zone, **5120 px from the anchor's center** (`data/tether.json → level1Radius`). That rim is the player's boundary: - **The union is the space.** The ship may be anywhere inside ANY of its tethers' zones. Where two zones overlap there is **no wall and no line** — the boundary of the union is where the barrier lives, and the line is drawn only on each tether's *visible* arcs (`Tether.visibleArcs` — the rim arcs not covered by another zone). - **Outside the union is a hard barrier.** The ship is clamped to the closest boundary point with the outward normal; outward velocity AND acceleration are stripped (same static-resolve pattern as `Planet.constrainShip`, run after the planets in `GameScene.update`). Click-to-fly and autopilot targets are clamped too, so an out-of-range destination becomes "fly to the rim and rest on the line". - **Feedback on contact:** the line shudders locally at the hit point (TetherField pulse + radial zap), a small camera kick, and a throttled magenta console toast (`tether.contact` in the config). **Layering (same rules as everything else):** - `js/tether/Tether.js` — PURE (no Phaser): the record (id/anchor/level/ radius), `radiusForLevel`, union membership, `clampPoint` (nearest union boundary point + normal), and `visibleArcs` (circle-arc interval math on the unit circle). Node-tested by `dev/tether.test.mjs` — including the two-direction boundary property (nothing visible is inside another zone; nothing exposed is missing from the visible arcs). - `js/tether/TetherField.js` — the scene-facing field: owns the tether list, exposes the seam the build system will use (`add(id, x, y, level)`, `remove(id)`, `setLevel(id, level)`, `onChange` → HUD refresh), applies the constraint (`constrainShip`), and renders the barrier: dash dots of constant world size along the visible arcs, on-screen culling only, marching offset, additive glow + RGB ghost fringe under the main pass, ambient glitch bursts (dash flicker, radial displacement, data drops, sparks), contact pulses. All tuning in `data/tether.json → line/contact`. - `data/tether.json` — level radii (`level1Radius`, `radiusGrowth`, `maxLevel`), the starting tether (`homeId`/`homeLevel`), and every visual/feel parameter. **Upgrade economics (planned):** radius(level) = 5120 × 1.25^(level−1) — level 4 = 10000 px, which reaches the far system orbit (maxOrbit 8800) plus rim clearance. Extra tethers anchored on planets/stations will be the "expand your reach" verb; the seams are in place. **Phaser v4 note:** the barrier is two `Graphics` layers redrawn per frame (`clear()` → one `strokePath` per pass) — no textures, no physics bodies, no camera math beyond culling. The scene drives `tick()`/`draw()` from `update()` alongside the TimeClock/tween stepping. ## 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`. - **Ship art** — the starter ship is now a spritesheet frame: `data/ship.json → texture` (assets/images/ships-player.png, 256×256 frames, `frame` 0), mirroring the planets pattern. The frame art faces `artFacing` ("north" — the nose is the frame's top edge; the glass cockpit section is aft, per the art's author); `Ship` applies a constant render offset (`artOffset`) so heading math is identical and the ship rotates about the frame center. Sprite scale = (size×scale)/frameWidth keeps the 46 px world size (and 23 px collision radius) regardless of frame size. Missing/failed sheet → built-in procedural dart (facing east), with a console note. - 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 - [x] Compass autopilot: clicking a name tag sends the ship to that world — it targets the keep-out rim on the side the ship is approaching from (arrival facing the world; docking seam next). The scene's click-to-fly guards against deck AND chip clicks (`ActionBar.contains` / `DiscoveryCompass.contains`) (`js/galaxy/Discovery.js`) - [x] The tether — the player's range: a level-1 tether (5120 px) anchored on the home world; the ship lives in the UNION of its tethers' zones (overlap = no line, no wall), the rim is a hard barrier rendered as a thick glitchy dotted line; click/autopilot targets clamp to the boundary; contact feedback (line shudder, camera kick, toast). Pure range math in `js/tether/Tether.js` (Node-tested), visuals + constraint seam in `js/tether/TetherField.js`, tuning in `data/tether.json` - [ ] Tether progression: the build/research verbs that anchor tethers on planets/stations and upgrade levels (the `add`/`setLevel`/`onChange` seams are in place; the costs + panel + research gate come next) - [ ] 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)