536 lines
36 KiB
Markdown
536 lines
36 KiB
Markdown
# Orbit
|
||
|
||
> A procedurally generated space RPG in the spirit of *Privateer*, in
|
||
> top-down 2D. Built with **Phaser 4** as plain **ES6 modules** — no build
|
||
> step, no package managers required to run.
|
||
|
||
## Run it
|
||
|
||
Any static file server works (Python, Node, Caddy, nginx, …):
|
||
|
||
```sh
|
||
cd orbit
|
||
node dev/server.mjs 8080
|
||
# → http://localhost:8080
|
||
```
|
||
|
||
> **Why not `python3 -m http.server`?** It sends `Last-Modified` with no
|
||
> `Cache-Control`, so browsers apply *heuristic* caching to the ES
|
||
> modules. While iterating, the browser then keeps running an **old**
|
||
> `js/*.js` module graph (e.g. a `GameScene.js` from before a feature
|
||
> landed) long after the files on disk changed — while the JSON data is
|
||
> fresh. That mix (new data + old JS) is the classic "it still behaves
|
||
> like the old version" bug, and clearing the JSON cache or hard-resetting
|
||
> the game does not fix it. The dev server above sends
|
||
> `Cache-Control: no-store` on every response, so a normal reload always
|
||
> runs what's on disk.
|
||
|
||
> Must be served over **http(s)** — opening `index.html` via `file://` won't
|
||
> work, because the game uses ES modules and `fetch`es its JSON config.
|
||
|
||
**Combat** (tuned before it ships in the real game) has two fronts, and
|
||
every combat number — thrust, speeds, hulls, shields, the lance, rams,
|
||
respawns, the bar — lives in **`data/combat.json`**:
|
||
|
||
- **The training range** (a standalone arena — where the feel is tuned):
|
||
the dev server maps **`/combat`** → `combat.html` (any server can too).
|
||
One fixed arena: you (WASD/arrows + SPACE) against a pack of the enemy
|
||
craft (`assets/images/ships-enemy.png`). `R` resets the range, `P`
|
||
freezes it (positions hold while you poke
|
||
`window.__COMBAT__ = { reset, spawnEnemy, player, kills }` in DevTools).
|
||
- **In the main game** (the integration stand-in — what players will
|
||
eventually feel): **`index.html?combat`** boots the *normal* game, and
|
||
when the player starts, a pack of hostiles spawns around the ship.
|
||
The mouse steers; while a hostile is in range the ship **auto-fires**
|
||
at the nearest one (the cannon tracks — `js/combat/CombatSystem.js`).
|
||
While combat is live the action deck **swaps for the combat bar**
|
||
(`js/ui/CombatBar.js`) — Diablo 4 style: vertical shields + hull on
|
||
the left, the ENERGY gauge on the right, eight skill seats wearing
|
||
keys **1–8** in the middle (the skills land later — for now they're
|
||
empty seats; a press lands on the system's `cast()` stub). Wipe the
|
||
pack and the deck comes back. Kills also earn **combat XP** (the
|
||
run's level ledger — `js/combat/XpManager.js`; the curve is
|
||
`data/progression.json`, each type's value is its own `xp` in
|
||
`data/combat.json`) — a level-up bumps the ship's armor (the
|
||
placeholder toughness), fires a burst + a distinct voice, and lands
|
||
a line in the kill feed. Poke the live fight in DevTools via
|
||
`window.__ORBIT_COMBAT__ = { system, bar, player, kills, spawn,
|
||
damagePlayer, cast, xp, grantXp }`.
|
||
- **Neutral encounter** — **`index.html?neutral`** boots that same normal
|
||
game with a NEUTRAL-ONLY encounter live instead of hostiles: two
|
||
pilots stream in on a short escort window — the neutral hum replaces
|
||
the combat music the moment one is on screen, the pilot's SCAN comm
|
||
lands as it flies in, and the SCAN-COMPLETE comm rides the break-off
|
||
(the dev stand-in for the neutral encounter beat — safe to run even at
|
||
home, where hostiles never go).
|
||
|
||
- **Feel** — the handling is what the range tunes. Every feel number
|
||
(thrust, top speed, drag, turn rate + turn response, throttle ramp/
|
||
decay, brake) lives in `data/combat.json` under `combat.player`, and
|
||
both fronts run the same pure `handleFeel` integration
|
||
(`js/combat/Combat.js`) — so what you feel in the range is what the
|
||
main game ships. Thrust ramps in and decays out, turns ease on, and
|
||
the brake kills the velocity instantly.
|
||
- **Feedback — VFX** — every combat event has a distinct *visual* cue,
|
||
all procedural (`js/combat/Fx.js` — canvas dots / additive glows, no
|
||
new art): a tracer + muzzle flash on **firing**, a spark burst +
|
||
damage number on **impact**, a cyan flash + damage number on a
|
||
**shield hit**, an orange flash + damage number on a **hull hit**, a
|
||
big burst + double ring on an **enemy kill**, a DOWN burst when the
|
||
**player goes down**, and a REFORMED bloom on **respawn**. Shield
|
||
reads cyan and hull reads orange in both the flash and the number, so
|
||
a glance tells you which layer took the hit.
|
||
- **Feedback — SFX** — the same ten events each get a *distinct* voice,
|
||
through one shared event→voice map + playback path
|
||
(`js/combat/CombatSfx.js`) so the two fronts can't drift: the player's
|
||
cannon = `mass_cannon` (assets/fx/combat-mass-cannon.mp3), a hostile
|
||
taking the player's shot = `mass_cannon_hit`
|
||
(assets/fx/combat-mass-cannon-hit.mp3), the enemy's laser = `laser`
|
||
(assets/fx/combat-laser.mp3), impact = `ui_click`, shield =
|
||
`solar_buzz`, hull = `mining_split`, kill = `thunder_01..04`
|
||
(one at random), down = `deconstruct`, respawn = `discovery`,
|
||
level-up = `solar_flare_01`.
|
||
- **Readability layer** — a shared HUD (`js/combat/CombatHud.js`, pure
|
||
rules in `js/combat/Combat.js`) makes the fight *readable* in both
|
||
fronts: a rotating **lock reticle** on the current target (the same
|
||
target the cannon auto-fires — one rule, one target), pulsing
|
||
**threat markers** over every live hostile, **off-screen arrows** at
|
||
hostiles outside the camera (the main game's camera follows the ship),
|
||
a **kill feed** (newest-first, top-right), and a **gauge flash** — the
|
||
combat bar's shields/hull gauge lights the moment it takes the hit.
|
||
- **Combat levels (XP)** — kills grant the hostile's type's own `xp`
|
||
(the roster sets XP-per-threat: the base craft 10, the bomber 25 —
|
||
`data/combat.json → enemy.xp`), banked against a curve that's easy at
|
||
first and hard later, never a grind (`data/progression.json` —
|
||
`base × n^power`, shipped 50 × n², capped at level 80; five base kills
|
||
buy level 1, the whole run to the cap is 8.7M XP). A crossed level
|
||
fires a `levelup` EVENT (one per level — the armor bump
|
||
(`progression.reward`: +3 hull / +2 shields, full heal), the burst +
|
||
voice, the feed's line all ride it; the skill layer — active and
|
||
passive, the post-cap progression — will subscribe to that same event
|
||
later). At the cap the level holds and the overflow is harmlessly
|
||
banked. The ledger is part of the save as `{ level, xp }` (old saves
|
||
load — a fresh run starts at level 0), and the bar's XP row (below
|
||
the orbs — the gold summary strip) shows the level, the run toward
|
||
the next, and the numbers (banked over the next cost; at the cap the
|
||
banked total). `node dev/xp.test.mjs` covers
|
||
the curve, the grants, the cap, the events, and the save round-trip.
|
||
- **Dev hooks + checkers** — besides the two pokes above, every SFX call
|
||
is logged to `window.__COMBAT_SFX__ = { log, last }` (capped), so the
|
||
wiring is verifiable headless in both fronts. CDP checkers drive the
|
||
real game and read state back: `node dev/combat-flycheck.mjs`
|
||
(flight feel — t60/t30, spin, stop distance, top speed),
|
||
`node dev/combat.sfxcheck.mjs` (all the SFX events fire through the
|
||
shared path, in both the range and the main game), and
|
||
`node dev/combat-shot.mjs [outDir]` (captures a labeled feedback frame
|
||
per context through the production paths into `dev/shots/` — the
|
||
committed `combat-range.png` + `combat-main.png` are that evidence).
|
||
|
||
## The class layer — who you are, picked at level 5
|
||
|
||
Cross **combat level 5** and, once the shooting stops, the world **freezes** and
|
||
the **class console** opens full-screen (`js/ui/ClassWindow.js`): Trestle's muted
|
||
feed loops on the left (`assets/videos/classes.mp4`), the class tabs ride the
|
||
right in their own colours (`assets/images/classes.png`, 128x128 frames — frame
|
||
0 Vanguard, frame 1 Wraith). Its first state plays
|
||
`assets/speech/trestle/classes.mp3` over his briefing copy; every tab then shows
|
||
that class's doctrine and a **PICK THIS CLASS** action that asks once more before
|
||
it locks.
|
||
|
||
- **Vanguard** (red) — **DUAL CANNON**: the volley leaves two barrels, each bolt
|
||
carrying the weapon's full damage. Double the metal per beat, same rate of fire.
|
||
- **Wraith** (purple) — **ADVANCED TARGETING COMPUTER**: predictive aim. The
|
||
cannon fires where the hostile *will* be when the bolt lands (a real intercept
|
||
solve in `js/combat/Aiming.js`), capped so an unreachable lead can't whip the
|
||
nose around — no solution, and it aims at where the ship is now.
|
||
- Both bank **+5 skill points** into the run (`js/player/ClassState.js`, saved with
|
||
the run). The ship console's plate then shows the crest, the class name in its
|
||
accent colour and the banked points; the Skill Tree tab lights up as soon as the
|
||
filed class owns a tree (below) — otherwise it stays an offline socket.
|
||
- **The rules**: asked once (the prompt is spent on lock-in, never nagged), only
|
||
when combat is quiet and nothing else owns the screen. Closing without picking
|
||
DEFERS — `CHOOSE YOUR CLASS` in the ship console is the way back. While the
|
||
console is up the world is parked through `GameScene`'s existing `_frozen`
|
||
contract (bodies damped, AI/combat/missions skipped), and Trestle's voice ducks
|
||
the other audio beds, restoring them on the way out whatever you chose.
|
||
- **Try it**: **`index.html?level5`** (aliases `?cls`, `?class`) starts a fresh run
|
||
and walks straight into the console — no grinding required. Add `?tab=wraith`
|
||
to open on a specific class; `?noclass` closes both dev doors for an honest run.
|
||
Evidence: `dev/classes.test.mjs`, `dev/class-combat.test.mjs`,
|
||
`dev/classes-browser.test.mjs`, and the screenshot pass `dev/class-shot.mjs`.
|
||
- Everything is data — roster, colours, icon frames, all of Trestle's copy, the
|
||
confirm wording and each perk's mechanics numbers live in **`data/classes.json`**.
|
||
See *The class layer* in `docs/PROJECT_NOTES.md`.
|
||
|
||
## The skill layer — spend the points in the SKILL TREE
|
||
|
||
The bank the class layer fills has somewhere to go. Open the **ship console**
|
||
and press **SKILL TREE** for the full-screen console (`js/ui/SkillWindow.js`) — a
|
||
cabinet in three regions: the **class plate** upper-left (crest, feed tag, the points
|
||
still unspent), the **tree** upper-right — square icon tiles on the grid the data
|
||
authored, joined by elbow lines and badged **`n/m`** on the corner — and a **detail
|
||
drawer** across the bottom third with that skill's copy and its actions lower-right.
|
||
|
||
- **One point per press.** ADD A SKILL POINT spends a single banked point
|
||
(`0/5` → `5/5`) — for the selected skill, whether you press the button or click
|
||
its tile again; points are never lost and a rank 0 skill is inert. The ledger
|
||
(`js/skills/SkillState.js`) travels with the run in the save; an old save simply
|
||
starts with an empty ledger.
|
||
- **A session is undoable, a closed console is not.** Opening the console opens a
|
||
SESSION: every point spent inside it stays PENDING (the plate says `+n THIS SESSION`),
|
||
and **RESET** hands them back — down a branch this session bought, never past a point
|
||
committed earlier (a reset that would strand one says so instead of de-powering the
|
||
ship). Closing the console — ✕, ESC or a click on the scrim — COMMITS what is left (and
|
||
so does leaving the run: an open session never survives a save/load). Handing a skill to
|
||
a seat steps the console aside but does NOT commit — the undo stays live while a seat is
|
||
being picked.
|
||
- **The tree scrolls.** Wheel, drag or the thumb; this Phaser build has no clip mask,
|
||
so a tile is drawn only while it fits the pane WHOLE and the join lines are clipped by
|
||
hand — the same rule the shipyard's buy list follows.
|
||
- **ACTIVE skills take a seat.** ASSIGN SEAT puts the skill "in the hand" — the
|
||
console steps aside, the combat bar breathes, and a click on a seat (or the key
|
||
**1**–**8**) binds it. Casting spends its energy and starts its own recharge;
|
||
right-click a seat to clear it. Seats are addresses, not owners: rebinds are free
|
||
and a cooldown follows the skill.
|
||
- **PASSIVE skills never sit in the bar.** Ranked ≥ 1 means always on — Vanguard's
|
||
Reinforced Plating grows the hull pool from the moment its first point lands, and
|
||
growth is never a free repair.
|
||
- **The two Vanguard skills**: **Ramming Charge** (OFFENSE — dash past the nearest
|
||
hostile and arrive with a blast; the pack is flown *through*, so one charge is
|
||
one settlement) and **Reinforced Plating** (PASSIVE — more hull to spend).
|
||
- **Try it**: **`index.html?skill`** files a tree-owning class and banks exactly
|
||
what that tree can absorb; `?skill=wraith` names the class, `?skill=max`
|
||
(`?skill=all`) ranks the whole tree, and `?skilltree` walks into the console on
|
||
boot. `?noclass` closes these doors too. No flag, no effect — normal runs are
|
||
untouched.
|
||
- Everything is data — tree membership, ids, labels, types, energy, recharge, max
|
||
points, `requires` edges, effect numbers, icon frames, tooltips, toasts, the
|
||
**authored grid** each skill sits on (`node: {col, row}`), the console's whole
|
||
geometry/palette block (`window.tree`: region split, tile size, join colours,
|
||
rank badge, empty sockets, scroller) and the bar's whole look live in
|
||
**`data/skills.json`**. Code owns behaviour only: adding
|
||
skill #3 is a data row (plus one branch in `js/skills/SkillEffects.js` if it needs
|
||
a new kind of effect). See *The skill layer* in `docs/PROJECT_NOTES.md`.
|
||
|
||
## Current state — v0.2: a seedable galaxy
|
||
|
||
- Main menu with **Continue** (resumes the newest save — grayed out while
|
||
the save bank is empty), **New Game**, **Load Game** (the full slot
|
||
bank), and a **Galaxy Seed** panel: the seed is displayed, editable
|
||
(click it and type), and rerollable — and the menu shows what that seed
|
||
builds (the galaxy's name, system count, archetype count) **before** you
|
||
commit. Same seed ⇒ same galaxy.
|
||
- **Procedural galaxy**: 90 star systems in a seeded wide (2:1, plate-shaped) field —
|
||
even, organic spacing (Poisson disk, `data/galaxy.json`), the player's
|
||
home in the lower-right corner, and a home→far difficulty axis split
|
||
into near/middle/far zones — typed into six themed archetypes
|
||
(`data/systems.json`) with per-type weights and per-zone mixing
|
||
(`galaxy.distribution.zoneMix`) — the galaxy lays itself out on a
|
||
progression the trade/combat economy can lean on.
|
||
- **Two-level generation**: the whole galaxy roster is generated at New
|
||
Game (~15 ms); each system's planets/moons/belts/settlements are
|
||
generated lazily on arrival, deterministically (seed + system id), so
|
||
lazy and eager give identical results.
|
||
- **A lived-in galaxy**: the galaxy was settled long before you arrive.
|
||
Every planet is settled (for now): colonies on habitable worlds, mining
|
||
stations over the rest, cloud bases riding gas giants — plus stations
|
||
adrift in open space and beacons, whose odds are per-archetype
|
||
(`data/systems.json`) and thin out from the settled core to the wilder
|
||
rim (`data/galaxy.json`). **Barren systems** (~10%, the
|
||
`objectCount` → 0 stops) are the DEAD-END LEAVES of the jump-gate maze:
|
||
they hold nothing but their single jump gate (and 1–2 asteroid
|
||
clusters drifting inside that gate's tether, their only payload) and
|
||
report *charted · unclaimed*; you enter and exit through the same
|
||
gate. Non-home systems hold 0/2/3/4/5 objects
|
||
(planets + free-space stations, `data/systems.json → objectCount`);
|
||
every non-barren system holds at least one planet, and the gate's
|
||
tether anchors on one of them (so the player can always build and
|
||
expand out from the gate);
|
||
the starting system always holds the home world, a gas giant,
|
||
and a rocky world. Each settlement has a name, population, and an
|
||
`owner` seam reserved for the factions/pirates to come.
|
||
The current system's dossier (name, identity, what's there) shows
|
||
top-left in the game scene.
|
||
- **Names with a point of view** (`data/naming.json`): planets draw from a
|
||
deep curated bank — colonial "New Denver" worlds beside alien "Klaxoria"
|
||
worlds — and stations from a bank of official designations ("Deep Space
|
||
SC-145") and smuggler hangouts ("Hell's Hideout"). Names are dealt out
|
||
randomly per system **without repeats until the bank is exhausted** (a
|
||
seeded shuffle, so the first N draws are N distinct names). Stars and the
|
||
galaxy itself are still synthesised from syllable pools, so you never run
|
||
out of those. Within a system a planet never shares a name with another
|
||
planet and a station never with another station.
|
||
- Game screen with a top-down ship (real spritesheet art — frame 0 of
|
||
`assets/images/ships-player.png`, see `data/ship.json`): **click anywhere to fly there**
|
||
in the current system's open space. Every system also holds 1–3 JUMP
|
||
GATES — solid, discoverable exits that face their destination star —
|
||
wired into a PURE SPANNING TREE gate network (data/gates.json, no
|
||
shortcuts, no closed loops: exactly one route between any two systems —
|
||
a maze of dead ends and long hauls); every gate is `active: false` for
|
||
now — DORMANT (dimmed, field still) — and activation is the seam for
|
||
the tether mechanic (an activated gate anchors a level-1 tether);
|
||
the jump drive itself comes next.
|
||
Discovered worlds get a screen-edge arrow + a name tag showing the
|
||
world's **name** (e.g. `HOME WORLD · ESHKAELURA`); **clicking the
|
||
name tag autopilots the ship there** (it arrives on the keep-out rim,
|
||
facing the world)
|
||
- **The home planet** — the player's world — in the system you start in:
|
||
a 1024 px disc rendered 1:1 from the `assets/images/planets.png`
|
||
spritesheet, with a **proper name drawn from the planet name bank** (the
|
||
first name off the starting system's deck, so it never clashes with one
|
||
of the other worlds). Which Terran face it shows (frames 0–2) is a
|
||
seed-deterministic pick, so the same galaxy always yields the same home
|
||
world. The ship spawns ~150 px (edge-to-edge) off its rim in a
|
||
seed-derived direction, may fly in as close as 50 px from the rim, and
|
||
can never cross it — a planet is solid (tuning in `data/planets.json`)
|
||
- **The tether — your range.** You start with a level-1 tether anchored
|
||
on the home world: a circular zone, **5120 px from the planet's center**
|
||
(`data/tether.json`). The ship cannot leave it — anywhere in the union
|
||
of its tethers' zones is open space (where zones overlap there is no
|
||
line and no wall), and the rim is a hard barrier rendered as a thick,
|
||
glitchy, marching dotted line (neon core + RGB fringe + ambient glitch
|
||
bursts, contact shudder where the ship bumps it). Click-to-fly and
|
||
autopilot targets are clamped to the union boundary, so a world outside
|
||
your range is where the ship parks — on the line — until a tether grows.
|
||
Upgrades and extra anchors (planets/stations) plug into
|
||
`TetherField.add/setLevel` (tuning in `data/tether.json`)
|
||
- **The Research console** — the **RESEARCH** deck button opens a full-screen
|
||
window: the left pane loops the muted `assets/videos/research-computer.mp4`
|
||
archive feed (scanlines, sweep band, REC pulse, glitch bursts), the right
|
||
pane holds the category tabs and the **branching tech tree** for the
|
||
selected category — it starts at the top and unlocks downward, one branch
|
||
at a time. Selecting a tech shows its dossier (icon + description +
|
||
duration) and a **RESEARCH** button, which appears only when the tech is
|
||
researchable and nothing else is in progress (one project at a time,
|
||
`maxConcurrent: 1`). A deck progress bar tracks the run; completion fires
|
||
the tech's `effects` (e.g. `tether: {level: N}` grows the home tether's
|
||
range) and a toast. Each category's tree is its own JSON file under
|
||
`data/research/` (**Exploration**: Tether Level 1–3;
|
||
**Mining**: Improved Mining Arm, Advanced Mining
|
||
Arm, Improved Mining Storage — all three unlock the matching build in
|
||
the console's MINING tab). Unlocks and in-progress work
|
||
persist in the save bank — and resume correctly on load
|
||
- **The Build console** — the **BUILD** deck button (on a planet surface,
|
||
where the deck's SHOP/BUILD/TAKE OFF live) opens a full-screen window:
|
||
the left pane loops the muted 2:3 `assets/videos/build.mp4` build feed
|
||
(scanlines, sweep band, REC pulse), the right pane holds the category
|
||
tabs (**Planet / Cargo**, `data/builds.json`) and a **list** of the
|
||
category's buildable items — not a tree: builds are one-off installs on
|
||
the planet. Locked items are grayed out with their missing requirements
|
||
(research, tether level); installed ones read **BUILT ✓**. Selecting a
|
||
build shows its dossier (glyph + description) with the **highlighted
|
||
cost** and a **BUILD** button that appears only when the build is
|
||
available and affordable (one build at a time, `maxConcurrent: 1`).
|
||
Every world — planet or deep-space station — gets **Tether – Level 1**
|
||
installed automatically the moment you first land (free + instant, no
|
||
console step); the home world ships with it from the start. **Tether
|
||
– Level 2** needs the level-2 tether research plus a world holding a
|
||
level-1 tether, and installs in **20 s for 200 minerals** — the planet's
|
||
tether range then grows from 5120 to 10240 px (world state that outlives
|
||
the surface stay). While a build runs the whole deck is locked (BUILD
|
||
stays open to watch the progress); completion fires the build's
|
||
`effects` and a toast. Build records + the in-progress build persist in
|
||
the save bank — and resume with their remaining time on load
|
||
- **The Map console** — the **MAP** deck button opens a full-screen
|
||
cartography window (`data/map.json`, `js/ui/MapWindow.js`), the
|
||
ResearchWindow's bones: the left pane loops the muted 2:3
|
||
`assets/videos/map.mp4` feed (scanlines, sweep band, REC pulse, glitch
|
||
bursts). Two tabs on the chart plate:
|
||
- **CURRENT SYSTEM** — the whole solar system on a padded frame
|
||
(discovered objects, the tether range as a glitch-dotted union
|
||
boundary, and a fog-of-war dim over what the tether doesn't reach
|
||
yet), the system's discovery + resources readout, hover tooltips,
|
||
and click-to-ENGAGE AUTOPILOT (the same autopilot as a world click);
|
||
wheel zoom / drag pan (the chart repaints crisp at the new scale)
|
||
- **GALAXY** — the whole galaxy: a **glowing star per system**, each
|
||
colored by its archetype and pulsing on that archetype's heartbeat
|
||
(binary stars beat fast with a twin core), the **jump-lane web** as
|
||
thin lanes — traveled lanes glow brighter with a flow packet drifting
|
||
the route, frontier lanes (one end charted) brighter still, unexplored
|
||
lanes faint threads — plus the **charted region** (the hull of the
|
||
visited systems, soft fill + subtle outline), HOME/SHIP markers, and
|
||
a reveal ripple out of home. Hover a star for its readout + its link
|
||
state to your current system; **click a star on a live lane → CONFIRM
|
||
JUMP** (the same gate transport as the world's gates). The run's
|
||
footprint — visited systems + traveled lanes — is part of the save
|
||
(old saves load, defaulting to “your system is charted, nothing
|
||
traveled”). All of it tuned in `data/map.json → galaxy`
|
||
- Camera gently trails the ship; the **parallax starfield** streams past
|
||
while it flies and the view slowly recenters (≈1.5 s) once the ship
|
||
comes to rest
|
||
- Config-driven setup: every tunable value lives in `data/*.json`
|
||
|
||
## Project layout
|
||
|
||
```
|
||
orbit/
|
||
├── index.html # boots the game
|
||
├── data/ # ← ALL tunable config (edit these freely)
|
||
│ ├── manifest.json # which config files exist
|
||
│ ├── game.json # dimensions, colors, starfield, …
|
||
│ ├── menu.json # menu text, colors, button layout, seed panel
|
||
│ ├── theme.json # shared UI theme: fonts, palette, CRT/glitch tuning
|
||
│ ├── ship.json # the ship: art, feel (thrust, drag, maxSpeed…), base stats (hull, shields, cargo, minerals)
|
||
│ ├── combat.json # COMBAT: the range (player feel, enemy craft + AI + lance, rams, respawns, arena) + the main-game demo (game.*, energy, the bar)
|
||
│ ├── progression.json # COMBAT LEVELS: the XP curve (base × n^power, the cap) + the per-level armor reward
|
||
│ ├── planets.json # home world + system layout + solid-disc rules
|
||
│ ├── map.json # the MAP console: tabs, the system chart, the GALAXY tab (stars/pulse/lanes/region), stats, zoom
|
||
│ ├── tether.json # the tether (your range): level radii, barrier line, glitch, contact
|
||
│ ├── galaxy.json # galaxy scale & shape (count, 2:1 field, zones, corner home…)
|
||
│ ├── systems.json # system archetypes: theme, attributes, distribution
|
||
│ ├── settlements.json # the lived-in layer: settlement kinds & populations
|
||
│ ├── gates.json # JUMP GATES: network (1–3 gates, local jumps, pure spanning tree — maze, no shortcuts) + placement (tether anchor, facing, radii, gaps)
|
||
│ ├── research.json # RESEARCH: global rules (time unit, one at a time) + category registry
|
||
│ │ # trees live one-per-file below (section name = file basename)
|
||
│ ├── research/
|
||
│ │ ├── exploration.json# the Exploration tree: tether levels, anchoring, signal amp
|
||
│ │ └── mining.json # the Mining tree: improved/advanced mining arm, improved storage (blueprints)
|
||
│ ├── builds.json # BUILDING: the build console's categories/resources + the buildable items
|
||
│ ├── actionbar.json # the command deck: 6 slots (Research, Scan, Ship, ·, ·, Menu)
|
||
│ ├── naming.json # names: star/galaxy syllable pools + the curated PLANET & STATION name banks
|
||
│ └── classes.json # THE CLASS LAYER: Vanguard/Wraith roster, accents, icon frames, Trestle's copy, perk numbers
|
||
├── assets/images/ # art: planets.png (1024×1024 frames), ships-player.png (256×256 frames), ships-enemy.png (128×128 frames)
|
||
├── assets/fonts/ # UI typefaces: Ethnocentric (headers), Centauri (body)
|
||
├── lib/ # vendored third-party libs (Phaser 4.2.1)
|
||
├── js/
|
||
│ ├── main.js # entry point: load config → boot Phaser (?combat boots the normal game with the combat demo live)
|
||
│ ├── config/ # Config singleton, ConfigLoader, game config
|
||
│ ├── scenes/ # MenuScene, GameScene (thin, orchestration)
|
||
│ ├── player/ # THE CLASS LAYER: ClassModel (pure reads of data/classes.json), ClassState (the run ledger: class + perks + banked skill points)
|
||
│ ├── combat/ # COMBAT: CombatSystem (the main-game pack — auto-fire, tracers, rams, armor, energy), the XP ledger (XpManager — levels from kills, data/progression.json), the range (CombatScene, js/combat/main.js boots it), EnemyShip, Bullet, Fx, StarDrift, pure rules in Combat.js
|
||
│ ├── entities/ # Ship (own behavior), Planet (home world, solid), Station, JumpGate (solids)
|
||
│ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator, SystemReport, JumpNetwork (the gate network)
|
||
│ ├── tether/ # Tether (pure range math) + TetherField (constraint + barrier line)
|
||
│ ├── research/ # ResearchModel (pure tree rules/layout), ResearchState (unlocks + active run), ResearchIcons
|
||
│ ├── build/ # BuildModel (pure build rules), BuildState (installed + in-progress records)
|
||
│ ├── ui/ # MenuButton, GlitchText, CyberShape, ActionBar, CombatBar (the combat-mode deck), DiscoveryCompass (reusable), ResearchWindow, BuildWindow, ClassWindow (the level-5 class console)
|
||
│ ├── visuals/ # Starfield, CyberOverlay (CRT/glitch, shared)
|
||
│ ├── utils/ # small pure helpers (Color, Rng, NameGenerator)
|
||
│ └── vendor/ # shim to the vendored Phaser
|
||
└── docs/PROJECT_NOTES.md # ← project conventions: read this
|
||
```
|
||
|
||
## Conventions (short version)
|
||
|
||
- **Config in JSON.** If a value might change, it goes in `data/`, not code.
|
||
Add a file = one line in `data/manifest.json`.
|
||
- **One class per file**, ES modules, scenes stay thin, entities own their
|
||
behavior. Details in [`docs/PROJECT_NOTES.md`](docs/PROJECT_NOTES.md).
|
||
- Phaser is imported only via `js/vendor/phaser.js` (one-file version swap).
|
||
|
||
## The player's loop: research + building
|
||
|
||
Beyond flying, Orbit is built around two progression verbs:
|
||
|
||
- **Research** (`data/research.json` + `data/research/<category>.json`) —
|
||
*time-based*. A project takes a fixed `duration` (`timeUnit: seconds`);
|
||
the player researches **one thing at a time** (`maxConcurrent: 1`).
|
||
Research is the gate: it unlocks **builds** and **further research**
|
||
(each category tree is a DAG — a node's `requires` must be unlocked
|
||
first). The RESEARCH deck button opens the console window
|
||
(`js/ui/ResearchWindow.js`): looping archive feed, category tabs, the
|
||
branching tree, per-tech dossier + RESEARCH button, and a deck progress
|
||
bar. The rules are pure (`js/research/ResearchModel.js`,
|
||
`ResearchState.js` — Node-testable); the window is a passive view that
|
||
asks `GameScene` to start a run. `effects` on a finished node carry the
|
||
payload (e.g. `{ tether: { level: 2 } }` → `TetherField.setLevel`), so new
|
||
effect kinds plug in without touching the tree data.
|
||
- **Building** (`data/builds.json`) — *cost-based*, paid in **minerals**
|
||
(and credits, when they land; `resources`). A build is a **one-off
|
||
install** (`category: planet | cargo | mining`): planet builds land on
|
||
the world's surface (`targets: ["planet"]`); the Mining tab's builds are
|
||
**ship upgrades** (`targets: ["ship"]` — the improved/advanced mining
|
||
arm set the ship's mining rate to 1.5 / 2.0 minerals per second, the
|
||
improved storage grows the hold to 350): built from any planet's
|
||
console, the effect is ship-wide, and it counts as built wherever the
|
||
ship lands (no re-buy). Each build names its `cost`, its `duration`
|
||
(seconds), the research that unlocks it (`requires`, mirroring the
|
||
research tree's `unlocks.builds`), optional world-state gates
|
||
(`planetRequires`, e.g. the tether level on that world), and its
|
||
`effects` (e.g. `tether: {level: 2}` → the world's tether field
|
||
strengthens; `mining: {rate: 2.0}` / `mining: {capacity: 350}` → the
|
||
ship's stats, MAX wins). The BUILD deck button (on a planet surface)
|
||
opens the console window (`js/ui/BuildWindow.js`): looping build feed,
|
||
category tabs, the build LIST (locked items grayed with their missing
|
||
gates), per-build dossier + highlighted cost + BUILD button. While a
|
||
build runs the deck is locked (one at a time) and the state ticks on
|
||
the game-loop clock — so a build started on the surface keeps running
|
||
if the player takes off. The rules are pure (`js/build/BuildModel.js`,
|
||
`BuildState.js` — Node-tested in `dev/builds.test.mjs`); the window is
|
||
a passive view that asks `GameScene.beginBuild()` to run the gates,
|
||
charge the cost, and start the clock, and the scene applies `effects`
|
||
on completion (ship upgrades re-derive from the build records on load,
|
||
`GameScene._restoreMiningUpgrades`).
|
||
- **The command deck** (`data/actionbar.json`, rendered by
|
||
`js/ui/ActionBar.js`) — the cyberpunk bar across the bottom of the
|
||
screen. Six evenly spaced slots: **Research, Scan, Ship, ·, ·, Menu**
|
||
in space; on a planet surface the same bar re-deals itself —
|
||
**Shop, Build, Ship, ·, Take Off, Menu** (`surface.buttons`). The
|
||
slots fire `onAction` (GameScene / SurfaceScene own the behavior);
|
||
two slots are reserved. Dressed with the menu's CRT language —
|
||
scanlines over the strip and the GlitchText RGB pull-apart on labels
|
||
and the panel outline during bursts. All layout, palette, and motion
|
||
(boot flicker, rail comet, glitch bursts, RGB split, scanlines) live in
|
||
the config.
|
||
|
||
`_`-prefixed keys (`_comment`, `_template`, …) are documentation, not
|
||
runtime data — loaders and tests ignore them.
|
||
|
||
## Dev tools
|
||
|
||
```sh
|
||
node dev/run-all.mjs # the whole suite (dev/*.test.mjs) — every test runs with plain `node`
|
||
node dev/ship-behavior.test.mjs # runs the real Ship.update() loop in Node
|
||
node dev/starfield.test.mjs # runs the real Starfield.create() in Node
|
||
node dev/galaxy.test.mjs # galaxy determinism, composition, lazy vs eager, gradient + report
|
||
node dev/frames.test.mjs # the galaxy-wide (class, frame) pass: in-pool, spread, determinism
|
||
node dev/jumps.test.mjs # the gate network: 1–3 gates, locality, strong connectivity + placement invariants
|
||
node dev/jumpgate.test.mjs # the JumpGate entity: discovery fields, dormant/active, the solid contract (Node)
|
||
node dev/discovery.test.mjs # discovery rules + compass geometry + chip hit test
|
||
node dev/tether.test.mjs # tether range math: union, clamp, visible arcs (no line in overlaps)
|
||
node dev/research-builds.test.mjs # data contract: research/builds/actionbar shapes + manifest
|
||
node dev/builds.test.mjs # the build console's pure rules + state machine (Node)
|
||
node dev/world-names.test.mjs # world-name casing: display casing must never leak into the data
|
||
node dev/saves.test.mjs # the save bank + capture/restore hand-off (incl. builds)
|
||
node dev/decode.test.mjs # the shared decode scramble (menu seed + system dossier)
|
||
node dev/system-hud.test.mjs # real GameScene dossier: layout + staggered decode to final report
|
||
node dev/quest-tracker.test.mjs # the quest tracker HUD: slide in/out, live checklist, priority, SHOW MORE, the popup contract
|
||
node dev/perimeter-shimmer.test.mjs # the READY row's dual-shimmer rim: perimeter arc math + the counter-streaming comets
|
||
node dev/quests.test.mjs # the quest ledger: grant/claim + the ONE priority (set/clear/sticks/auto-assign) + save round-trip
|
||
node dev/sfx.test.mjs # the shared SFX voice: guards + sfx.json keys/files
|
||
node dev/music.test.mjs # the shared music voice: guards + music.json contract
|
||
node dev/skills.test.mjs # the skill layer's pure half: data contract, the ledger (points/ranks/seats), save round-trip
|
||
node dev/skills-combat.test.mjs # the cast pipeline in Node: energy, per-skill recharge, the dash + arrival blast, hull passives
|
||
node dev/skills-bar-browser.test.mjs # the real bar: seat binding, icons, cooldown sweep, right-click clear, cast by key (Playwright)
|
||
node dev/skills-window-browser.test.mjs # ship console → SKILL TREE console: rank up with real clicks, hand a skill to a seat (Playwright)
|
||
node dev/skills-devdoor-browser.test.mjs # the ?skill doors — and that no flag means nothing happens at all
|
||
node dev/combat.test.mjs # the combat RULES: armor damage, the enemy's ring AI + lance decision, bullet life, CombatSystem (pack, auto-fire, hits, rams, armor, respawn), ship FEEL (handleFeel), the SFX event→voice map, and the READABILITY rules (pickTarget, feed, off-screen arrows, the shared CombatHud) — real data/combat.json
|
||
```
|
||
|
||
`dev/test-game.html` boots straight into the GameScene (no menu click),
|
||
handy for manual testing of the flight feel. `dev/research-shot.html`
|
||
does the same but opens the Research console and starts a run, and
|
||
`dev/cdp-shot.mjs` waits for a readiness expression before screenshotting
|
||
through CDP (any Chromium-CDP browser on `127.0.0.1:9333`):
|
||
|
||
```sh
|
||
node dev/cdp-shot.mjs "http://127.0.0.1:8081/dev/research-shot.html" shot.png \
|
||
"window.__RESEARCH_SHOT && window.__RESEARCH_SHOT.ready" 90000
|
||
```
|
||
|
||
`dev/build-check.html` plays the real Build flow (menu → New Game → home
|
||
world → build console) and prints a plain-text report of the L1/L2 row
|
||
states. Every real page (index.html) also carries a read-only console
|
||
diagnostic: opening the Build console logs a one-line `[orbit-diag v…]`
|
||
summary (planet, home, built records, tether level, L1/L2 row states,
|
||
stale-JS probe), and `orbitDiag()` in DevTools prints the full report
|
||
(data contract, save bank, scene wiring, errors since boot). If
|
||
`orbitDiag` is *undefined*, the browser served a cached older
|
||
`js/main.js` — run the dev server above, or hard-reload with the cache
|
||
disabled (DevTools → Network → Disable cache).
|
||
|
||
## Phaser
|
||
|
||
Phaser 4.2.1 is vendored at `lib/phaser.min.js` (UMD build, MIT license —
|
||
see `lib/PHASER_LICENSE.md`). No internet or npm needed at runtime.
|