24 KiB
24 KiB
Progress
Phase 1 — Repository Setup
- Step 1.1 — Create the monorepo. pnpm workspace with
apps/web,apps/server,packages/sim,packages/schema,packages/content(all@deadhead/*), stricttsconfig.base.jsonextended by each package.pnpm install+pnpm typecheckpass. Repo moved toC:\Projects\Deadhead Space Freight Tycoon(G: is exFAT, no symlinks). - Step 1.2 — Linting and formatting. ESLint 10 + typescript-eslint + Prettier at the root (
pnpm lint,pnpm format). Purity rule bansMath.random,Date.nowand argument-lessnew Date()inpackages/simandpackages/content; verified it fails on a plantedMath.random(). - Step 1.3 — Test runner. Vitest 5 with one project per workspace package (
pnpm test). Trivial smoke test inpackages/sim/test/smoke.test.ts; 1 passing. - Step 1.4 — Continuous integration. Repo pushed to Gitea;
.gitea/workflows/ci.ymlruns install, typecheck, lint and test on every push (runnerhomelab-runner). Run #1 failed because Prettier scanned the runner's in-workspace.pnpm-store/; ignored it, and run #2 is green. - Step 1.5 — Local Postgres.
docker compose up -dstarts Postgres 18 withdeadhead(dev) anddeadhead_testdatabases; connected to both with psql..env.examplelists the server's env vars.
Phase 2 — Content Package
- Step 2.1 — Content schemas. Zod 4 schemas in
packages/content/src/schemas.tsfor Location, ShipModel, CargoType, Skill and GameConfig, with inferred types. Tests check that each schema accepts a valid example and rejects it with any one field missing. - Step 2.2 — The five locations.
src/data/locations.ts: Earth, Moon, Mars, Ceres Station, Jupiter Cloud Platform. Earth and Moon start discovered; Earth has the highest launch multiplier. - Step 2.3 — The three ship models.
src/data/ships.ts: Courier, Freighter, Bulk Hauler, all atmospheric. Speed, capacity and price orderings tested; range rules are checked in Step 3.7. - Step 2.4 — Cargo types and skills.
src/data/cargo.ts: Standard, Heavy, High-Value, Dangerous; skills Long Haul, Heavy Cargo, Expensive Cargo, Dangerous Cargo (max level 5). The location test now checks cargo ids against the real data. - Step 2.5 — Default game config.
src/data/config.ts. Courier + Earth bay ≈ 21 tutorial runs with the placeholder average payout. Money values rescaled ×10 so tutorial runs pay meaningful amounts; epoch moved from.env.exampleinto GameConfig. - Step 2.6 — Single content bundle.
contentinsrc/bundle.tsholds all data pluslocation,shipModel,cargoTypeandskilllookups that returnundefinedfor unknown ids.
Phase 3 — Simulation Package
- Step 3.1 — Game clock conversion.
src/clock.ts:realToGame/gameToRealusing the config epoch and compression; times are Unix-ms numbers. - Step 3.2 — Seeded RNG.
src/rng.ts:createRng(seed)(mulberry32) withnext,int,pick;deriveSeed(parent, label)for child seeds. - Step 3.3 — Location positions.
src/positions.ts: heliocentric ecliptic positions (million km) fromastronomy-enginefor Earth, Mars and Jupiter; Moon = Earth + fixed offset; Ceres on a circular orbit. - Step 3.4 — Route distance.
src/distance.ts:routeDistance(from, to, gameTime), straight-line million km. - Step 3.5 — Travel time.
src/travel.ts:travelTimeGameHours(ship, distance)= cruise time + overhead. - Step 3.6 — Fuel cost.
src/fuel.ts:fuelCost(ship, origin, distance)= distance × cost per million km × origin launch multiplier. - Step 3.7 — Range and landing eligibility.
src/eligibility.ts:checkRoutereturnsokorcannot-land/out-of-range;canLandAtranks vacuum < atmospheric < hardened. Step 2.3 ranges confirmed against sampled real distances. - Step 3.8 — Contract model and payout.
src/contract.ts:Contractinterface andcontractPayout(base × multiplier^tier × mass × distance × first-contact multiplier, rounded). - Step 3.9 — Contract board generation.
src/board.ts:generateContractBoard(seeded per world, bay and period), plusboardPeriodIndex,requiredLongHaulLeveland theSimContentinput type. First contact ≈10% over 1,000 boards. - Step 3.10 — Tutorial contract board.
src/tutorial.ts:generateTutorialBoard(Earth↔Moon from the loaner's location, Standard cargo capped at Courier capacity, payout × tutorial share);loanerModel.SimContentgainedshipModels. - Step 3.11 — Pilot eligibility.
src/pilot.ts:checkPilotEligibility(skills, contract)returnsokorcargo-skill-too-low/long-haul-too-low. - Step 3.12 — Dispatch validation.
src/dispatch.ts:ShipState,PilotState,validateDispatch(first failing reason, plan order),tripFuelCost(rounded). One test per failure reason plus tutorial cases. - Step 3.13 — Dispatch record creation.
createDispatchRecordinsrc/dispatch.ts:DispatchRecordwith arrival from travel time, fuel cost, departure distance, atutorialflag and a per-dispatch seed. - Step 3.14 — Reposition dispatch.
validateRepositionandcreateRepositionRecordinsrc/dispatch.ts: empty flight to a player bay, range/landing/fuel only, zero payout. Zero XP is covered by resolution (Step 3.17). - Step 3.15 — In-flight position and progress.
src/flight.ts:dispatchProgress(clamped 0–1) anddispatchPosition(straight line from origin at departure to destination at arrival). - Step 3.16 — XP and leveling.
src/xp.ts:deliveryXp,pilotLevel,skillPointsAvailable. - Step 3.17 — Dispatch resolution.
src/resolution.ts:resolveDispatchreturnsin-flightor aResolution(credits after pilot pay, XP, new ship location, discovery). - Step 3.18 — Public API and coverage.
src/index.tsexports an explicit list (content lookups and contract building stay internal).pnpm coverage(V8,packages/sim/src, 90% line threshold) reports 100% lines; CI now runspnpm coverageinstead ofpnpm test.
Phase 4 — Shared Schema Package
- Step 4.1 — API data shapes.
packages/schema/src: entity schemas (entities.ts), error format and codes (errors.ts), request/response bodies (requests.ts). Error codes include every sim validation reason; sim now exportsDISPATCH_FAILURES/REPOSITION_FAILURES.architecture.mdstarted with the API data model. - Step 4.2 — API route catalog.
packages/schema/src/routes.ts: 13 typed endpoints under/apiwith request/response schemas and anauthflag, plusRouteRequest/RouteResponsehelper types. Route table added toarchitecture.md.
Phase 5 — Server Foundation
- Step 5.1 — Fastify skeleton.
apps/server: Zod-validated env config (src/config.ts),buildAppwith pino JSON logging andGET /api/health(src/app.ts), entry pointsrc/main.ts(loads repo-root.envif present).pnpm --filter @deadhead/server dev|startrun it with tsx. Integration test starts the app on an ephemeral port and calls health over HTTP. - Step 5.2 — Server time endpoint.
GET /api/timereturns{ realTime, gameTime }from an injectableClock(src/clock.ts, passed viabuildApp(config, { clock })). - Step 5.3 — Database schema and migrations. Drizzle schema (
apps/server/src/db/schema.ts, copied intoarchitecture.md), first migrationdrizzle/0000_init.sql,db:generate/db:migratescripts. Migration test runs against an emptied test database twice. CI now has a Postgres 18 service; server tests run one file at a time. - Step 5.4 — Test database harness.
apps/server/test/db.ts:useTestDatabase()(migrate once, truncate all tables before each test) andseedPlayer(db, "tutorial" | "owner"). New-player creation lives insrc/game/new-player.ts(createPlayer), shared with guest sign-up. - Step 5.5 — Guest accounts. Better Auth with the anonymous plugin (
src/auth.ts, tables insrc/db/auth-schema.ts, migration0001_auth).POST /api/guestcreates a guest session and a tutorial player;GET /api/statereturns player state for the session or401 unauthorized. Error handler sends the documented error format. - Step 5.6 — Request validation from the route catalog.
registerRoute(src/http/route.ts) validates merged params/query/body before auth, checks sessions forauthroutes, and checks responses outside production. Malformed JSON and unexpected fields return400 validation-error.
Phase 6 — Server Game Logic
- Step 6.1 — Lazy resolution on read.
resolveArrivedDispatches(src/game/resolve.ts) resolves arrived dispatches in arrival order insidewithPlayerLock(src/game/lock.ts, a transaction holding the company row lock); a conditional status update makes each resolve exactly once.GET /api/stateruns it before reading. Routes moved tosrc/routes/. - Step 6.2 — Get full player state. Endpoint from Step 5.5 plus lazy resolution; test covers a player with one ship in flight and one idle (crewed by a hired pilot), checking ships, pilots (XP, level, skills, points, assignment, status, location), bays, dispatches, summaries and schema validity.
- Step 6.3 — Tutorial board endpoint.
GET /api/tutorial/board(src/routes/boards.ts,src/game/boards.ts): stored per (period, loaner location) on first request, accepted contracts hidden,409 not-in-tutorialafter the tutorial. Every game route now runs throughwithPlayer(lock + lazy resolution). - Step 6.4 — Tutorial dispatch.
POST /api/dispatcheswithkind: "tutorial"(src/game/dispatch.ts): avatar + loaner, sim validation, no fuel, one run at a time; contract marked accepted. Arrival pays the stored tutorial payout and moves loaner and avatar. - Step 6.5 — Buy a ship.
POST /api/ships(src/game/fleet.ts,src/routes/fleet.ts): deducts the price, adds an idle uncrewed ship at Earth named from the world seed (src/game/names.ts);409 insufficient-fundschanges nothing. - Step 6.6 — Lease a bay.
POST /api/bays(leaseBayinsrc/game/fleet.ts): one-time lease price at a discovered location;409 location-undiscovered,409 bay-already-leased,409 insufficient-funds,404 not-found. - Step 6.7 — End tutorial.
maybeEndTutorial(src/game/tutorial.ts) runs after each resolution pass and after every game action: with a ship, an Earth bay and no tutorial run in flight, tutorial mode turns off, the loaner is removed, and the avatar becomes an idle, unassigned pilot at Earth. - Step 6.8 — Hire and assign pilots.
POST /api/pilots,/api/pilots/:pilotId/assign,/api/pilots/:pilotId/unassign(src/game/pilots.ts): hire at a bay for the fee with a seeded name; assign to an idle, uncrewed ship at the same location; unassign only at a bay. New error codeavatar-in-tutorial. - Step 6.9 — Bay contract board endpoint.
GET /api/bays/:locationId/board(bayBoardinsrc/game/boards.ts): generated from the discovered set on first request in a period and stored; accepted contracts hidden;409 no-bay-at-locationwithout a bay. The expired-old-board dispatch check is tested in Step 6.10. - Step 6.10 — Contract dispatch.
POST /api/dispatcheswithkind: "contract"(dispatchContract): sim validation, fuel deducted, contract accepted, ship and pilot in flight. One integration test per sim reason (cannot-landvia a test-only vacuum ship in mocked content), each checking nothing changed; also covers the expired-old-board case from Step 6.9. - Step 6.11 — Reposition dispatch.
POST /api/dispatcheswithkind: "reposition"(dispatchReposition): sim reposition validation against the player's bays, fuel only, no payout or XP; ship and pilot arrive at the destination. - Step 6.12 — Spend skill points.
POST /api/pilots/:pilotId/skills(spendSkillPoint): +1 skill level, +1 point spent;409 no-skill-points,409 skill-maxed,404 unknown-skill. - Step 6.13 — Acknowledge summaries.
POST /api/summaries/acknowledge(src/game/summaries.ts) setsacknowledged_aton the player's resolved, unacknowledged dispatches among the given ids and returns how many; resolved effects are untouched. - Step 6.14 — Concurrency and money safety. Every authenticated endpoint runs in
withPlayer(transaction + company row lock). Race tests: two and eight simultaneous purchases, a contract dispatched twice at once, a bay leased twice at once; three of them fail when the lock is removed.
Phase 7 — Background Workers
- Step 7.1 — pg-boss setup.
src/jobs/queue.ts:JobQueueinterface, pg-boss implementation (queues always created; workers and cron schedules only whenWORKERS_ENABLED=true), and a recording fake for tests. The app starts the queue on ready and stops it on close. Test runs a no-op job to completion with real pg-boss. - Step 7.2 — Arrival resolution job. Every dispatch (contract, tutorial, reposition) schedules a
resolve-arrivalsjob at its arrival (src/jobs/definitions.ts), which runs the same locked resolution as lazy reads and is a no-op if already resolved. Clock is now base clock + offset; test-onlyPOST /api/test/clock-offset(not registered in production) moves it. - Step 7.3 — Expired contract cleanup. Hourly
cleanup-expired-contractsjob (src/jobs/cleanup.ts) deletes stored boards and accepted-contract records more than 24 board periods old; current ones stay.
Phase 8 — Client Foundation
- Step 8.1 — Vite + React app.
apps/web: Vite 8, React 19, Tailwind 4, React Router 8. Layout (header with credits and game clock, main area, fixed six-tab bottom navigation) and placeholder screens for HQ, Contracts, Fleet, Pilots, Market, Locations. Component tests in jsdom. Dev server proxies/apito127.0.0.1:3000. Manual 375px check: passed on the user's iPhone. - Step 8.2 — Typed API client.
apps/web/src/api/client.ts:createApiClient().call(name, input)built from the route catalog: path params filled, GET query / POST JSON body,credentials: "include", responses validated (ApiValidationError), server errors raised asApiErrorwith code and message. - Step 8.3 — Server-synced clock.
apps/web/src/clock: ZustanduseClockstore,syncClock(api)(offset from server time, half round trip),serverNow(),startClockTicker()(1 s), and aGameClockheader component showing in-game time. - Step 8.4 — Guest onboarding. No session (state request returns 401) shows
StartScreen; "Start your company" callscreateGuest, caches the returned state and opens HQ. The session is Better Auth's persistent cookie. TanStack Query and anApiProvidercontext added; header shows live credits. Verified end to end through the Vite proxy. - Step 8.5 — Player state store.
usePlayerState(TanStack Query) refetches on window focus and, viauseRefetchOnArrival, once per dispatch when the server clock passes its arrival time.
Phase 9 — Client Screens
- Step 9.1 — HQ screen.
src/screens/HQ.tsx: credits, game date, idle/in-flight counts, next arrival with live countdown, tutorial goal progress (cheapest ship + Earth bay). Shared helpers:src/format.ts,Countdown,useLoadedPlayer/useGameAction(src/state/actions.ts). - Step 9.2 — "While you were away" summary.
src/components/AwaySummary.tsx: dialog over every screen listing each unacknowledged delivery (ship or "Company loaner", route, credits, XP, discovery); "Got it" hides it at once and callsacknowledgeSummaries. - Step 9.3 — Tutorial contract board. Contracts screen shows the tutorial board during the tutorial (
src/screens/TutorialBoard.tsx): route, cargo, mass, payout, sim-predicted trip time (src/game.ts), and "Fly it" (disabled while a run is in flight; server errors shown). - Step 9.4 — Bay contract boards. After the tutorial the Contracts screen shows
BayBoards: a bay selector and, per contract, route, cargo (and tier), mass, current distance, payout, required skills, live expiry countdown and a first-contact badge. - Step 9.5 — Dispatch flow.
src/screens/DispatchSheet.tsx: each idle ship at the pickup with its pilot, checked with simvalidateDispatch; eligible pairs show trip time, fuel, net profit and game/real ETA; ineligible pairs are disabled with a short reason; Confirm sends the dispatch and shows server errors. - Step 9.6 — Fleet screen.
src/screens/Fleet.tsx: ships with model, location or route, pilot; in-flight progress bar (simdispatchProgress) and countdown; "Reposition" for idle crewed ships at a bay opens a picker of the player's other bays. - Step 9.7 — Pilots screen.
src/screens/Pilots.tsx: level, XP progress to next level, skills, points, assignment and status per pilot; hire at a chosen bay (cost shown, disabled when unaffordable); assign to idle uncrewed ships at the pilot's location; "+" to spend a skill point. - Step 9.8 — Market screen.
src/screens/Market.tsx: ship models with speed, capacity, range, landing class and price; Buy disabled when unaffordable; a bought ship shows a confirmation and appears in Fleet after the state refresh. - Step 9.9 — Locations screen.
src/screens/Locations.tsx: discovered locations show type, landing need, current distance from Earth, produces/demands and "Lease bay (price)" or "Bay owned"; undiscovered ones show only the name and "Undiscovered — deliver here to unlock." - Step 9.10 — Empty and error states. First-load failure shows "Could not reach the server" with Retry; a failed refresh keeps the last state with an "Offline — showing last update" banner and Retry; boards have their own Retry. Empty states on HQ, Fleet, Contracts (no bays, empty bay/tutorial board) and Pilots. Tests cover every screen.
Phase 10 — Simple System Map
- Step 10.1 — PixiJS map component. Map tab (
/map, lazy-loaded) with a PixiJS v8 canvas (src/map/MapCanvas.tsx): Sun, orbit rings, five locations (dimmed if undiscovered, ring if a bay), drag to pan, pinch or wheel to zoom, "Reset view". Square-root radial scale and screen projection insrc/map/projection.ts, camera insrc/map/camera.ts. Tests: scale monotonic, locations fit a 375 px map for 12 years, zoom keeps the anchor fixed. Manual phone check pending. - Step 10.2 — Ships on the map.
flightMarker/buildScene(src/map/scene.ts): each in-flight dispatch draws a route line (origin at departure → destination at arrival) with a marker placed by sim progress every frame; idle ships (tutorial loaner included) show as a count badge at their location. Test: marker at 50% lies halfway along the route line on screen. Manual smoothness check pending. - Step 10.3 — Map interactions. Taps (single pointer, under 8 px of movement) are hit-tested (
src/map/hitTest.ts, 22 px reach, ships win ties). A location panel shows details (or "Undiscovered") and, with a bay, a link to/contracts?bay=…which preselects that board; a ship panel shows route, pilot and ETA. Component tests tap through a canvas test double that uses the real scene.
Phase 10A — Faster Start (CR-001)
Spec: memory-bank/change-requests/CR-001-faster-start.md. Decisions recorded as plan §0.5.29–0.5.34.
- Step 10A.1 — Content: new config and loaner ship.
purchasableon ShipModel; "Company Courier" (not purchasable, 6M km/h, 0.3 h overhead, 20 t); config:tutorialLoanerShipModelId,tutorialRunsBeforeDeal4,tutorialBoardSize3,tutorialMassTonnes5/12/20,handlingGameHoursPerTonne0.02,loanRepaymentShare0.3,dealLoanAmount5000,startingCredits500. "Unmarked crates" scripted contract (run 3, ×2) and six onboarding goals (src/data/onboarding.ts). Step 2.5 test replaced per CR §6; Market lists purchasable models only and the server will not sell the loaner. - Step 10A.2 — Sim: handling time.
travelTimeGameHours(ship, distance, massTonnes, config)addsmass × handlingGameHoursPerTonne; dispatch records pass the contract mass (0 for repositions); client predictions pass contract mass. Step 3.5 hand-calculated test updated. - Step 10A.3 — Sim: tutorial board v2.
generateTutorialBoard({ loanerLocationId, runsCompleted, periodIndex }): light/medium/full Standard contracts at the tutorial share; a scripted contract for the upcoming run replaces the medium slot (scriptedIdon the contract, at its pay multiplier). Loaner isconfig.tutorialLoanerShipModelId. Step 3.10 tests replaced. Server passesrunsCompleted: 0until Step 10A.7. - Step 10A.4 — Sim: loan repayment.
splitLoanRepayment(credits, balance, share)(src/loan.ts);resolveDispatchtakesloanBalanceand returnsloanRepaymentwith netcredits. Server passes a zero balance until Step 10A.9. - Step 10A.5 — Sim: onboarding goal evaluation.
completedGoals(goals, progress)/isGoalConditionMet(src/goals.ts) over aGoalProgresssummary; tests check each goal just short of and at its condition. Sim line coverage still 100%. - Step 10A.6 — Schema and database. Migration
0002_cr001_faster_start:companies.loan_balance,players.tutorial_runs_completed/deal_accepted,contract_boards.tutorial_run(in the unique key), tableclaimed_goals; repayment stored in each dispatch's resolution JSON. API:company.loanBalance,tutorial.runsCompleted/dealAvailable,goals, summaryloanRepayment/scriptedId; routesacceptDeal,repayLoan,claimGoal; error codestutorial-active,deal-not-available,unknown-goal,goal-not-ready,goal-already-claimed.architecture.mdupdated. - Step 10A.7 — Server: tutorial flow v2. Tutorial boards come from the v2 generator, stored per run and period; each resolved tutorial run increments
tutorial_runs_completed;dealAvailablein state; buying ships, leasing bays and hiring pilots return409 tutorial-activeduring the tutorial. Tests for 6.5/6.6/6.8/6.14 now start from an owner; old Step 6.7 tests removed (their trigger is now impossible; the rule itself goes in 10A.8). - Step 10A.8 — Server: accept the deal.
POST /api/deal/accept(acceptDealinsrc/game/deal.ts) in one locked transaction: Courier at Earth (named from the seed) crewed by the avatar, Earth bay, loan =dealLoanAmount, tutorial off, loaner removed, deal accepted; returns the new player state.409 deal-not-availableotherwise. OldmaybeEndTutorialremoved (replaces Step 6.7). - Step 10A.9 — Server: loan repayment. Resolution reads the company loan balance, adds only the net payout and lowers the balance by the repayment (stored in the dispatch resolution, shown in summaries).
POST /api/loan/repay(src/game/loan.ts) moves up to the balance from credits to the loan under the player lock;409 insufficient-fundswith no changes if credits fall short. - Step 10A.10 — Server: goals.
src/game/goals.ts:goalProgress(own-ship deliveries, skill points spent, ships, hires, extra discoveries, deal, loan balance) →goalStates(empty during the tutorial) in player state;POST /api/goals/:goalId/claimadds the reward once (goal-not-ready,goal-already-claimed,404 unknown-goal). - Step 10A.11 — Client: first launch goes straight to work. "Start your company" opens Contracts; on the first run the tutorial board shows the Halvorsen intro card and highlights the light contract as "Suggested". Step 8.4 test updated.
- Step 10A.12 — Client: tutorial board shows the tradeoff
- Step 10A.13 — Client: the deal
- Step 10A.14 — Client: loan on HQ
- Step 10A.15 — Client: goals on HQ
- Step 10A.16 — Client: whole map from the start
- Step 10A.17 — Client: something waiting on return
- Step 10A.18 — Plan amendments and regression