Skip to content

Latest commit

 

History

History
374 lines (347 loc) · 23.8 KB

File metadata and controls

374 lines (347 loc) · 23.8 KB

Architecture

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.

Core boundaries

  • src/game/game.ts is 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.ts owns the entities. One place holds the ships, the cargo, the effects and the system's scenery. A module that needs the sky takes a World. The spawn is there, and Game.spawnNpc only forwards to it.
  • src/game/world-step.ts holds the headless flight loop. persistence.ts holds persistence. station.ts holds the dock transition and the launch transition. snapshot.ts says what a saved world IS. snapshot-parse.ts is the door that untrusted bytes come through.
  • src/engine/shell.ts is the platform seam. browser-shell.ts owns access to the browser and to the DOM. render-stack.ts is the only module that depends on the GPU. The controls read an input interface, not the browser. Two pointer seams cross the platform seam. onScreenClick is input. onScreenMove reports only: a screen may repaint what it describes, but it must never select or spend. engine/hold-buttons.ts is 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.ts works out where a marker goes. hud-binding.ts turns the state into a dashboard. hud.ts paints one. hud-buttons.ts paints 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 behind ui/screen-host.ts, and it reaches the page through ui/screen-shell.ts. A screen owns its own rendering, its own input and its own local state.
  • src/game/prompts.ts decides what a key can do about the situation right now. It returns a Command and words. It never returns a letter. cockpit-view.ts looks the label up, through ui/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.ts decides 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 of prompts.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.ts flies the picked course, one frame at a time, and it reports a FlightDemand. flight-instruments.ts throws 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.ts then 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.ts holds 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, with bankToTurn and both sticks. course-actions.ts joins the list to the Game: it builds the flat view, and it applies a pick. At the station, the LAUNCH row opens screens/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.queued is 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. tickMessage promotes it when the console falls quiet, docked or in flight. A message event carries queued, 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's recordVerdict is the one home of what a moved record says. Two rules in law-actions.ts reach it. One raises a record. The other works one off. law.ts's harmVerdict is the one home of the FIRST half, for a shot that lands on a ship the law protects. combat.ts asks it on the frame the ship turns against the commander, so it speaks once per ship (docs/TODO/173).

The orchestrator's children

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 left flight-instruments.ts on 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.

World and ships

  • src/galaxy/galaxy.ts generates the 1984 galaxy from seeds. No table ever stores it. world/system-scene.ts and the planet shader derive the system visuals from the same seed.
  • src/galaxy/living.ts advances the off-screen trade in whole days. It stores only deltas: the convoys, the danger and a bounded price pressure. populateSystem turns each arrival record into a ship. Its prewarm gives PREWARM_DAYS of 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.ts and price-divergence.ts. ui/chart-galactic.ts and ui/chart-local.ts paint the stars. ui/chart-overlays.ts paints the marks over them, and it paints only what those models return. game/chart-overlay.ts names the modes that T cycles through, and carries the result. The Game owns the current mode, so both charts show the same one.
  • src/galaxy/navigation.ts owns the chart metric and the cost of one jump. src/galaxy/route.ts answers 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.ts places a system's traffic: the traders, the police, the rocks, the hermit and the reception that waits for you. spawning-arena.ts places 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 through src/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 LineSegments with a black mesh that carries a polygon offset. A logarithmic depth buffer disables that offset. Never enable one.

