deadhead-space-freight-tycoon/memory-bank/progress.md

24 KiB
Raw Blame History

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/*), strict tsconfig.base.json extended by each package. pnpm install + pnpm typecheck pass. Repo moved to C:\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 bans Math.random, Date.now and argument-less new Date() in packages/sim and packages/content; verified it fails on a planted Math.random().
  • Step 1.3 — Test runner. Vitest 5 with one project per workspace package (pnpm test). Trivial smoke test in packages/sim/test/smoke.test.ts; 1 passing.
  • Step 1.4 — Continuous integration. Repo pushed to Gitea; .gitea/workflows/ci.yml runs install, typecheck, lint and test on every push (runner homelab-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 -d starts Postgres 18 with deadhead (dev) and deadhead_test databases; connected to both with psql. .env.example lists the server's env vars.

Phase 2 — Content Package

  • Step 2.1 — Content schemas. Zod 4 schemas in packages/content/src/schemas.ts for 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.example into GameConfig.
  • Step 2.6 — Single content bundle. content in src/bundle.ts holds all data plus location, shipModel, cargoType and skill lookups that return undefined for unknown ids.

Phase 3 — Simulation Package

  • Step 3.1 — Game clock conversion. src/clock.ts: realToGame / gameToReal using the config epoch and compression; times are Unix-ms numbers.
  • Step 3.2 — Seeded RNG. src/rng.ts: createRng(seed) (mulberry32) with next, int, pick; deriveSeed(parent, label) for child seeds.
  • Step 3.3 — Location positions. src/positions.ts: heliocentric ecliptic positions (million km) from astronomy-engine for 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: checkRoute returns ok or cannot-land / out-of-range; canLandAt ranks vacuum < atmospheric < hardened. Step 2.3 ranges confirmed against sampled real distances.
  • Step 3.8 — Contract model and payout. src/contract.ts: Contract interface and contractPayout (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), plus boardPeriodIndex, requiredLongHaulLevel and the SimContent input 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. SimContent gained shipModels.
  • Step 3.11 — Pilot eligibility. src/pilot.ts: checkPilotEligibility(skills, contract) returns ok or cargo-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. createDispatchRecord in src/dispatch.ts: DispatchRecord with arrival from travel time, fuel cost, departure distance, a tutorial flag and a per-dispatch seed.
  • Step 3.14 — Reposition dispatch. validateReposition and createRepositionRecord in src/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) and dispatchPosition (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: resolveDispatch returns in-flight or a Resolution (credits after pilot pay, XP, new ship location, discovery).
  • Step 3.18 — Public API and coverage. src/index.ts exports an explicit list (content lookups and contract building stay internal). pnpm coverage (V8, packages/sim/src, 90% line threshold) reports 100% lines; CI now runs pnpm coverage instead of pnpm 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 exports DISPATCH_FAILURES / REPOSITION_FAILURES. architecture.md started with the API data model.
  • Step 4.2 — API route catalog. packages/schema/src/routes.ts: 13 typed endpoints under /api with request/response schemas and an auth flag, plus RouteRequest / RouteResponse helper types. Route table added to architecture.md.

Phase 5 — Server Foundation

  • Step 5.1 — Fastify skeleton. apps/server: Zod-validated env config (src/config.ts), buildApp with pino JSON logging and GET /api/health (src/app.ts), entry point src/main.ts (loads repo-root .env if present). pnpm --filter @deadhead/server dev|start run 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/time returns { realTime, gameTime } from an injectable Clock (src/clock.ts, passed via buildApp(config, { clock })).
  • Step 5.3 — Database schema and migrations. Drizzle schema (apps/server/src/db/schema.ts, copied into architecture.md), first migration drizzle/0000_init.sql, db:generate / db:migrate scripts. 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) and seedPlayer(db, "tutorial" | "owner"). New-player creation lives in src/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 in src/db/auth-schema.ts, migration 0001_auth). POST /api/guest creates a guest session and a tutorial player; GET /api/state returns player state for the session or 401 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 for auth routes, and checks responses outside production. Malformed JSON and unexpected fields return 400 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 inside withPlayerLock (src/game/lock.ts, a transaction holding the company row lock); a conditional status update makes each resolve exactly once. GET /api/state runs it before reading. Routes moved to src/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-tutorial after the tutorial. Every game route now runs through withPlayer (lock + lazy resolution).
  • Step 6.4 — Tutorial dispatch. POST /api/dispatches with kind: "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-funds changes nothing.
  • Step 6.6 — Lease a bay. POST /api/bays (leaseBay in src/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 code avatar-in-tutorial.
  • Step 6.9 — Bay contract board endpoint. GET /api/bays/:locationId/board (bayBoard in src/game/boards.ts): generated from the discovered set on first request in a period and stored; accepted contracts hidden; 409 no-bay-at-location without a bay. The expired-old-board dispatch check is tested in Step 6.10.
  • Step 6.10 — Contract dispatch. POST /api/dispatches with kind: "contract" (dispatchContract): sim validation, fuel deducted, contract accepted, ship and pilot in flight. One integration test per sim reason (cannot-land via 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/dispatches with kind: "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) sets acknowledged_at on 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: JobQueue interface, pg-boss implementation (queues always created; workers and cron schedules only when WORKERS_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-arrivals job 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-only POST /api/test/clock-offset (not registered in production) moves it.
  • Step 7.3 — Expired contract cleanup. Hourly cleanup-expired-contracts job (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 /api to 127.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 as ApiError with code and message.
  • Step 8.3 — Server-synced clock. apps/web/src/clock: Zustand useClock store, syncClock(api) (offset from server time, half round trip), serverNow(), startClockTicker() (1 s), and a GameClock header component showing in-game time.
  • Step 8.4 — Guest onboarding. No session (state request returns 401) shows StartScreen; "Start your company" calls createGuest, caches the returned state and opens HQ. The session is Better Auth's persistent cookie. TanStack Query and an ApiProvider context 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, via useRefetchOnArrival, 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 calls acknowledgeSummaries.
  • 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 sim validateDispatch; 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 (sim dispatchProgress) 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 in src/map/projection.ts, camera in src/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. purchasable on ShipModel; "Company Courier" (not purchasable, 6M km/h, 0.3 h overhead, 20 t); config: tutorialLoanerShipModelId, tutorialRunsBeforeDeal 4, tutorialBoardSize 3, tutorialMassTonnes 5/12/20, handlingGameHoursPerTonne 0.02, loanRepaymentShare 0.3, dealLoanAmount 5000, startingCredits 500. "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) adds mass × 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 (scriptedId on the contract, at its pay multiplier). Loaner is config.tutorialLoanerShipModelId. Step 3.10 tests replaced. Server passes runsCompleted: 0 until Step 10A.7.
  • Step 10A.4 — Sim: loan repayment. splitLoanRepayment(credits, balance, share) (src/loan.ts); resolveDispatch takes loanBalance and returns loanRepayment with net credits. Server passes a zero balance until Step 10A.9.
  • Step 10A.5 — Sim: onboarding goal evaluation. completedGoals(goals, progress) / isGoalConditionMet (src/goals.ts) over a GoalProgress summary; 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), table claimed_goals; repayment stored in each dispatch's resolution JSON. API: company.loanBalance, tutorial.runsCompleted / dealAvailable, goals, summary loanRepayment / scriptedId; routes acceptDeal, repayLoan, claimGoal; error codes tutorial-active, deal-not-available, unknown-goal, goal-not-ready, goal-already-claimed. architecture.md updated.
  • 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; dealAvailable in state; buying ships, leasing bays and hiring pilots return 409 tutorial-active during 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 (acceptDeal in src/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-available otherwise. Old maybeEndTutorial removed (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-funds with 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/claim adds 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: when tutorial.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 calls acceptDeal, stores the returned state and goes to HQ.
  • Step 10A.14 — Client: loan on HQ. HQ's savings goal is replaced: during the tutorial a "Company runs: n of 4" card; afterwards LoanCard (balance, paid-off bar, "Pay down" with amounts above credits or balance blocked in the UI). Delivery summaries show "X cr went to the lender". Step 9.1 tutorial-goal tests replaced.
  • 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