173 lines
9.1 KiB
Markdown
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.
|