deadhead-space-freight-tycoon/memory-bank/tech-stack.md

173 lines
9.1 KiB
Markdown

# Tech Stack — Deadhead: Space Freight Tycoon
Companion to `game-design-document.md`.
## TL;DR
**TypeScript end to end, in one monorepo.**
| Layer | Choice |
|---|---|
| Language | TypeScript (client, server, shared sim) |
| Monorepo | pnpm workspaces |
| Client UI | React + Vite |
| Client state | Zustand (game state) + TanStack Query (server sync) |
| System map rendering | PixiJS (via `@pixi/react`) |
| Styling | Tailwind CSS |
| PWA | `vite-plugin-pwa` (Workbox) |
| Native wrapper | Capacitor (iOS/Android) |
| API server | Node + Fastify |
| Database | PostgreSQL |
| ORM / migrations | Drizzle ORM |
| Validation | Zod (shared schemas, client + server) |
| Auth | Better Auth (self-hosted) — or Supabase Auth if going managed |
| Job queue / scheduler | pg-boss (Postgres-backed, no Redis needed) |
| Push notifications | `web-push` (VAPID) for PWA; FCM/APNs via Capacitor for native |
| Planetary positions | `astronomy-engine` |
| Seeded RNG | Small pure-TS PRNG (e.g., sfc32 / mulberry32) |
| Testing | Vitest (unit/sim), Playwright (E2E) |
| Deployment | Docker containers (API + Postgres), static client on a CDN |
---
## 1. Why TypeScript Everywhere
The single most important architectural decision is the **deterministic dispatch model** (GDD §14): a trip's arrival time, position, and events are all derived from its dispatch record and RNG seed.
That logic has to run in **two places**:
- **Client** — to show ships moving along their routes, display ETAs, and preview a contract before dispatch.
- **Server** — as the authority that resolves payouts, XP, and events.
If both sides run the *same code*, they can never disagree. A shared TypeScript package gives you that for free. With a C# or Python backend you'd be maintaining two implementations of the sim and chasing drift bugs forever.
## 2. Repo Layout
```
/apps
/web React + Vite client (also the Capacitor app)
/server Fastify API, workers
/packages
/sim Pure, deterministic game logic (no I/O)
/schema Zod schemas + shared types (API contracts)
/content Static game data: ships, destinations, goods, skills
```
### `packages/sim` rules
- **Pure functions only.** No `Date.now()`, no `Math.random()`, no network, no DB.
- Time is always passed in as an argument.
- Randomness comes only from a seeded PRNG derived from the dispatch seed.
- Fully unit-testable: given a dispatch record + timestamp, the output is always identical.
Core functions (illustrative):
```ts
travelTime(route, ship, cargo, throttle): Duration
positionAt(dispatch, now): Vec2
rollEvents(dispatch): GameEvent[] // pre-rolled at dispatch time
resolveDispatch(dispatch, now): Resolution // payout, XP, damage, discovery
generateContracts(bay, worldState, seed): Contract[]
```
### `packages/content`
Ships, destinations, goods, and skills live as typed data files (TS or JSON validated by Zod). This makes balancing a data edit, not a code change, and makes the content easy to hot-reload during tuning.
## 3. Client
### React + Vite
The game is overwhelmingly **UI**: contract boards, fleet lists, pilot sheets, bay management. That's what React is good at. A game engine would be fighting you on every menu.
### PixiJS for the system map
The one truly "game-like" view is the solar system map with ships moving along routes. PixiJS gives GPU-accelerated 2D rendering that stays smooth on phones, embedded inside the React app as a single component.
- Planets positioned from `astronomy-engine` (scaled/compressed for readability — soft sci-fi).
- Ships interpolated along routes with `sim.positionAt()` every frame.
- Pan/zoom with touch support.
### State
- **TanStack Query** owns server data (fleet, contracts, company) — caching, refetch on focus, optimistic updates on dispatch.
- **Zustand** for local UI and derived real-time state (current clock, selected ship, map camera).
### Server time on the client
On connect, the client fetches server time and stores the offset. All "now" values used for rendering come from `Date.now() + serverOffset`, so a wrong device clock only affects visuals and is never trusted for outcomes.
### PWA + Capacitor
- `vite-plugin-pwa` makes the web build installable with offline asset caching.
- Capacitor wraps the **same build** for the App Store and Play Store when you want that. It gives more reliable push notifications and native polish without a second codebase.
- Web Push works on installed PWAs on iOS 16.4+, so Capacitor can wait until after launch.
## 4. Server
### Fastify
Fast, TypeScript-friendly, schema-based validation (pairs well with Zod via `fastify-type-provider-zod`). Small surface area.
### API shape
Plain REST/JSON is enough. This isn't a twitch-multiplayer game; nothing needs WebSockets at launch. The client polls/refetches on focus, which is exactly the "pick up the phone, see what happened" pattern.
### Server-authoritative resolution
- **Dispatch:** server validates (ship available, pilot qualified, landing class OK, fuel range OK), stamps departure time, generates the seed, pre-rolls events via `sim`, and stores the record.
- **Resolution:** happens lazily when the player loads state *and* via a scheduled job at arrival time (so notifications fire and the economy stays consistent even if the player never opens the app).
- Resolution must be **idempotent**: a dispatch resolves exactly once, guarded by a status column + transaction.
### PostgreSQL + Drizzle
- Relational data fits this game well: companies, ships, pilots, bays, contracts, dispatches.
- Drizzle is TypeScript-first, light, and its schema can live in `packages/schema` alongside Zod types.
- JSONB columns for flexible bits (pre-rolled event lists, ship loadouts).
### pg-boss for scheduled work
Postgres-backed job queue, so you don't need to run Redis.
- Schedule an "arrival" job per dispatch → resolve + send push notification.
- Periodic jobs: regenerate contract boards, recompute fuel prices, run automation rules (standing routes).
### Auth
- **Better Auth**: self-hosted, TypeScript-native, supports email/password, OAuth (Google/Apple/Discord), and anonymous accounts.
- Strongly recommend **guest accounts upgradable to full accounts** — players can start instantly on mobile and link an account later to sync across devices.
- If you'd rather not run auth yourself, Supabase (Auth + Postgres) is the managed alternative and plays fine with Drizzle.
## 5. Notifications
| Platform | Mechanism |
|---|---|
| Web / installed PWA | Web Push with VAPID keys (`web-push` library) |
| Capacitor iOS | APNs via `@capacitor/push-notifications` |
| Capacitor Android | FCM via `@capacitor/push-notifications` |
Fired from the pg-boss arrival job. Batch notifications when many ships arrive at once ("4 ships arrived — +12,400 cr").
## 6. Planetary Positions
`astronomy-engine` computes real planetary positions for any date, in pure JS, no API calls.
- Run it in `packages/sim` so client and server agree on distances.
- Use a fixed **game epoch** mapped to real ephemeris dates, advanced by the compressed game clock (1 real min = 1 game hour by default).
- Moons and stations inherit their parent body's position plus a small fixed offset — soft sci-fi doesn't need lunar orbits.
- For interstellar (late game), real star positions from a static dataset in `packages/content`.
## 7. Deployment
- **Client:** static build on any CDN (Cloudflare Pages, Netlify, Vercel).
- **Server + Postgres + workers:** Docker containers. Easy to run on a home Docker host during development, then move to a VPS or a managed platform (Fly.io, Railway, Render) for production.
- **Backups:** automated Postgres dumps from day one — player saves *are* the database.
## 8. Testing
- **Vitest on `packages/sim`** is where most of the value is. Determinism tests: same dispatch + time → same result, every time.
- **Balance simulations:** scripts that run thousands of simulated dispatches through `sim` to check payout curves and progression pacing without playing the game.
- **Playwright** for a handful of critical E2E flows (sign up → first job → first ship).
## 9. Alternatives Considered
| Option | Why not (for this game) |
|---|---|
| Unity | Heavy web builds, slow mobile-web load times, painful for UI-heavy screens. |
| Godot | Better than Unity for web, but still a game engine for a game that's mostly menus; larger downloads than a React app. |
| Phaser | Great for action games; the map is the only part that needs it, and PixiJS covers that more lightly. |
| .NET backend | Solid, but loses shared sim code with the client (unless you go all-in on Blazor, which is a weaker mobile web story). |
| Firebase | Fast to start, but deterministic, transactional server-side resolution is awkward in Firestore + Cloud Functions. |
| Redis + BullMQ | Excellent, but an extra service to run when Postgres + pg-boss covers the need at this scale. |
## 10. Build Order
1. `packages/sim` + `packages/content`: travel time, positions, seeded events, resolution. Fully tested, no UI.
2. Server: auth (guest accounts), dispatch + resolve endpoints, Postgres schema.
3. Client: contract board, fleet list, dispatch flow. Plain UI, no map yet.
4. pg-boss arrival jobs + Web Push.
5. PixiJS system map.
6. PWA install, then Capacitor builds.