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. Each tutorial contract shows Pay, Time and XP side by side (time and XP from sim); scripted contracts get their own name and fuchsia styling; their flavor message appears in the delivery summary. Step 9.3 tests updated.
- Step 10A.13 — Client: the deal.
src/components/DealModal.tsx: whentutorial.dealAvailable(and no delivery summary is open), a dialog with the Halvorsen story beat, the Courier and Earth bay, the loan amount and repayment share, and one "Shake on it" button that callsacceptDeal, stores the returned state and goes to HQ. - 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