One world state. A pure step advances it. Rendering only reads it. Keep each
file focused. Define each rule one time. INVARIANTS.md is the authority on the
rules. This file is a map.
src/game/game.tsis the orchestrator. It owns which mode the game is in. It hands each frame to the child that answers for that mode. It owns nothing else. A module decides and reports; an orchestrator applies the effect. For example, an NPC returns a fire event, a collision returns a pair, an encounter asks for a spawn, and a screen returns an outcome.src/game/world.tsowns the entities. One place holds the ships, the cargo, the effects and the system's scenery. A module that needs the sky takes aWorld. The spawn is there, andGame.spawnNpconly forwards to it.src/game/world-step.tsholds the headless flight loop.persistence.tsholds persistence.station.tsholds the dock transition and the launch transition.snapshot.tssays what a saved world IS.snapshot-parse.tsis the door that untrusted bytes come through.src/engine/shell.tsis the platform seam.browser-shell.tsowns access to the browser and to the DOM.render-stack.tsis the only module that depends on the GPU. The controls read an input interface, not the browser. Two pointer seams cross the platform seam.onScreenClickis input.onScreenMovereports only: a screen may repaint what it describes, but it must never select or spend.engine/hold-buttons.tsis the third: a button that holds a key down while it is held, such as the laser (docs/TODO/206).- The HUD is a read-only painter, and it is four files.
hud/hud-model.tsworks out where a marker goes.hud-binding.tsturns the state into a dashboard.hud.tspaints one.hud-buttons.tspaints the buttons over the flight view, such as the course buttons, and a click on one sends its code (docs/TODO/205). A screen lives behindui/screen-host.ts, and it reaches the page throughui/screen-shell.ts. A screen owns its own rendering, its own input and its own local state. src/game/prompts.tsdecides what a key can do about the situation right now. It returns aCommandand words. It never returns a letter.cockpit-view.tslooks the label up, throughui/key-help.ts. That is how invariant 9 reaches prose. A prompt is derived state, and the code saves nothing about it.src/game/courses.tsdecides what the ship can do next with no hand on the stick (docs/TODO/205). A course is one such thing, such as a trip to the station or a skim of the star. It follows the shape ofprompts.ts: a flat view in, a ranked list out, and no key. The list is derived state. The code saves the course that the pilot picks, and never the list.course-pilot.tsflies the picked course, one frame at a time, and it reports aFlightDemand.flight-instruments.tsthrows the switches that a course asks for: the torus drive, the hand-over to the docking computer, and the end of the course. A commander with no docking computer gets the docking mini game (docs/TODO/212). EVERY commander gets the same line-up first. The computer flies the ship to the right distance from the station, stops it, and turns it to face the port.world-step.tsthen asks the one question: a fitted docking computer takes the ship in, and a commander with none gets the rails and the game.dock-rails.tsholds the ship on the slot axis while the pilot matches the slot and the speed. It is the one file that moves the commander's ship other than by flying it, and it only ever takes up a residue. The last turn is flown, withbankToTurnand both sticks.course-actions.tsjoins the list to the Game: it builds the flat view, and it applies a pick. At the station, the LAUNCH row opensscreens/courses.ts, and a pick leaves on the course. In flight, the list is a set of buttons over the view, and not a screen, because the flight world stops under a screen.- The console is one line, so
SessionState.queuedis the line that waits for it (session.ts). Some consequences make sense only after their cause: what a scan cost your legal record, or what a deed cost your reputation. The console queues such a consequence; it does not say it at once.tickMessagepromotes it when the console falls quiet, docked or in flight. A message event carriesqueued, so a pure module can ask for the same treatment. A deed gets this order: what you did → what the sky did about it → where you now stand.law.ts'srecordVerdictis the one home of what a moved record says. Two rules inlaw-actions.tsreach it. One raises a record. The other works one off.law.ts'sharmVerdictis the one home of the FIRST half, for a shot that lands on a ship the law protects.combat.tsasks it on the frame the ship turns against the commander, so it speaks once per ship (docs/TODO/173).
Two halves split what a commander does. Neither half reaches into the other.
docked.ts— what the commander does once the ship stops. The arrival, the menu, the market, the outfitters and the board are here.flight.ts— what the commander does in the sky. That is one slice of time advanced, and who is at the controls for it. It has two children of its own:flight-weapons.ts— what the ship spends, and what it takes. Laser fire is here.flight-instruments.ts— the switches that change who flies the ship.flight-course.ts— the course at the controls. Three things end it. The work is done, or the pilot taps the lit button, or a hostile ship turns up. It leftflight-instruments.tson 2026-09-12.
A step that ends in a dock, a jump, a tow or a death reports that end to
game.ts. The orchestrator then decides what the game becomes.
Seven more children each hold one subject, beside the rules that each one spends:
world-build.ts— what is in the sky when you arrive.cockpit-view.ts— what the cockpit shows about the world.law-actions.ts— what the law does to a commander. It applies a record.hyperspace-actions.ts— how a commander leaves a system, and how they arrive in one. The jump is here.career.ts— what a career keeps when a flight ends.persistence.ts— the world written down, and put back.ui/screen-host.ts— the screen stack.
Each child's own module header is the long form. This map says where a thing lives, and it restates no rule.
Two quirks are deliberate:
- Witch-space reuses a normal system scene. That scene moves its bodies to ±1e8.
- The tests measure docking in station-local space. They measure it against the slot on the station's −Z face, and against the roll alignment.
src/galaxy/galaxy.tsgenerates the 1984 galaxy from seeds. No table ever stores it.world/system-scene.tsand the planet shader derive the system visuals from the same seed.src/galaxy/living.tsadvances the off-screen trade in whole days. It stores only deltas: the convoys, the danger and a bounded price pressure.populateSystemturns each arrival record into a ship. ItsprewarmgivesPREWARM_DAYSof history to a galaxy that nobody saved yet, on a derived stream. A new career and a galactic jump therefore both arrive in an economy that already runs. Three read-only models decide what the charts draw over it:danger-overlay.ts,trade-lanes.tsandprice-divergence.ts.ui/chart-galactic.tsandui/chart-local.tspaint the stars.ui/chart-overlays.tspaints the marks over them, and it paints only what those models return.game/chart-overlay.tsnames the modes thatTcycles through, and carries the result. The Game owns the current mode, so both charts show the same one.src/galaxy/navigation.tsowns the chart metric and the cost of one jump.src/galaxy/route.tsanswers the same question for a journey of several jumps. It searches over edges of one full tank, because fuel costs money and no days. Both modules are pure, and the charts print what they return.src/game/spawning.tsplaces a system's traffic: the traders, the police, the rocks, the hermit and the reception that waits for you.spawning-arena.tsplaces a combat-training exercise instead. The two share the idea of a scatter, and they answer to different constants.- A generator writes the released ship data under
src/game/elite-a/. The lookup and the combat profiles enter through its catalogue. The hull lookup enters throughsrc/ships/registry.ts. - A ship definition uses the source convention, a +Z nose.
buildShip()rotates it half a turn around Y, to face three.js forward (−Z). It does not mirror it. - A ship combines wireframe
LineSegmentswith a black mesh that carries a polygon offset. A logarithmic depth buffer disables that offset. Never enable one.
src/game/npc.tsowns the ship. It reads the world, it holds the saved state, it steers, it moves, and it reports a shot. It decides no flight.NpcShip.updateis 37 lines, and it is a dispatch (docs/TODO/184).- A BEHAVIOUR SAYS WHAT A SHIP DOES, AND A PILOT SAYS HOW IT FLIES. That
split is docs/TODO/182's, and Chris named the cause on 2026-08-17: the project
never used a good OO approach. A ship holds one behaviour for its role, built
in the constructor. There are three behaviours.
npc-idle.tsflies a rock, a hermit and a derelict.npc-fighter.tsflies the pirate, the police, the bounty hunter, the Thargoid and its drone.npc-trader.tsflies the trader. The three pilots arenpc-brain-pilot.ts,npc-attack-run.tsandnpc-pursuit.ts. - EACH ONE TAKES A CONTEXT RATHER THAN THE CLASS.
npc-pilot.tsdeclaresPilotShip, andnpc-behaviour.tsdeclaresBehaviourShipover it. Every import ofnpc.tsin those eight files isimport type, so no file holds a runtime dependency on the ship.npc-state.tsowns the saved shape. hostility.tsowns one rule over a fleet: it answers whether a ship attacks the commander, and it names no ship class (docs/TODO/169).trader-flight.tsowns a trader's working life: it arrives, it works the lane, then it docks or it leaves. It names no ship class either (docs/TODO/176).flight-maths.tsowns the nose, the thrust and the throttle rules that the ships, the trainer, the two spawners and the HUD's lead marker all share.brain-names.tsis the source of truth for a pilot assignment.brains.tsimports no trained weights today.pursuitflies the shipped opposition.attack-runnames the defensive flight logic: the armed trader's three-phase run, and the player's pure-pursuit combat computer.scriptedis the A/B control. It restores the plain attack run and disables the defence.src/ai-training/scenario.tsbuilds each episode from the real modules:NpcShip,PlayerShip, gunnery, collision, RNG and the fixed step. Only the trainer's own concerns belong to the trainer — fitness, observations and opponent pools.src/game/fire-resolution.tsis the shared shot resolver. The game and the trainer both call it. Presentation stays with the caller. The shield selection lives one time, inshield-face.ts.test/fire-resolution.test.tschecks that both callers agree.combat.tstakes each ingredient separately, so a test can drive it.combat-player.tsis the assembly step that builds the player's own trigger out of oneGameState.src/game/targets.tslists what the ship can fight, and holds the pilot's pick (docs/TODO/206). The pick is one flag on the picked ship's own state, so a save carries it. It sits on top ofthreat-lock.ts, and never changes that rule.target-actions.tsjoins the list to its buttons, and gives each ship a code that names it for as long as it lives.close-pass.tsnames a neutral trader that comes close, one time, so the pilot knows the chance is there (docs/TODO/209). It never stops the ship.cockpit-buttons.tssays what every button over the flight view reads, in words, and it holds no world (docs/TODO/206 M5).game/mission-course.tssays what a live mission asks the ship to do here (docs/TODO/208). There are five shapes: a fight, a hold, an escort, a scoop, and a slip past the police.game/derelict.tssays what a scan of a derelict reports, read off the world's own seed.src/game/threat.tscomputes the pirate count, the group tier and the organisation from the visible value and the reputation.ship-specs.tsmaps a tier to a hull. The campaign simulator calls the same rules.- Which hulls a system can send is three modules, and each answers one part.
ship-roles.tssays which designs a role may EVER fly. That is permission.blueprint-set.tssays which one of the 23 released sets a system flies.set-roster.tssays what that one set files under each job. That is selection. A design must pass permission and selection both. The set is drawn once on arrival, and it is saved state. Two released overrides can answer in place of that number: a mission's leg raises one, and witch-space raises the other.missions/queries.tsandworld-build.tsname the override. Each of the three module headers holds the rule and the measurement behind it. src/game/contract-record.tsholds the shape of a job: theContractunion onkind, and the two kinds that carry goods. It holds no rule. It leftcommander.tswhen docs/TODO/190 M1 pushed that file over the size ceiling.src/game/contract-offers.tsowns what the bulletin board offers today, and how each job reads.src/game/contracts.tsowns the rest of a contract's life: what acceptance costs the hold, what a delivery pays, and what a failure costs. On a failure the freight goes back, and the contract bills you for the part that you cannot hand back.src/game/market.tsowns what a station charges. All three modules are pure, and the campaign simulator shares them (invariant 10). The place where the state lives splits a contract's consequences. Settlement applies the commander's disrepute, and it credits itself. The orchestrators apply the destination's regional heat from thepaidevent, one time each (invariant 15). Those orchestrators areGame.applyContractsand the campaign's settle site.src/game/contract-eta.tsowns what the CHARTS say about a job you hold. That is three things: the worlds to mark, the words of the verdict, and whether the deadline still holds. It is pure, and it paints nothing. It takes the commander, not a day number, so a caller cannot measure a deadline from the living galaxy's day by mistake.src/game/orders.tslists everything a commander is under orders to do. The game has two kinds of standing order: a signed contract, and a live mission. This module asks both kinds the same question, so the menu line, the MISSIONS screen and the charts cannot hold three answers. Invariant 16 lives here. It restates no words: a contract reads throughcontract-offers.ts, and a mission through its skeleton (missions/queries.ts).src/game/mission-bridge.tsis the game's side of the mission machine. It runs one input, installs the record on the commander, and applies the effects that are the commander's: credits, reputation and legal status. It hands the words back, and the caller says them. It hands a spawn order back too, and the world step spawns an ambush (docs/TODO/214).src/game/mission-desk.tsis the MISSIONS screen's two actions, acceptance and abandonment, and the offers it lists.src/game/hunt-warning.tsprices the commander's gun against the ship a hunt names, through the combat oracle.src/missions/is the mission machine (docs/TODO/190).model.tsholds the types,effects.tsthe consequences the game applies, andwords.tsthe shape of a patron and a dossier. A skeleton underskeletons/is one mission's rules, written by a developer. A dossier is its generated words, and the machine never reads one.machine.tsis one pure step. It takes the record, one input and the facts it may read. It returns a new record with the effects the game applies.settlement.tsapplies a branch's settlement, andleads.tssaves a lead (docs/TODO/217). A verb module underverbs/decides what an input means for one leg, and the machine takes the branch.triggers.tssays what a trigger is called, and which dossier line a branch may speak with.lookups.tsreads a skeleton: a leg by id, a patron's key, and where a skeleton is offered.placement.tspicks a leg's world with one draw. A band measures tenths on the chart. A handover measures jumps on the full-tank graph.tour.tsplaces the five arc starts from the seed, and an arc is offered in galaxy 1 alone. So a lead keeps its galaxy across a galactic jump (docs/TODO/213).queries.tsanswers the game's questions without a change.lint.tsholds the five failure rules as data checks, andtest/mission-skeletons.test.tsruns it over every skeleton.repair.tsreads a saved record for both loaders, and drops the old stage number.offers.tsdecides what a station offers: the gate, the lead, the slot cap and the re-offer delay. It also draws each world's roster of two or three side jobs from the seed, throughseed-pick.ts.hints.tsdecides how a far commander hears about a lead. It writes the MISSIONS row, the board rumour, the DATA ON line, and the one message a dock may say.hail.tsis what a dock says about the offers and that message, and the machine calls it on a dock (docs/TODO/203).game/mission-arrival.tssays where a target is on an arrival, as a distance and a bearing in words. All eight verbs have a module underverbs/.skeletons/side.tsis one local side job per verb, so every verb is a job a player can take.skeletons/arcs/holds the five arcs of the tour, one file each, andARC_TOURlists them in order (docs/TODO/192). The escort and the scan verdicts are the world's to give, andworld-step.tsgives them.story.tstells the journal as pages, one per run, in a dossier's words where one exists and the skeleton's plain words where none does.route-map.tsdraws the worlds a story names as SVG text. The LOG screen (game/screens/log.ts,ui/screens-log.ts) shows both, andlogHtmlthere is the one builder of the log's markup.tour-page.tsis the mission tour as data for the site's missions page (docs/TODO/193).dossiers.tsreads the committed dossiers through the generateddossiers/index.ts(docs/TODO/191).tools/generate-dossiers.tswrites one file per skeleton from the promptstools/dossier-prompts.tsbuilds from a skeleton's shape and its patron.tools/dossier-faults.tsrefuses a dossier before it is committed, andtools/ladder-rules.tsis the one home of the ladder-word lists it and the ladder test share.tools/batch.tsruns the batch for all three generators.patrons.tsanswers who offers a mission, by name and by face (docs/TODO/191). It reads the committed file underpatrons/, whichtools/generate-patrons.tswrites from the promptstools/patron-prompts.tsderives from the seed. A world with no record gets a plain patron, and the Navy is fixed in code.src/game/character.tsowns the disrepute ladder. It owns what a score is CALLED, and how a deed and a quiet week move it. It also owns whether a move crossed a rung that the pilot must hear about. Every deed in the game asks it the same question, so no site is free to disagree about what a crossing is.src/game/survivors.tsowns what becomes of a person that you scoop out of a capsule.screens/survivors.tsforces that choice when you dock. The module is pure, like the two modules above. It clears the crew spaces and reports. The orchestrator applies the heat and the record that a sale earns. It never touchescargo, because a rescued pilot is not stock.
The trainer is a real fight that costs nothing. docs/COMBAT-SIM.md is the long
form. Ten modules hold it, at about 4,600 lines, and this map named none of them
until docs/TODO/176.
One rule governs all ten: nothing that happens in the simulator leaves it.
Above all it must not advance a commander toward E L I T E.
src/game/combat-sim-safety.ts is the one home of that argument, in three
layers: the commander clone, an alternative StepHost, and the entry snapshot.
test/combat-sim-career.test.ts proves it.
Two parents hold the rest. One runs a fight. The other shows it to a pilot.
src/game/combat-sim.tsruns the exercise. It owns the commander swap, the entry snapshot, its ownStepHostand the round loop. An exercise is ordinary flight with a different step behind it: the sameWorldStep, the same brains, the same guns and the same seeded stream.src/game/screens/combat-sim.tsis the front of house. One screen id holds three panels, with the fight in between. It has two children:screens/combat-sim-setup.ts— the draft a pilot builds, and the rows that show it. It is pure, and it is the half worth a test.screens/combat-sim-notes.ts— the prose under those rows. Each block also states the tallest it could ever be, so a note that appears cannot shift the row under the cursor.
Four more modules each hold one subject. Each is pure, and each decides without applying anything:
combat-sim-scenarios.ts— who you fight, and whether the round is over. The scenarios are a table. The wave ramp and the live reception share it.combat-sim-opening.ts— where the fight happens, and where it starts. A training fight opens where the pilot can see it. A fight about an ambush is the one exception, and the record says so.combat-sim-report.ts— what happened, counted. It is fed samples and events, and it derives. The record carries a schema version, because the trainer's own exports are read outside the game.combat-sim-compare.ts— what two records may honestly show. Its load-bearing part is the refusal: six things must match before a difference column is a result. It offers no verdict, because the pilot judges.
combat-sim-strip.ts is the third view of a fight, and it is the one a pilot
reads DURING the exercise. It counts nothing. Every figure comes from the
round's own recorder, so the strip and the report cannot disagree.
src/ai-training/ is a different thing, and the two are often confused. The
trainer above is a person flying a fight. ai-training/ is a search that fits a
policy over thousands of episodes with nobody watching. Invariant 5 is what ties
them: the search flies the shipped modules rather than a copy.
- A generator writes
src/constants/CATALOG.mdfrom the exported constants and their comments. Usenpm run constants:findto find a constant. Runconstants:checkbefore you add or change a rule. - Money is integer tenths of a credit; fuel is tenths of a light-year.
- Forward is −Z. Pitch is local X. Roll is local Z. Nose-up is +X.
- Chart distance is
4·sqrt(dx² + (dy/2)²), in tenths of a light-year. The chart draws Y at half scale. - A save is one atomic record, and
storage.tsowns it. The ID separates a named save, a dock checkpoint and the three-entry flight ring. For the key grammar, see invariant 3 andsave-file.ts. - World randomness comes only from the seeded
game/rng.ts. Two training episodes must never interleave, because each one reseeds the world stream. - The flight bindings live in
engine/keymap.ts. The command bindings and their descriptions live incontrols.tsandcommand-help.ts.ui/key-help.tsrenders the help panel, the manual and the dock menu from those tables. - The supported debug handles are
window.__game,window.__policyKit,state.brains.scriptedandstate.cheat. They expose or change explicit state. World logic does not read an ambient global.