Combat and pilots

  • src/game/npc.ts owns 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.update is 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.ts flies a rock, a hermit and a derelict. npc-fighter.ts flies the pirate, the police, the bounty hunter, the Thargoid and its drone. npc-trader.ts flies the trader. The three pilots are npc-brain-pilot.ts, npc-attack-run.ts and npc-pursuit.ts.
  • EACH ONE TAKES A CONTEXT RATHER THAN THE CLASS. npc-pilot.ts declares PilotShip, and npc-behaviour.ts declares BehaviourShip over it. Every import of npc.ts in those eight files is import type, so no file holds a runtime dependency on the ship. npc-state.ts owns the saved shape.
  • hostility.ts owns one rule over a fleet: it answers whether a ship attacks the commander, and it names no ship class (docs/TODO/169). trader-flight.ts owns 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.ts owns 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.ts is the source of truth for a pilot assignment. brains.ts imports no trained weights today.
  • pursuit flies the shipped opposition. attack-run names the defensive flight logic: the armed trader's three-phase run, and the player's pure-pursuit combat computer. scripted is the A/B control. It restores the plain attack run and disables the defence.
  • src/ai-training/scenario.ts builds 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.ts is the shared shot resolver. The game and the trainer both call it. Presentation stays with the caller. The shield selection lives one time, in shield-face.ts. test/fire-resolution.test.ts checks that both callers agree. combat.ts takes each ingredient separately, so a test can drive it. combat-player.ts is the assembly step that builds the player's own trigger out of one GameState.
  • src/game/targets.ts lists 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 of threat-lock.ts, and never changes that rule. target-actions.ts joins the list to its buttons, and gives each ship a code that names it for as long as it lives. close-pass.ts names 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.ts says what every button over the flight view reads, in words, and it holds no world (docs/TODO/206 M5). game/mission-course.ts says 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.ts says what a scan of a derelict reports, read off the world's own seed.
  • src/game/threat.ts computes the pirate count, the group tier and the organisation from the visible value and the reputation. ship-specs.ts maps 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.ts says which designs a role may EVER fly. That is permission. blueprint-set.ts says which one of the 23 released sets a system flies. set-roster.ts says 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.ts and world-build.ts name the override. Each of the three module headers holds the rule and the measurement behind it.
  • src/game/contract-record.ts holds the shape of a job: the Contract union on kind, and the two kinds that carry goods. It holds no rule. It left commander.ts when docs/TODO/190 M1 pushed that file over the size ceiling.
  • src/game/contract-offers.ts owns what the bulletin board offers today, and how each job reads. src/game/contracts.ts owns 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.ts owns 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 the paid event, one time each (invariant 15). Those orchestrators are Game.applyContracts and the campaign's settle site.
  • src/game/contract-eta.ts owns 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.ts lists 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 through contract-offers.ts, and a mission through its skeleton (missions/queries.ts).
  • src/game/mission-bridge.ts is 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.ts is the MISSIONS screen's two actions, acceptance and abandonment, and the offers it lists. src/game/hunt-warning.ts prices the commander's gun against the ship a hunt names, through the combat oracle.
  • src/missions/ is the mission machine (docs/TODO/190). model.ts holds the types, effects.ts the consequences the game applies, and words.ts the shape of a patron and a dossier. A skeleton under skeletons/ is one mission's rules, written by a developer. A dossier is its generated words, and the machine never reads one. machine.ts is 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.ts applies a branch's settlement, and leads.ts saves a lead (docs/TODO/217). A verb module under verbs/ decides what an input means for one leg, and the machine takes the branch. triggers.ts says what a trigger is called, and which dossier line a branch may speak with. lookups.ts reads a skeleton: a leg by id, a patron's key, and where a skeleton is offered. placement.ts picks a leg's world with one draw. A band measures tenths on the chart. A handover measures jumps on the full-tank graph. tour.ts places 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.ts answers the game's questions without a change. lint.ts holds the five failure rules as data checks, and test/mission-skeletons.test.ts runs it over every skeleton. repair.ts reads a saved record for both loaders, and drops the old stage number. offers.ts decides 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, through seed-pick.ts. hints.ts decides 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.ts is what a dock says about the offers and that message, and the machine calls it on a dock (docs/TODO/203). game/mission-arrival.ts says where a target is on an arrival, as a distance and a bearing in words. All eight verbs have a module under verbs/. skeletons/side.ts is 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, and ARC_TOUR lists them in order (docs/TODO/192). The escort and the scan verdicts are the world's to give, and world-step.ts gives them. story.ts tells 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.ts draws the worlds a story names as SVG text. The LOG screen (game/screens/log.ts, ui/screens-log.ts) shows both, and logHtml there is the one builder of the log's markup. tour-page.ts is the mission tour as data for the site's missions page (docs/TODO/193). dossiers.ts reads the committed dossiers through the generated dossiers/index.ts (docs/TODO/191). tools/generate-dossiers.ts writes one file per skeleton from the prompts tools/dossier-prompts.ts builds from a skeleton's shape and its patron. tools/dossier-faults.ts refuses a dossier before it is committed, and tools/ladder-rules.ts is the one home of the ladder-word lists it and the ladder test share. tools/batch.ts runs the batch for all three generators. patrons.ts answers who offers a mission, by name and by face (docs/TODO/191). It reads the committed file under patrons/, which tools/generate-patrons.ts writes from the prompts tools/patron-prompts.ts derives from the seed. A world with no record gets a plain patron, and the Navy is fixed in code.
  • src/game/character.ts owns 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.ts owns what becomes of a person that you scoop out of a capsule. screens/survivors.ts forces 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 touches cargo, because a rescued pilot is not stock.

The combat trainer

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.ts runs the exercise. It owns the commander swap, the entry snapshot, its own StepHost and the round loop. An exercise is ordinary flight with a different step behind it: the same WorldStep, the same brains, the same guns and the same seeded stream.
  • src/game/screens/combat-sim.ts is 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.

Conventions

  • A generator writes src/constants/CATALOG.md from the exported constants and their comments. Use npm run constants:find to find a constant. Run constants:check before 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.ts owns it. The ID separates a named save, a dock checkpoint and the three-entry flight ring. For the key grammar, see invariant 3 and save-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 in controls.ts and command-help.ts. ui/key-help.ts renders the help panel, the manual and the dock menu from those tables.
  • The supported debug handles are window.__game, window.__policyKit, state.brains.scripted and state.cheat. They expose or change explicit state. World logic does not read an ambient global.