76 KiB
Implementation Plan — Deadhead: Space Freight Tycoon (Base Game)
Companion to game-design-document.md and tech-stack.md.
This plan covers the base game only: the core loop of taking contracts, dispatching ships, earning money, buying ships, renting bays, hiring pilots, discovering locations, and leveling pilots — all in real time with offline progress. Everything else is deferred (see §0.3).
0. Read First
0.1 Rules for AI developers
- Do the steps in order. Do not start a step until the previous step's test passes.
- Each step is one commit (or one PR). The commit message names the step number.
- Every step's test must be automated unless the step explicitly says "manual check."
packages/simmust stay pure: noDate.now(), noMath.random(), no network, no database, no file I/O. Time and seeds are always passed in as arguments.- All game numbers (prices, speeds, payouts, XP curves) live in
packages/content, never hard-coded in sim, server, or client. - The server is the authority. The client may predict (ETAs, positions, previews) using
sim, but never decides outcomes. - If a step is ambiguous, choose the simplest interpretation that satisfies its test, and leave a short note in
DECISIONS.mdat the repo root. - Do not add features not listed in a step.
- Update
progress.mdafter every step. Updatearchitecture.mdafter any database schema or API change; the full Drizzle schema lives inarchitecture.md. - Commit after every step without waiting for review. Stop for the user's review at the end of each phase.
0.2 Base game scope
| Area | In base game |
|---|---|
| Locations | Earth, Moon, Mars, Ceres Station (belt), Jupiter Cloud Platform |
| Landing classes | Vacuum, Atmospheric (Hardened is deferred) |
| Ships | 5 models: Courier, Lighter, Clipper, Freighter, Bulk Hauler (CR-012 added the Lighter and Clipper; it was 3 archetypes) |
| Cargo types | Standard, Heavy, High-Value, Dangerous |
| Pilot skills | Long Haul, Heavy Cargo, Expensive Cargo, Dangerous Cargo |
| Phases | Tutorial (contract pilot) → Owner-Operator → small fleet |
| Time | Real time with offline progress, compressed game clock |
| Platform | Web, installable as a PWA |
0.3 Explicitly deferred (do NOT build)
Random events, reputation, fuel price variation, throttle setting, multi-leg routes, recurring bay rent, pilot salaries over time, pilot poaching, automation/standing routes, interstellar, push notifications, Capacitor builds, Hardened landing class, Venus/Io, Fragile/Urgent/Fuel-efficiency skills, social features, monetization, account linking (email, Apple, Google sign-in) (CR-011).
0.4 Glossary
- Location — a place a ship can land (planet, moon, station, platform).
- Discovered — a location the player has delivered to at least once (Earth and Moon start discovered).
- Bay — a docking bay the player leases at a discovered location. It is an HQ: pilots are hired there, newly bought ships are delivered there, and pilots can be unassigned there. A bay does not control where contracts are offered. (Changed by CR-002.)
- Contract board — the freight market at a discovered location. Every discovered location has one per player per board period, whether or not the player has a bay there. (CR-002.)
- Deadhead — a reposition: a ship flying empty to another location. Fuel only, no payout, no XP. (CR-002.)
- Contract — a job: cargo of a type and mass, from origin to destination, with a payout and skill requirements.
- Dispatch — a ship + pilot sent on a trip, either carrying a contract or repositioning empty.
- Game time — the in-world clock, derived from real time × compression factor.
- Resolution — applying the result of a completed dispatch (payout, XP, ship relocation, discovery) exactly once.
- Avatar — the player's own pilot, created at the start. Exempt from pilot pay.
- Loaner — the company-owned Courier the avatar flies during the tutorial. Removed when the tutorial ends.
0.5 Resolved design decisions
These answer the pre-build review. Log each one in DECISIONS.md as it is implemented.
- All three base ships are atmospheric-rated. In the base game, ships are gated by range, capacity and speed. The landing-class check stays in the sim (it matters once Hardened ships arrive) and is tested with a test-only vacuum ship.
- Pilot locations. An assigned pilot is wherever their ship is. An unassigned pilot stays where they were unassigned. Pilots travel only by flying a ship. Unassigning is allowed only at a location with a player bay. New hires appear at a player bay the player chooses.
- Tutorial movement. The avatar and loaner move with each tutorial run. The tutorial board starts at the loaner's current location (Earth→Moon or Moon→Earth). The company pays fuel and pilot pay; the player receives only the tutorial share. (Amended by CR-001: the board has three contracts of different masses (0.5.31), and the loaner is a dedicated content ship (0.5.29).)
- Tutorial end. (Replaced by CR-001.) The tutorial ends when the player accepts the deal (0.5.30). Buying a ship and leasing an Earth bay no longer end it.
- Contract boards are stored. (Replaced by CR-002.) A board exists for every discovered location, per player, per period. It is generated on the first request in a period and stored; later requests in that period return the stored board. Every contract expires at the end of its board period.
- Cargo tier. Standard is tier 0. A non-Standard contract has a tier from 1 to 5, and that tier is the required level in the cargo's skill. Amended by 0.5.55 (CR-007): High-Value and Dangerous tiers come from the good.
- Long Haul. A config list of distance cut-offs, one per Long Haul level. The required Long Haul level is the number of cut-offs the route distance exceeds.
- Config-driven tuning. First-contact chance, first-contact multiplier, contract mass range, and Long Haul cut-offs all live in GameConfig. (Amended after the CR-001 playtest: the single mass range is replaced by per-ship-size mass bands,
contractMassBands.) - Game epoch. Two fields: a real start timestamp and the in-game ephemeris date it maps to. The clock is global for all players.
- World seed is per player and is also used for pilot and ship names.
- Payout is fixed when a contract is offered, using the distance at that moment. Travel time and fuel use the distance at departure.
- Pilot pay. (Replaced by CR-010.) Hired pilots earn
max(0, payout − fuel cost) × pilot profit shareper contract delivery. The avatar is exempt. Deadheads and tutorial runs pay nothing. - Hull rating is stored but has no effect in the base game.
- Ship names are generated from the world seed. No renaming.
- Bays have no ship limit.
- Tutorial purchases. (Replaced by CR-001.) During the tutorial, buying ships, leasing bays and hiring pilots are blocked with a
tutorial-activeerror. - Cargo fallback. If no cargo type is both produced at the origin and demanded at the destination, use Standard. Amended by 0.5.54 (CR-007): the class fallback uses each location's
cargoClassMix; the good then falls back to a Standard good on the route, else any Standard good made at the origin. - Testable clock. The server reads time through an injectable clock. Tests call job handlers directly; E2E tests use a test-only clock-offset endpoint that does not exist in production.
- Summaries. Dispatches have an acknowledged-at column; resolved dispatches without one form the "while you were away" summary.
- Same domain. In production, the client and API share one domain, with the API under
/api, so the session cookie is first-party (iOS Safari blocks cross-site cookies). - Tutorial boards per location. The tutorial board is stored per period and per loaner location (one Earth→Moon board, one Moon→Earth board). (Amended by CR-001: boards are also keyed by the number of tutorial runs completed, so each run gets a fresh three-contract board; the "large enough" size rule no longer applies.)
- Pilot endpoints. The route catalog includes "unassign pilot", and "hire pilot" takes the bay location to hire at.
- Avatar during tutorial. The avatar is locked to the loaner and cannot be assigned to an owned ship until the tutorial ends.
- Repositions need no skills. A reposition checks range and landing only; no Long Haul or cargo skill is required.
- Freighter range covers Ceres at its farthest from Earth (≈600M km), so it can also reach Jupiter at its closest. (Superseded by CR-012, decision 0.5.81: a Freighter reaches Ceres from Earth on most dates, not all, and never reaches Jupiter from Earth; it gets there by hopping via Ceres.) A ship left out of range of every bay waits until distances shrink; this is intended.
- Loaner in player state. Player state has a
tutorialobject with the loaner's location and whether a run is in flight. The loaner is not in the ships list. - Pilot location storage. Only unassigned pilots store a location; an assigned pilot's location is derived from their ship.
- Git hosting. The repo is on a self-hosted Gitea instance:
http://192.168.1.107:3000/rory/deadhead-space-freight-tycoon. CI uses Gitea Actions. - Loaner ship (CR-001). The loaner is its own ship model, "Company Courier", marked not purchasable. It is faster and has lower overhead than the player's Courier so tutorial runs take under one real minute.
- The deal (CR-001). Once the player has completed at least the configured number of tutorial runs and the loaner is at Earth with nothing in flight, a one-time deal becomes available. Accepting it removes the loaner, gives the player a Courier and an Earth bay, assigns the avatar to that Courier, sets the loan balance, and ends the tutorial. There is no decline option.
- Handling time (CR-001). Travel time = distance ÷ cruise speed + overhead + cargo mass × handling time per tonne. Repositions carry zero mass. Light contracts earn more XP per minute; heavy contracts earn more credits per minute.
- Loan (CR-001). The company has a loan balance. When a dispatch resolves with positive credits (after pilot pay), the lender takes min(balance, credits × repayment share). The player can pay down any amount up to the balance at any time. No interest, no missed payments, no fail state.
- Scripted tutorial contracts (CR-001). Content can define special tutorial contracts keyed to a tutorial run number. A scripted contract replaces one board slot on that run and can carry a custom name, pay multiplier, and a flavor message shown after delivery.
- Onboarding goals (CR-001). A fixed, ordered list of goals in content, each with a condition evaluated from player state and a one-time credit reward the player claims. Goals appear after the tutorial ends.
- Ship delivery (CR-002). Buying a ship requires choosing one of the player's bays. The new ship appears there, idle and uncrewed.
- Undiscovered locations (CR-002). Requesting the board of an undiscovered location fails with a
location-undiscoverederror. Deadheading to an undiscovered location is not allowed; only a contract delivery discovers a location. - Map art pipeline (CR-003). Source art lives in the map assets folder and is never edited. A script generates optimized copies (trimmed, resized at 1x and 2x, background converted to WebP) into a generated folder that the app loads.
- Constant screen size (CR-003). Body and ship sprites keep a roughly constant on-screen size as the player zooms, scaled only between a configured minimum and maximum factor. Positions still use the compressed radial scale from Step 10.1.
- Child bodies (CR-003). The Moon (and any future location whose position is "parent plus small offset") is drawn at a fixed screen-space offset from its parent, not at its scaled real position, so it never overlaps the parent at any zoom.
- Background (CR-003). The background is not tiled. It is scaled to cover the viewport with a margin and moves with gentle parallax, clamped so its edges never show.
- Fallback (CR-003). If any asset is missing or fails to load, the map draws the old placeholder for that item and logs one warning. The map never fails to render because of art.
- Duty clock (CR-004). Pilots accrue duty hours equal to each trip's travel time in game hours, for contracts and deadheads. Tutorial runs accrue nothing.
- Duty limit (CR-004). Base duty limit plus a per-level Long Haul bonus, both in config.
- Finish the job (CR-004). Dispatch is blocked only while a pilot is resting. A trip that will exceed the limit is allowed; mandatory rest starts on arrival.
- Rest (CR-004). Starting rest sets duty hours to zero and sets rest-ends-at to start time plus the rest duration. Mandatory rest starts at the trip's arrival time, so it is correct even if resolution happens later (offline).
- Rest covers a board refresh (CR-004). The rest duration converted to real time must be at least the contract board period. This is enforced by a content test.
- Visual identity (CR-005). One "freight terminal HUD" look for the whole client: space background, translucent panels, amber/cyan/emerald/rose/fuchsia accent roles, Chakra Petch + Barlow fonts, self-hosted.
- Tokens and kit (CR-005). Colors, fonts, radii, shadows and motion are Tailwind theme tokens in
apps/web/src/index.css; shared components live inapps/web/src/ui/and screens compose them. - Navigation (CR-005). Icon tabs: a bottom bar on phones, a left rail from 768 px wide. Tab order and labels are unchanged. Attention badges are decorative (
aria-hidden), so each tab's accessible name stays its label. - Art on screens (CR-005). Ships and locations show their content
spritefrom the generated map art as plain images with emptyalt; the name is always in text beside it. A missing image hides itself. - Presentation only (CR-005). The overhaul changes no game rules, API calls, copy or accessible names.
- Boards fit the fleet (CR-006). Location boards are generated with the player's fleet (ship models and pilot skills) as input. Bands the fleet can't carry shrink to a preview; in bands it can, all but a configured number of contracts are ones some ship-and-pilot pair can take.
- Weight warning (CR-006). The board highlights a contract's mass when none of the player's ships at that location (or in the fleet, when none are there) can carry it. Display only.
- Goods (CR-007). Contracts carry a named good from the catalog; a location's produces and demands lists name goods. After the generator picks a contract's class (unchanged), it picks a good of that class produced at the origin and demanded at the destination, falling back to a Standard good on the route, else any Standard good produced at the origin. Mass comes from the slot's ship-size band, inside the good's range when they overlap.
- Tier from goods (CR-007). High-Value and Dangerous contracts take their tier from the good; Standard is tier 0; Heavy keeps the random tier.
- Priority (CR-007). A Priority-eligible good becomes Priority with the configured chance. Its delivery window is the reference ship model's trip time for the cargo and route at offer time × the window factor; a ship whose trip time exceeds it fails dispatch with
too-slow. Priority pays the configured bonus; no skill is required. - Board mix unchanged (CR-007). Class selection (by each location's class-level
cargoClassMix), board size and first-contact chance keep their logic and config. - Shippers (CR-007). Each contract gets a shipper name from its origin's list. Flavor only.
- Tutorial goods (CR-007). Tutorial contracts use Standard goods produced at the loaner's location and demanded at the other end; never Priority. The scripted "Unmarked crates" contract is unchanged.
- Market rate (CR-008). Each board contract (not tutorial) gets its own pay rate, uniform within ±
contractPayVarianceof 1, multiplied into its payout; the average is unchanged. - More early goods (CR-008). Water Recyclers, Spare Parts and Lunar Base Supplies (Earth → Moon), Crew Personal Effects and Recycled Alloys (Moon → Earth) join the catalog.
- Visible refresh (CR-008). When a location's board changes, the screen announces "New contracts posted" and tags contracts not on the last board the player saw there "New" (remembered per location in the browser; nothing tagged on a first visit).
- Effective stats (CR-009). A ship's effective stats are its model's base stats adjusted by its installed upgrade levels. Percentage bonuses are of the base stat and add up linearly.
- Where and when (CR-009). An upgrade can be started only on an owned ship that is idle (not in flight, not already in the shop) at a location with a player bay, with tutorial mode off. The Company Courier loaner can't be upgraded.
- One level at a time (CR-009). Each purchase raises one upgrade by exactly one level. A ship can be in the shop for one upgrade at a time.
- Shop time (CR-009). Starting an upgrade deducts the cost, raises the level immediately, and sets the ship's in-shop-until time. The ship can't be dispatched until then (
ship-in-shop). Because dispatch is blocked, there is nothing to complete later: no background job is needed. The assigned pilot stays assigned and can rest meanwhile. - Models stay distinct (CR-009). Content caps guarantee: a maxed Courier's capacity is below a base Freighter's; a maxed Bulk Hauler's speed is below a base Freighter's; a maxed Freighter's range is below a base Bulk Hauler's (so only Bulk Haulers reach Jupiter at its farthest). (Amended by CR-012:) also a maxed Clipper's capacity is below a base Freighter's; a maxed Courier's speed is below a base Clipper's; a maxed Lighter can't reach Mars from Earth or the Moon on any sampled date; "only Bulk Haulers reach Jupiter at its farthest" is measured from Earth.
- Courier to Mars (CR-009). A Courier with maxed fuel tanks can reach Mars at and around closest approach, but not at Mars's farthest distance. Base and lower-level Couriers still can't reach Mars at all. (Amended by CR-012:) measured from Earth; from the Moon it reaches Mars more often, which is fine.
- Deduction order (CR-010). On resolution: pilot pay is taken from the payout first; loan repayment (0.5.32) is calculated on what remains.
- Veteran pilot (CR-010). Accepting the deal also creates the veteran (CR-010 §2): at Earth, unassigned, level 2 with Heavy Cargo 1. The veteran is a normal hired pilot in every respect except how they were acquired. (Amended by CR-013:) the veteran starts with Expensive Cargo 1 (was Heavy Cargo 1) and crewing the deal's Clipper (0.5.82), not unassigned.
- No accounts yet (CR-011). Players remain guests. Account linking (email, Apple, Google) stays deferred (§0.3).
- Backup code (CR-011). Each player can have one backup code: random, at least 80 bits of entropy, shown as groups of letters and digits that avoid look-alike characters (no 0/O, 1/I/L). The server stores only a hash. Generating a new code invalidates the old one. Entering a valid code on any device signs that device into the same player. Restore attempts are rate-limited per IP and per device.
- Several devices (CR-011). More than one device may be signed into the same player at once; the server is authoritative, so this is safe.
- New company (CR-011). Starting a new company deletes all of the player's company data and recreates the starting state exactly as guest creation does (CR-001 tutorial state, new world seed). The player record and backup code stay the same.
- Company name (CR-011). Players can rename their company: 3–32 characters, letters, digits, spaces, and basic punctuation, trimmed. The default name is generated from the world seed.
- Display preferences (CR-011). Clock defaults to the device's locale format. Countdowns default to "real time". Reduced motion defaults to the device's reduced-motion setting and can be overridden.
- Per-leg range (CR-012). Ship range is checked per leg, from the departure location, at departure time. Design targets and tests name their origin explicitly.
- Gravity-well range factor (CR-012). Each location has a launch range factor (Earth 1.0, Mars 1.15, Moon 1.3, Ceres Station 1.3, Jupiter Cloud Platform 1.0). Effective range from an origin = ship effective range × origin factor. Separate from the launch fuel-cost multiplier.
- No-strand rule (CR-012). A dispatch (contract or deadhead) is rejected with
would-strandif, at arrival plus a configured margin (48 game hours), the ship could not reach any other discovered location from the destination with its effective range from there. - New ships (CR-012). Lighter (short-haul heavy lifter) and Clipper (fast long-legged runner), purchasable, atmospheric.
- Reach targets (CR-012). CR-012 §3's reach table is the source of truth for ship range values; ship ranges (not location factors) are adjusted so every cell holds.
- The veteran's Clipper (CR-013). Accepting the deal also creates an owned Clipper at Earth with the veteran assigned to it.
- Deal loan (CR-013). The deal loan amount is Courier price + Clipper price + Earth bay lease, computed from content.
Phase 1 — Repository Setup
Step 1.1 — Create the monorepo
- Create a pnpm workspace with folders
apps/web,apps/server,packages/sim,packages/schema,packages/content. - Each package gets its own
package.jsonwith a name scoped as@deadhead/<name>. - Add a root TypeScript base config with strict mode on; each package extends it.
- Test: Running the root install command succeeds, and running the root type-check command reports zero errors across all packages (each containing one empty placeholder file).
Step 1.2 — Add linting and formatting
- Add ESLint and Prettier at the root with shared config.
- Add a lint rule that forbids
Date.now,new Date()with no arguments, andMath.randominsidepackages/simandpackages/content. - Test: The root lint command passes. Temporarily adding
Math.random()to a file inpackages/simmakes lint fail; remove it afterward.
Step 1.3 — Add the test runner
- Add Vitest at the root configured to run tests in every package.
- Add one trivial passing test in
packages/sim. - Test: The root test command runs and reports 1 passing test.
Step 1.4 — Add continuous integration
- Initialize git in the project folder (it is not a repo yet) and push to the Gitea remote (decision 0.5.28).
- Add a Gitea Actions workflow (
.gitea/workflows/) that runs install, type-check, lint, and test on every push. - Test: Push the branch; the Actions run is green. Ask the user to confirm in the Gitea UI.
Step 1.5 — Add local Postgres via Docker Compose
- Docker Desktop is installed on the development machine. Add a
docker-compose.ymlwith a Postgres service for development and a second Postgres database for tests. - Add an
.env.examplelisting every environment variable the server will need (database URLs, auth secret, port, game clock settings). - Test: Starting compose brings Postgres up, and connecting with a Postgres client to both databases succeeds.
Phase 2 — Content Package (static game data)
Step 2.1 — Define content schemas
- In
packages/content, define Zod schemas for: Location, ShipModel, CargoType, Skill, and a GameConfig object. - Location fields: id, display name, type (atmospheric world / airless body / cloud platform / belt station), landing requirement (vacuum or atmospheric), astronomy body it is positioned from (e.g., Earth, Moon, Mars, Ceres, Jupiter), gravity launch-cost multiplier, starts discovered (yes/no), bay lease price, produces (list of cargo type ids), demands (list of cargo type ids).
- ShipModel fields: id, display name, archetype, price, cruise speed (km per game hour), cargo capacity (tonnes), fuel range (million km), hull rating, landing class, takeoff/landing overhead (game hours), fuel cost per million km.
- CargoType fields: id, display name, base pay per tonne per million km, tier pay multiplier, required skill id (none for Standard). Tiers follow decision 0.5.6.
- Skill fields: id, display name, max level, description.
- GameConfig fields: time compression factor; game epoch as two fields (real start timestamp and the in-game ephemeris date it maps to); starting credits; tutorial payout share; contract board size; contract board period (real minutes — contracts expire at the end of their period); first-contact chance; first-contact payout multiplier; contract mass range; Long Haul distance cut-offs (one per level); XP per delivery formula inputs; XP needed per pilot level; skill points per level; hire cost per pilot; pilot pay per delivery (flat fee).
- Test: Unit tests confirm each schema accepts a valid example and rejects an object missing a required field.
- Amended by CR-004: GameConfig gains
dutyLimitGameHours,longHaulDutyBonusGameHoursandrestGameHours(Step 10D.1). - Amended by CR-009: Content gains the Upgrade schema (
upgradeSchema,SHIP_STATS, per-model overrides), the five upgrades, andupgradeLevelscost/shop-time config (Step 10I.1). - Amended by CR-010: GameConfig's flat
pilotPayPerDeliveryis replaced bypilotProfitShare(0.25); content gains the veteran pilot (Step 10J.1). - Amended by CR-012: Location gains
launchRangeFactor; ShipModel gains aroleline and an optional ownhandlingGameHoursPerTonne; GameConfig gainsnoStrandMarginGameHours(Step 10L.1).
Step 2.2 — Add the five locations
- Add data for Earth, Moon, Mars, Ceres Station, and Jupiter Cloud Platform following the destinations table in the GDD.
- Earth and Moon start discovered. Earth is atmospheric with the highest launch multiplier; Moon is vacuum with a low multiplier; Mars is atmospheric; Ceres is a belt station (vacuum); Jupiter Cloud Platform is a cloud platform requiring atmospheric landing.
- Test: A test loads all locations through the schema without errors, asserts exactly five locations, asserts exactly Earth and Moon start discovered, and asserts every cargo id referenced in produces/demands exists.
- Amended by CR-007:
produces/demandslist good ids (derived from the goods catalog); the old class lists are kept ascargoClassMix, and each location hasshippers(Step 10G.1).
Step 2.3 — Add the three ship models
- Courier: fastest, smallest hold, short range, atmospheric, lowest price.
- Freighter: balanced, atmospheric, mid price.
- Bulk Hauler: slowest, largest hold, longest range, atmospheric, highest price.
- Pick numbers so that, at default config: a Courier can reach the Moon but not Mars, Ceres or Jupiter, even at their closest; a Freighter can reach Mars and Ceres; only the Bulk Hauler can reach Jupiter at its farthest distance.
- All three ships are atmospheric (decision 0.5.1).
- Test: A test validates all ships through the schema and asserts the ordering rules: Courier speed > Freighter speed > Bulk Hauler speed, Bulk Hauler capacity > Freighter > Courier, price Courier < Freighter < Bulk Hauler. (Range rules are tested in Phase 3 once distances exist.)
- Amended by CR-012: The Lighter and Clipper join the lineup; these ordering tests are replaced by the CR's orderings (Step 10L.1), and the ranges are set by the reach table (Step 10L.4: Freighter 450, Bulk Hauler 1,250).
Step 2.4 — Add cargo types and skills
- Cargo types: Standard (no skill), Heavy (Heavy Cargo skill), High-Value (Expensive Cargo skill), Dangerous (Dangerous Cargo skill). Non-standard types pay more per tonne-distance.
- Skills: Long Haul, Heavy Cargo, Expensive Cargo, Dangerous Cargo, each max level 5.
- Test: A test validates all entries, asserts every cargo type's required skill exists, and asserts each non-standard cargo pays more than Standard.
- Amended by CR-007: Cargo types are the four cargo classes; a goods catalog (
data/goods.ts) adds named goods with class, mass range, pay multiplier, tier (High-Value and Dangerous), Priority eligibility and where they're made and wanted (Step 10G.1).
Step 2.5 — Add the default game config
- Set time compression to 60 (1 real minute = 1 game hour).
- Choose starting credits, tutorial payout share, and ship prices so that the cheapest ship plus an Earth bay lease is affordable after roughly 15–25 tutorial runs.
- Test: A test validates the config through the schema. A second test computes (Courier price + Earth bay lease) ÷ average tutorial payout (using a fixed placeholder average defined in the test) and asserts it falls between 15 and 25. This test will be revisited in Phase 12 with real sim numbers.
- Amended by CR-001: Replace the 15–25 runs test. New test: the total tutorial share earned over the configured tutorial runs, plus starting credits, covers fuel for at least three Courier Earth→Moon runs (no soft-lock after the deal).
Step 2.6 — Export a single content bundle
- Export one
contentobject containing all locations, ships, cargo types, skills, and config, plus lookup helpers by id. - Test: A test imports the bundle and looks up one item of each kind by id successfully; looking up an unknown id returns "not found" rather than throwing.
Phase 3 — Simulation Package (pure, deterministic core)
Step 3.1 — Game clock conversion
- Add functions to convert real timestamps to game time and back, using the config's epoch and compression factor.
- Test: Converting real → game → real returns the original value. One real minute after the epoch equals exactly one game hour after the game epoch at compression 60.
Step 3.2 — Seeded random number generator
- Add a small seeded PRNG with functions: next float in [0,1), integer in range, pick from list, and derive a child seed from a parent seed plus a string label.
- Test: The same seed produces the same sequence of 1,000 values across two runs. Different seeds produce different sequences. Child seeds derived with different labels differ.
Step 3.3 — Location positions
- Add a function that returns a location's 2D position (in million km, ecliptic plane) for a given game time, using
astronomy-enginefor the parent body. - Earth and Mars use their own heliocentric positions. Moon uses Earth's position plus a small fixed offset. Ceres uses its orbital elements (or a fixed-radius circular orbit approximation if the library does not provide Ceres). Jupiter Cloud Platform uses Jupiter's position.
- Test: For a fixed known date, Earth is about 150 million km from the Sun, Mars between 206 and 250 million km, Jupiter between 740 and 820 million km. Calling the function twice with the same time returns identical values.
Step 3.4 — Route distance
- Add a function returning the distance between two locations at a given game time.
- Test: Earth→Moon is under 1 million km at any time. Earth→Mars distance varies by at least 200 million km across a 2-game-year span (sample monthly). Distance A→B equals B→A.
Step 3.5 — Travel time
- Add a function: travel time = distance ÷ ship cruise speed + ship takeoff/landing overhead. Distance is measured at departure time (no mid-flight recalculation).
- Test: For fixed inputs, the result matches a hand-calculated value. A faster ship always arrives sooner on the same route. Overhead dominates on Earth→Moon (overhead is more than half of total time for the Courier).
- Amended by CR-001: Travel time also includes cargo handling: + mass × handling time per tonne (Step 10A.2, decision 0.5.31).
- Amended by CR-009: Travel time takes effective stats, including the ship's own handling time per tonne (Step 10I.2).
Step 3.6 — Fuel cost
- Add a function: fuel cost = distance × ship fuel cost per million km × origin location's launch multiplier.
- Test: Earth→Moon costs more than Moon→Earth for the same ship. Fuel cost scales linearly with distance.
Step 3.7 — Range and landing eligibility
- Add a function that decides whether a ship can fly a route at a given time, returning either "allowed" or a specific reason: out of range, or cannot land at destination (landing class).
- Test: A Courier can fly Earth→Moon and Moon→Earth. A Courier is out of range for Mars at its closest. A test-only vacuum ship fixture is rejected at Earth with the cannot-land reason. At Jupiter's farthest point from Earth, only the Bulk Hauler is in range. A Freighter can reach Ceres. (These confirm the Step 2.3 number choices; adjust content if they fail.)
- Amended by CR-009: Range checks use effective stats (Step 10I.2).
- Amended by CR-012: The range check uses the effective range from the origin (range × the origin's
launchRangeFactor,rangeFrom), and the tests name their origin; a Freighter no longer reaches Jupiter from Earth or Ceres from Earth at its farthest (Steps 10L.2, 10L.4).
Step 3.8 — Contract model and payout
- Define a Contract shape: id, origin, destination, cargo type, tier, mass, payout, required skill and level, required Long Haul level, offered-at time, expires-at time (end of its board period), first-contact flag.
- Add a payout function: base pay per tonne per million km × tier multiplier × mass × distance, times the first-contact multiplier when flagged. Payout is fixed at offer time using the distance at that moment (decision 0.5.11).
- Test: Payout scales linearly with mass and distance. Dangerous cargo pays more than Standard for identical mass and route. A first-contact contract pays more than an otherwise identical normal contract.
- Amended by CR-007: Contracts carry
goodId,shipper,priorityanddeliveryWindowGameHours; payout includes the good's pay multiplier and the Priority bonus (Step 10G.2). - Amended by CR-008: Board contracts multiply their payout by a market rate within ±
contractPayVarianceof 1 (tutorial contracts don't) (Step 10H.2).
Step 3.9 — Contract board generation
- Add a function that generates a bay's contract board from: the bay's location, the set of discovered locations, the board period index (real time ÷ board period), and the player's world seed.
- Rules: the board has the configured number of contracts; destinations are mostly discovered locations; cargo type is chosen from types the origin produces and the destination demands, falling back to Standard if none match; the configured first-contact chance decides whether a contract targets a random undiscovered location instead (flagged first-contact); mass comes from the configured range; non-Standard contracts get a tier from 1 to 5, which is their required skill level; required Long Haul level comes from the distance cut-offs; every contract expires at the end of the board period.
- Test: The same inputs always produce an identical board. A different period index produces a different board. No contract has origin equal to destination. Over 1,000 generated boards, first-contact contracts are between 5% and 15% of all contracts and every one targets an undiscovered location.
- Amended after the CR-001 playtest: contract mass comes from per-ship-size bands (
contractMassBands, e.g. 3 × 5–20 t, 3 × 21–100 t, 2 × 101–400 t), and the first contract in each band is Standard cargo to a discovered location, so every ship size always has a job a new pilot can fly. - Amended by CR-002: Input is a location, not a bay: boards are generated for any discovered location (Step 10B.1).
- Amended by CR-006:
BoardInputtakes the player's fleet (ship models and pilot skills); bands the fleet can't carry shrink to a preview and flyable bands are mostly jobs the fleet can take (decision 0.5.52, Step 10F.3). - Amended by CR-007: Class selection unchanged; each contract then gets a good (mass from its band, inside the good's range when they overlap), maybe Priority with a window, and a shipper (Step 10G.3).
Step 3.10 — Tutorial contract board
- Add a separate generator for the tutorial "company board": Earth↔Moon only, Standard cargo only, payout reduced by the tutorial payout share, always flown with the company loaner Courier. Contracts start at the loaner's current location: Earth→Moon when it is at Earth, Moon→Earth when it is at the Moon.
- Test: With the loaner at Earth, every tutorial contract is Earth→Moon; with it at the Moon, every one is Moon→Earth. All use Standard cargo and pay exactly the tutorial share of the equivalent normal payout.
- Amended by CR-001: Superseded by Step 10A.3 (tutorial board v2: three mass tiers, scripted contracts, runs-completed input).
Step 3.11 — Pilot eligibility
- Add a function that decides if a pilot can take a contract given their skill levels: the pilot's level in the contract's required skill must meet the required level, and the pilot's Long Haul level must meet the contract's required Long Haul level (decision 0.5.7).
- Test: A level-0 pilot can take a short Standard contract. A level-0 pilot cannot take a Heavy contract requiring Heavy Cargo 2. A pilot with Heavy Cargo 2 can. A level-0 pilot cannot take a long route requiring Long Haul 1.
Step 3.12 — Dispatch validation
- Add a function that validates a proposed dispatch: ship is idle at the contract's origin, pilot is idle and assigned to that ship (or the tutorial loaner case), cargo mass fits ship capacity, route passes range and landing checks, pilot is eligible, contract has not expired, player can afford fuel (skipped for tutorial dispatches, where the company pays).
- Return "valid" or the first failing reason.
- Test: One test per failure reason, each constructing a scenario that triggers exactly that reason. One test for a fully valid dispatch.
- Amended by CR-004: Validation (contract and deadhead) gains
pilot-resting, checked before the other pilot checks (Step 10D.3). - Amended by CR-007: Validation gains
too-slow: a Priority contract's trip must fit its delivery window (Step 10G.4). - Amended by CR-009: Validation uses effective stats (capacity, range, Priority window) and gains
ship-in-shop(Steps 10I.2, 10I.3). - Amended by CR-012: Validation (contract and deadhead) gains
would-strandafter range and landing: at arrival plus the no-strand margin, the ship must be able to reach another discovered location from the destination (Step 10L.3, decision 0.5.79).
Step 3.13 — Dispatch record creation
- Add a function that turns a validated dispatch into a Dispatch record: id, ship, pilot, contract (or none for reposition), origin, destination, departure real time, arrival real time (computed from travel time and clock conversion), fuel cost, expected payout, seed.
- Test: Arrival time minus departure time equals the travel time converted to real time. Creating two records from identical inputs yields identical records.
Step 3.14 — Reposition dispatch
- Allow a dispatch with no contract to any discovered location that has a player bay, paying fuel only and earning no payout or XP.
- Test: A reposition record has zero payout and zero XP. A reposition to a location without a player bay fails validation.
- Amended by CR-002: Deadhead rules replace reposition-to-bay: from wherever the ship is to any other discovered location, range and landing only,
location-undiscoveredotherwise (Step 10B.2).
Step 3.15 — In-flight position and progress
- Add functions that return, for a dispatch and a real time: progress (0 to 1, clamped) and an interpolated 2D position along a straight line from the origin's position at departure to the destination's position at arrival.
- Test: Progress is 0 at departure, 0.5 at the midpoint, 1 at arrival, and stays 1 after arrival. Position at progress 0 equals the origin position at departure time.
Step 3.16 — XP and leveling
- Add functions for XP gained per delivery (based on distance and cargo tier from config), pilot level from total XP, and skill points available (levels × points per level minus points spent).
- Test: A longer delivery grants more XP than a shorter one. Crossing the XP threshold increases level by exactly one. Skill points available never go negative.
Step 3.17 — Dispatch resolution
- Add a function that, given a dispatch and current real time, returns either "still in flight" or a Resolution: credits to add (payout minus pilot pay, with the avatar exempt from pilot pay; for tutorial runs, exactly the tutorial share), XP to add to the pilot, ship's new location, and whether the destination becomes discovered.
- Test: Before arrival → in flight. At or after arrival → Resolution. A delivery to an undiscovered location marks it discovered. A delivery flown by the avatar deducts no pilot pay; one flown by a hired pilot does. A reposition gives no credits or XP. Resolving the same dispatch twice with the same inputs returns identical results.
- Amended by CR-004: Resolution returns the pilot's new duty hours and, on mandatory rest, rest-ends-at from the arrival time (Step 10D.3).
- Amended by CR-010: Hired pilots earn round(max(0, payout − fuel) × profit share); the loan repayment is taken from what remains (decisions 0.5.12, 0.5.69; Step 10J.2).
Step 3.18 — Sim package public API and coverage
- Export only the public functions from a single entry point. Configure coverage reporting for
packages/sim. - Test: The test suite passes with at least 90% line coverage on
packages/sim.
Phase 4 — Shared Schema Package
Step 4.1 — API data shapes
- In
packages/schema, define Zod schemas for every API request and response the base game needs: player state, company, ship, pilot, bay, location (with discovered flag), contract, dispatch, resolution summary, server time, and errors. - Errors have a stable machine code (matching the sim validation reasons) plus a human-readable message.
- Test: Each schema accepts a valid example and rejects an invalid one. Every sim validation reason from Step 3.12 has a matching error code.
Step 4.2 — API route catalog
- Define a single typed catalog listing every endpoint: method, path, request schema, response schema. This is the contract both server and client use.
- Endpoints: get server time, create guest account, get full player state, get contract board for a bay, get tutorial board, create dispatch, buy ship, lease bay, hire pilot (at a chosen bay), assign pilot to ship, unassign pilot, spend skill point, acknowledge "while you were away" summary (decision 0.5.22).
- Test: A test asserts every endpoint has request and response schemas and that no two endpoints share a method and path.
- Amended by CR-011: The catalog gains
renameCompany,newCompany,getBackupCode,createBackupCode,restoreCompany(no session), and the non-productiondevAdvanceClockanddevAddCredits(Steps 10K.5–10K.11).
Phase 5 — Server Foundation
Step 5.1 — Fastify skeleton
- Create the Fastify app in
apps/serverwith environment config loading, structured logging, and a health endpoint. - Test: An integration test starts the app and the health endpoint returns success.
Step 5.2 — Server time endpoint
- Implement the server time endpoint returning the current real time and the current game time.
- Test: The returned real time is within 2 seconds of the test's own clock, and game time matches the sim conversion of that real time.
Step 5.3 — Database schema and migrations
- Using Drizzle, define tables: players (with world seed), companies, ships, pilots (with current location and an is-avatar flag), bays, discovered_locations, contract_boards (one stored board per bay per period, plus tutorial boards per period per loaner location), accepted_contracts, dispatches. Dispatches store the full record including a snapshot of the contract, plus a status (in_flight or resolved), a resolved-at time, and an acknowledged-at time.
- Add a migration command.
- Test: Running migrations against the empty test database succeeds; running them a second time is a no-op.
- Amended by CR-002:
contract_boardsis keyed by player, kind (location/tutorial), location and period (plus tutorial run); one board per player, location and period (Step 10B.3). - Amended by CR-004: The pilots table gains
duty_hoursandrest_ends_at(Step 10D.4). - Amended by CR-009:
ship_upgradestable and, on ships, in-shop-until and the latest shop job (Step 10I.4). - Amended by CR-011: Players store a backup code hash (unique) and its creation time;
player_loginslinks extra sign-ins to a player; the company name was already stored (Steps 10K.5, 10K.8).
Step 5.4 — Test database harness
- Add a test helper that resets the test database between integration tests and can seed a player in a given state (tutorial, owner with one ship, etc.).
- Test: Two integration tests that each create a player both pass when run together, proving isolation.
Step 5.5 — Guest accounts
- Integrate Better Auth with anonymous/guest sign-in. Creating a guest creates a player (with a random world seed) and company with starting credits, Earth and Moon discovered, the avatar pilot (level 0) at Earth, the loaner Courier at Earth, and tutorial mode on.
- Test: Calling create-guest returns a session. Fetching player state with that session shows starting credits, two discovered locations, one pilot, zero ships, and tutorial mode on. A request with no session is rejected.
Step 5.6 — Request validation from the route catalog
- Wire the schema catalog into Fastify so every request and response is validated automatically.
- Test: Sending a malformed body to any endpoint returns a validation error with the documented error format. A response that violates its schema fails in test mode.
Phase 6 — Server Game Logic
Step 6.1 — Lazy resolution on read
- When player state is fetched, resolve every in-flight dispatch whose arrival time has passed, in arrival order, inside one database transaction, using the sim resolution function.
- Resolution is idempotent: a dispatch moves from in_flight to resolved exactly once, guarded by a conditional update on status.
- Resolved dispatches with no acknowledged-at time form the "while you were away" summary.
- Test: Seed a dispatch that arrived in the past; fetching state applies credits, XP, ship location, and discovery. Fetching again does not apply them twice. Two concurrent fetches apply them exactly once.
Step 6.2 — Get full player state
- Return company credits, ships (with location or active dispatch), pilots (XP, level, skills, assignment, status), bays, discovered locations, tutorial flag, and unacknowledged summaries.
- Test: For a seeded player with one ship in flight and one idle, the response reflects both correctly and validates against its schema.
Step 6.3 — Tutorial board endpoint
- Return the tutorial board for the current period and the loaner's current location, stored on first request per (period, location) (decision 0.5.21), excluding contracts this player has already accepted. Only available while tutorial mode is on.
- Test: A tutorial player at Earth gets a board of the configured size, all Earth→Moon. A non-tutorial player gets an error. Accepted contracts disappear from the board.
- Amended by CR-001: Board size 3, v2 generator, stored per run as well as per period and location (Step 10A.7).
Step 6.4 — Tutorial dispatch
- Allow a tutorial player to dispatch their avatar pilot with the company loaner Courier on a tutorial contract. The loaner is not owned and cannot be used for anything else. Only one tutorial dispatch may be in flight at a time.
- Test: A tutorial dispatch succeeds, charges no fuel, and creates an in-flight dispatch with the correct arrival time. A second tutorial dispatch while one is in flight is rejected. After arrival, state shows credits increased by exactly the tutorial share, pilot XP increased, and the loaner and avatar at the Moon; the next tutorial board is Moon→Earth.
- Amended by CR-001: Each resolved tutorial run increments the runs-completed counter (Step 10A.7).
Step 6.5 — Buy a ship
- Allow buying any ship model if the player has enough credits. A new ship appears idle at Earth with no pilot assigned.
- Test: Buying with enough credits deducts the price and adds the ship. Buying without enough credits returns the insufficient-funds error and changes nothing.
- Amended by CR-001: Blocked during the tutorial with
tutorial-active(Step 10A.7, decision 0.5.16 as replaced). - Amended by CR-002:
buyShiptakes abayId; the ship appears at that bay's location. No bay id is a validation error; another player's bay isnot-found(Step 10B.6, decision 0.5.35).
Step 6.6 — Lease a bay
- Allow leasing a bay at any discovered location where the player does not already have one, for that location's one-time lease price.
- Test: Leasing at Earth succeeds and deducts the price. Leasing at an undiscovered location fails. Leasing a second bay at the same location fails.
- Amended by CR-001: Blocked during the tutorial with
tutorial-active(Step 10A.7).
Step 6.7 — End tutorial
- When the player owns at least one ship, has a bay at Earth, and has no tutorial run in flight, tutorial mode turns off automatically. The loaner is removed and the avatar moves to Earth as a normal, unassigned pilot.
- Test: With the avatar at the Moon, buying a ship and leasing an Earth bay turns tutorial mode off, removes the loaner, and leaves the avatar unassigned at Earth. If a tutorial run is in flight when the conditions are met, the tutorial ends when that run resolves. Afterward, the tutorial board endpoint returns an error.
- Amended by CR-001: Replaced by Step 10A.8 (accepting the deal ends the tutorial; the automatic rule is removed).
Step 6.8 — Hire and assign pilots
- Hiring a pilot costs the configured fee and creates a level-0 pilot with a generated name (from the player seed) at a player bay the player chooses. Pilots can be assigned to an idle, uncrewed ship at the same location. Unassigning is allowed only at a location with a player bay. One pilot per ship (decision 0.5.2).
- Test: Hiring at the Moon bay deducts credits and places the pilot at the Moon; hiring at a location without a bay fails. Assigning to an idle ship at the same location succeeds; assigning to a ship at another location, to a crewed ship, or while in flight fails. Unassigning at a location without a bay fails.
- Amended by CR-001: Hiring is blocked during the tutorial with
tutorial-active(Step 10A.7).
Step 6.9 — Bay contract board endpoint
- Return the contract board for one of the player's bays for the current period. On the first request in a period, generate it with the sim using the player's discovered locations at that moment and store it in contract_boards; later requests in the period return the stored board. Exclude already-accepted contracts. Contracts from earlier periods are never shown.
- Test: A bay board is returned for an owned bay. Requesting a board for a location without a player bay fails. Two requests in the same period return the same board, even if a location is discovered between them. After the period rolls over, a new board is returned, and dispatching a contract from the old board fails as expired.
- Amended by CR-002:
GET /api/locations/:locationId/boardserves any discovered location, no bay needed; undiscovered islocation-undiscovered(Step 10B.4, decision 0.5.36). - Amended by CR-006: The endpoint passes the player's fleet (ships and assigned pilots' skills) to board generation (Step 10F.4).
Step 6.10 — Contract dispatch
- Dispatching a contract runs sim validation, deducts fuel cost, marks the contract accepted, and stores the dispatch record. Ship and pilot become in-flight.
- Test: One integration test per sim validation failure confirms the matching error code is returned and nothing changes. A valid dispatch deducts fuel, stores the record, and makes the contract disappear from the board.
- Amended by CR-002: No bay checks: any idle ship can take a contract from its current location's board (Step 10B.5).
Step 6.11 — Reposition dispatch
- Dispatching with no contract to a discovered location with a player bay deducts fuel and moves the ship and pilot with no payout.
- Test: A reposition succeeds and, after arrival, the ship is at the new location with credits reduced only by fuel.
- Amended by CR-002: Deadhead rules (Step 3.14 as amended); bays play no part (Step 10B.5).
Step 6.12 — Spend skill points
- Allow spending an available skill point on a skill below max level.
- Test: Spending increases the skill by one and reduces available points by one. Spending with no points or on a maxed skill fails.
Step 6.13 — Acknowledge summaries
- Allow the client to mark "while you were away" summaries as seen.
- Test: After acknowledging, state returns no unacknowledged summaries; resolved effects remain.
Step 6.14 — Concurrency and money safety
- All endpoints that change credits or ship/pilot state run in transactions with row locks on the company.
- Test: Firing two simultaneous buy-ship requests where the player can afford only one results in exactly one purchase and no negative balance.
Phase 7 — Background Workers
Step 7.1 — pg-boss setup
- Add pg-boss to the server, running workers in the same process for now, with a setting to disable workers.
- Test: An integration test enqueues a no-op job and observes it complete.
Step 7.2 — Arrival resolution job
- When a dispatch is created, schedule a job at its arrival time that runs the same resolution logic as Step 6.1 for that player.
- The server reads time only through an injectable clock (decision 0.5.18). Add a test-only clock-offset endpoint for E2E tests; it must not be registered in production mode.
- Test: With the clock controlled in tests, a dispatch's job resolves it at arrival without any player state fetch. If lazy resolution already happened, the job does nothing. In production mode, the clock-offset endpoint returns not-found.
Step 7.3 — Expired contract cleanup
- Add a periodic job that removes stored contract boards and accepted-contract records from periods older than a retention window.
- Test: Old boards and records are deleted; current-period ones remain.
Phase 8 — Client Foundation
Step 8.1 — Vite + React app
- Create the React app in
apps/webwith Tailwind, a basic layout (header with credits and game clock, main area, bottom navigation for mobile), and routing for: HQ, Contracts, Fleet, Pilots, Market, Locations. - Test: A component test renders the layout and each route shows its placeholder title. Manual check: the layout fits a 375px-wide viewport with no horizontal scroll.
- Amended by CR-005: The header becomes a HUD bar and navigation becomes icon tabs: a bottom bar on phones, a left rail from 768 px, with attention badges (decision 0.5.49, Step 10E.3).
- Amended by CR-011: The header gains a Settings gear (Step 10K.2).
Step 8.2 — Typed API client
- Build an API client from the route catalog so every call is typed and responses are validated.
- Test: Unit tests with a mocked fetch confirm a valid response is returned typed and an invalid response raises a validation error.
Step 8.3 — Server-synced clock
- On startup, fetch server time and store the offset. Provide a "now" helper that returns device time plus offset, and a ticking store that updates every second.
- Test: With a mocked server time 10 minutes ahead of the device, the "now" helper returns a time 10 minutes ahead.
Step 8.4 — Guest onboarding
- On first launch with no session, show a start screen with a "Start your company" button that creates a guest account. Persist the session.
- Test: Component test: clicking the button calls create-guest and navigates to HQ. Reloading with a stored session skips the start screen.
- Amended by CR-001: After creating the guest, the client lands on Contracts, not HQ (Step 10A.11).
- Amended by CR-011: The start screen gains "Restore a company" under "Start your company" (Step 10K.9).
Step 8.5 — Player state store
- Use TanStack Query to load player state, refetching when the app regains focus and when any in-flight dispatch's arrival time passes.
- Test: With mocked API, when the clock passes an arrival time, a refetch is triggered automatically.
Phase 9 — Client Screens
Step 9.1 — HQ screen
- Show credits, game date/time, counts of ships idle/in flight, and the next arrival with a countdown. During tutorial, show a clear goal: "Save up for your first ship and an Earth bay," with progress toward the combined cost.
- Test: Component test with seeded state shows correct counts and countdown. Tutorial goal progress shows the correct percentage.
- Amended by CR-001: The tutorial savings goal is replaced by tutorial run progress, the loan card and goals (Steps 10A.14, 10A.15).
Step 9.2 — "While you were away" summary
- When state contains unacknowledged summaries, show a modal listing each completed delivery (ship, route, credits, XP, discoveries). Dismissing acknowledges them.
- Test: Component test shows the modal with two seeded summaries; dismissing calls acknowledge and hides it.
Step 9.3 — Tutorial contract board
- During tutorial, the Contracts screen shows the tutorial board: route, cargo, payout, estimated trip time (computed with sim), and a "Fly it" button. The button is disabled while a tutorial run is in flight.
- Test: Component test shows the board with trip times matching the sim. "Fly it" calls dispatch; it is disabled while a run is in flight.
- Amended by CR-001: Each tutorial contract shows pay, trip time and XP; scripted contracts are styled (Step 10A.12).
Step 9.4 — Bay contract boards
- After tutorial, the Contracts screen has a selector for the player's bays and shows each bay's board. Each contract shows route, cargo type, mass, distance, payout, required skill, expiry countdown, and a first-contact badge where applicable.
- Test: Component test switches bays and shows the right board. A first-contact contract displays its badge.
- Amended by CR-002: The selector lists every discovered location with an idle-ship badge, defaulting to the most recently arrived idle ship's location, else Earth; Fleet gets "Find work here" (Step 10B.7).
- Amended by CR-006: A contract's mass is highlighted, with a "Too heavy" tag, when none of the player's ships at the location (or in the fleet, when none are there) can carry it (decision 0.5.53, Step 10F.1).
- Amended by CR-007: Cards headline the good with class/tier, Priority and Too slow chips, and show shipper · mass · distance (Step 10G.8).
- Amended by CR-008: When a location's board changes, the screen says "New contracts posted" and tags contracts not on the last board seen there "New" (Step 10H.3).
Step 9.5 — Dispatch flow
- Selecting a contract opens a dispatch sheet: choose an eligible ship+pilot pair at the origin. Show trip time, fuel cost, net profit, and ETA in both game time and real time. Ineligible pairs are listed but disabled with the reason shown (out of range, can't land, skill too low, too heavy).
- Confirm sends the dispatch; on error, show the server's message.
- Test: Component test: an eligible pair shows correct numbers from the sim; an ineligible pair shows the correct reason; confirming calls dispatch; a server error message is displayed.
- Amended by CR-004: The dispatch sheet disables resting pilots with a countdown and shows duty after the trip, warning "Will need rest on arrival" (Step 10D.7).
- Amended by CR-007: For a Priority contract each pair shows its trip against the window; too-slow pairs are disabled with the numbers (Step 10G.9).
- Amended by CR-009: The dispatch sheet uses effective stats and lists "In the shop — mm:ss" for ships in the shop (Step 10I.7).
- Amended by CR-010: The dispatch sheet shows a "Pilot share" line for hired pilots, and net profit accounts for it (Step 10J.6).
Step 9.6 — Fleet screen
- List all ships: name, model, location or route, assigned pilot, and for in-flight ships a progress bar and countdown. Idle ships at a bay location have a "Reposition" action to another bay location.
- Test: Component test: an in-flight ship's progress bar matches sim progress for the mocked time. Reposition opens a destination picker listing only bay locations.
- Amended by CR-002: "Reposition" becomes "Deadhead (fly empty)" for every idle ship, with fuel, time and disabled-with-reason destinations (Step 10B.8).
- Amended by CR-009: Fleet cards open the upgrade shop and show "In the shop — mm:ss"; the deadhead picker uses effective stats (Steps 10I.6, 10I.7).
Step 9.7 — Pilots screen
- List pilots: name, level, XP progress to next level, skill levels, available skill points, assignment, status. Actions: hire pilot (shows cost), assign to ship, spend skill point.
- Test: Component test: spending a point calls the endpoint and the UI updates; hire is disabled with insufficient credits; assign lists only idle ships at the pilot's location.
- Amended by CR-004: Pilot cards show a duty bar or a rest countdown, and "Rest now" with a confirmation (Step 10D.6).
Step 9.8 — Market screen
- List the three ship models with stats (speed, capacity, range, landing class, price). "Buy" is disabled with insufficient credits.
- Test: Component test: buy is disabled when credits are below price; buying calls the endpoint and the new ship appears in Fleet.
- Amended by CR-002: Buy opens a "Deliver to which HQ?" bay picker, preselected with one bay (Step 10B.9).
- Amended by CR-009: The Market lists the player's upgraded ships of each model with effective stats (Step 10I.7).
- Amended by CR-012: Each model shows its role line and a Reach line computed with the sim ("From Earth now: … · Other dates: … · By hopping: …") (Step 10L.7).
Step 9.9 — Locations screen
- List all locations. Discovered ones show type, landing requirement, current distance from Earth, produces/demands, and a "Lease bay" action with price (or "Bay owned"). Undiscovered ones show only the name and "Undiscovered — deliver here to unlock."
- Test: Component test: undiscovered locations hide details; leasing is available only for discovered locations without a bay.
- Amended by CR-002: Every discovered location shows "View contracts"; the lease action explains "Open an HQ here: hire pilots and take delivery of new ships at this location." (Step 10B.9).
Step 9.10 — Empty and error states
- Every screen has a sensible empty state (e.g., "No ships yet — visit the Market") and a retry option on network failure.
- Test: Component tests for each screen's empty state and network-error state.
Phase 10 — Simple System Map
Step 10.1 — PixiJS map component
- Add a Map route with a PixiJS canvas showing the Sun and the five locations at their current positions. Use a compressed (non-linear) radial scale so inner and outer locations are both visible. Support pan and pinch-zoom.
- Test: Unit test confirms the scale function is monotonic (farther in reality → farther on screen). Manual check: all five locations are visible on a phone-sized screen at default zoom; pinch-zoom works on a touch device.
- Amended by CR-003: Bodies use sprites with a roughly constant screen size (zoom^0.35, clamped 0.75–1.75×) and the Moon at a fixed screen offset from Earth (Step 10C.6, decisions 0.5.38–0.5.39).
Step 10.2 — Ships on the map
- Draw in-flight ships as markers along their route lines, positioned with sim interpolation every frame, and idle ships as badges at their location.
- Test: Unit test: marker screen position for a mocked dispatch at 50% progress lies on the route line between the two endpoints. Manual check: ships move smoothly.
- Amended by CR-003: In-flight ships use their model's sprite rotated to face their direction of travel, nudged apart when crowded; idle ships are shown only as count badges (Steps 10C.7, 10C.8).
Step 10.3 — Map interactions
- Tapping a location opens a panel with its details and a link to its contract board (if a bay exists). Tapping a ship shows its route, pilot, and ETA.
- Test: Component test with simulated taps opens the correct panels.
- Amended by CR-002: The location panel links every discovered location to "View contracts", with or without a bay (Step 10B.9).
- Amended by CR-003: Every tap target is at least 44 px, hit-tested against the same screen layout the renderer draws (Step 10C.6).
Phase 10A — Faster Start (CR-001)
Steps 10A.1–10A.18 are specified in memory-bank/change-requests/CR-001-faster-start.md §5, with its §6 amendments to earlier steps. Done before Phase 11; stop for review at the end.
Phase 10B — Open Markets, Bays as HQs (CR-002)
Steps 10B.1–10B.10 are specified in memory-bank/change-requests/CR-002-open-markets.md §4, with its §5 amendments to earlier steps. Done after Phase 10A and before Phase 11; stop for review at the end.
Phase 10C — Map Art (CR-003)
Steps 10C.1–10C.10 are specified in memory-bank/change-requests/CR-003-map-art.md §5, with its §6 amendments to earlier steps. Done after Phase 10B and before Phase 11; stop for review at the end.
Phase 10D — Pilot Duty and Rest (CR-004)
Steps 10D.1–10D.10 are specified in memory-bank/change-requests/CR-004-pilot-duty-rest.md §5, with its §6 amendments to earlier steps. Done after Phase 10C and before Phase 11; stop for review at the end.
Phase 10E — UI Overhaul (CR-005)
Steps 10E.1–10E.9 are specified in memory-bank/change-requests/CR-005-ui-overhaul.md §5, with its §6 amendments to earlier steps. Done after Phase 10D and before Phase 11; stop for review at the end.
Phase 10F — Boards That Fit Your Fleet (CR-006)
Steps 10F.1–10F.5 are specified in memory-bank/change-requests/CR-006-boards-that-fit.md §5, with its §6 amendments to earlier steps. Done after Phase 10E and before Phase 11; stop for review at the end.
Phase 10G — Cargo Goods, Priority Contracts and Shippers (CR-007)
Steps 10G.1–10G.11 are specified in memory-bank/change-requests/CR-007-cargo-goods.md §6 (written there as 10E.1–10E.11), with its §7 amendments to earlier steps. Done after Phase 10F and before Phase 11; stop for review at the end.
Phase 10H — Boards That Feel Fresh (CR-008)
Steps 10H.1–10H.4 are specified in memory-bank/change-requests/CR-008-fresh-boards.md §3. Done after Phase 10G and before Phase 11; stop for review at the end.
Phase 10I — Ship Upgrades (CR-009)
Steps 10I.1–10I.9 are specified in memory-bank/change-requests/CR-009-ship-upgrades.md §5 (written there as 10F.1–10F.9), with its §6 amendments to earlier steps. Done after Phase 10H and before Phase 11; stop for review at the end.
Phase 10J — Veteran Pilot and Profit-Share Pay (CR-010)
Steps 10J.1–10J.7 are specified in memory-bank/change-requests/CR-010-veteran-pilot-profit-share.md §5 (written there as 10G.1–10G.7), with its §6 amendments. Done after Phase 10I and before Phase 11; stop for review at the end.
Phase 10K — Settings Screen and Company Backup (CR-011)
Steps 10K.1–10K.12 are specified in memory-bank/change-requests/CR-011-settings-backup.md §5 (written there as 10H.1–10H.12), with its §6 amendments. Done after Phase 10J and before Phase 11; stop for review at the end.
Phase 10L — Lighter and Clipper, Per-Leg Range, Gravity-Well Range (CR-012)
Steps 10L.1–10L.11 are specified in memory-bank/change-requests/CR-012-lighter-clipper-range.md §6 (written there as 10I.1–10I.11), with its §7 amendments. Done after Phase 10K and before Phase 11; stop for review at the end.
Phase 10M — The Veteran's Clipper (CR-013)
Steps 10M.1–10M.5 are specified in memory-bank/change-requests/CR-013-veterans-clipper.md §5 (written there as 10J.1–10J.5), with its §6 amendments. Done after Phase 10L and before Phase 11; stop for review at the end.
Phase 11 — PWA
Step 11.1 — Installable PWA
- Add
vite-plugin-pwawith a manifest (name "Deadhead: Space Freight Tycoon", icons, theme color, standalone display) and a service worker that caches the app shell. - Test: A Lighthouse PWA audit in CI reports the app as installable. Manual check: the app installs to a phone home screen and launches standalone.
- Amended by CR-003: The service worker precaches the generated map assets in
public/assets/map-gen/(not the source art inpublic/assets/map/, which the build omits). - Amended by CR-005: The self-hosted font files (
@fontsourceChakra Petch and Barlow, bundled intodist/assets) are part of the offline cache.
Step 11.2 — Offline shell behavior
- When offline, the app loads from cache and shows the last known state with a clear "Offline — showing last update" banner. In-flight progress bars keep advancing from cached dispatch records. Actions are disabled until back online.
- Test: Playwright test with network disabled after first load: the app renders, the offline banner shows, progress bars advance, and action buttons are disabled.
- Amended by CR-011: Settings that need the server (rename, backup, restore, new company, developer tools) are disabled while offline; display and help settings still work.
Phase 12 — Balance Harness and End-to-End Tests
Step 12.1 — Balance simulation script
- Add a script in
packages/simthat simulates a player making reasonable choices (always take the highest-paying eligible contract, buy the cheapest ship when affordable, lease Earth bay, then hire pilots) over a configurable span of real time, using only sim functions. - Output: real time to finish the tutorial, real time to own 3 ships, credits over time, and pilot levels over time.
- Test: Running the script with a fixed seed produces identical output twice. With default content, tutorial completion falls between 45 and 120 real minutes of active play. (Adjust content values, not the test thresholds, if it fails; record changes in
DECISIONS.md.) - Amended by CR-001: Change the thresholds to: deal accepted under 10 real minutes; second ship bought between 30 and 45 minutes (changed by CR-010; was: first pilot hired); first discovery under 90 minutes; and a new check that after the deal the player always either has a ship in flight or can afford fuel for at least one contract. The simulated player accepts the deal as soon as it is available, takes the highest credits-per-minute contract, and pays down the loan only through automatic repayment. Adjust content values, not thresholds.
- Amended by CR-002: The simulated player picks up contracts at whatever location its ships are at, and only deadheads when no eligible contract exists there. Add a check: across the whole run, no ship is ever idle with no eligible contract and no reachable deadhead destination.
- Amended by CR-004: The simulated player follows rest rules: it rests a pilot when no eligible contract exists at its location and deadheading isn't better. Re-check all CR-001 timing targets with rest in place; adjust content values, not thresholds. Add a check that no pilot is ever dispatched while resting.
- Amended by CR-007: The simulated player considers Priority contracts and skips ones its ships are too slow for. Re-check all timing targets; adjust content values, not thresholds.
- Amended by CR-009: The simulated player may buy upgrades: when it can't yet afford its next planned ship, it buys the cheapest available upgrade on its most-used ship, preferring cargo bay and engines. Re-check all timing targets; adjust content values, not thresholds.
- Amended by CR-010: Replace "first pilot hired between 30 and 45 minutes" with "second ship bought between 30 and 45 minutes". Add a check that the veteran flies at least one contract in the first session. Re-check all targets with profit-share pay; adjust content values, not thresholds.
- Amended by CR-012: The script runs two second-ship policies, Lighter-first and Clipper-first; both must reach "own three ships" within 25 % of each other's time. (Replaced by CR-013, below.)
- Amended by CR-013: The player starts with two ships (Courier and Clipper) and a 66,600 cr loan; re-check every timing target with them. Targets: first Market purchase 45–75 minutes, first discovery under 90 minutes, loan paid off in under 4 hours of active play. Run two first-purchase policies, Lighter-first and Freighter-first; both must reach "own four ships" within 25 % of each other's time.
Step 12.2 — Revisit the tutorial economy test
- Replace the placeholder average in the Step 2.5 test with the average tutorial payout measured by the balance script.
- Test: The updated Step 2.5 test passes with real sim numbers.
- Amended by CR-001: Replaced: use the balance script's measured numbers in the new Step 2.5 test.
Step 12.3 — End-to-end: tutorial to first ship
- Playwright test against a real server and test database, with the server clock controllable via a test-only setting: start as guest → fly tutorial runs (advancing the clock) until affordable → buy a Courier → lease Earth bay → tutorial ends.
- Test: The E2E test passes and final state shows one owned ship, one Earth bay, and tutorial mode off.
- Amended by CR-001: Rewritten: guest → four tutorial runs → accept deal → state shows one owned Courier at Earth crewed by the avatar, one Earth bay, a loan balance, and tutorial off → one contract dispatched and resolved with a repayment applied.
- Amended by CR-011: End-to-end tests (12.3–12.6) may use "Start a new company" and the developer tools (advance clock, add credits, finish flights) to reset and fast-forward.
Step 12.4 — End-to-end: offline progress
- Playwright test: dispatch a contract, close the page, advance the server clock past arrival, reopen.
- Test: The "while you were away" modal shows the delivery, credits reflect the payout, and the ship is at the destination.
Step 12.5 — End-to-end: discovery
- Playwright test: take a first-contact contract to Mars with an eligible ship and pilot, advance the clock past arrival.
- Test: Mars shows as discovered on the Locations screen and can now be leased.
- Amended by CR-002: After Mars is discovered, also confirm Mars's contract board is available without a Mars bay.
Step 12.6 — End-to-end: pilot progression
- Playwright test: complete enough deliveries to level up a pilot, spend a point in Heavy Cargo, then confirm a previously ineligible Heavy contract becomes eligible in the dispatch flow.
- Test: The contract's ship/pilot pair changes from disabled ("skill too low") to enabled.
- Amended by CR-004: After leveling Long Haul, confirm the pilot's duty limit increased.
Phase 13 — Deployment (Base Game)
Step 13.1 — Containerize the server
- Add a Dockerfile for the server and a production compose file with the server and Postgres. Migrations run on startup.
- Test: Building and starting the production compose locally serves the health endpoint and the server time endpoint.
Step 13.2 — Deploy the client
- Serve the client and API from one domain, with the API under
/apibehind a reverse proxy (decision 0.5.20). Build the web app as static files and deploy them. Hosting providers and the backup storage location are chosen at the start of this phase. - Test: Manual check: the deployed site loads on desktop and mobile, a guest account can be created, and a tutorial run can be dispatched.
Step 13.3 — Database backups
- Add a scheduled Postgres dump to off-server storage with a retention policy.
- Test: Restore the latest dump into a fresh database and confirm a known test player's state is intact.
Definition of Done (Base Game)
- All step tests pass in CI.
- A new player can go from guest start to owning multiple ships, bays at several locations, and leveled pilots.
- Closing the app and returning later correctly shows completed deliveries.
- Discovery works for all four non-starting locations.
- The app installs as a PWA on iOS and Android and is usable at phone width.