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

21 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
  • Step 10A.3 — Sim: tutorial board v2
  • Step 10A.4 — Sim: loan repayment
  • Step 10A.5 — Sim: onboarding goal evaluation
  • Step 10A.6 — Schema and database
  • Step 10A.7 — Server: tutorial flow v2
  • Step 10A.8 — Server: accept the deal
  • Step 10A.9 — Server: loan repayment
  • Step 10A.10 — Server: goals
  • Step 10A.11 — Client: first launch goes straight to work
  • 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