138 lines
8.6 KiB
Markdown
138 lines
8.6 KiB
Markdown
# Tech Stack — Deadhead: Space Freight Tycoon
|
|
|
|
Companion to `game-design-document.md`. Updated for CR-014: the game runs entirely on the device; there is no game server.
|
|
|
|
## TL;DR
|
|
|
|
**TypeScript end to end, in one monorepo, running on the player's device.**
|
|
|
|
| Layer | Choice |
|
|
|---|---|
|
|
| Language | TypeScript (client, engine, shared sim) |
|
|
| Monorepo | pnpm workspaces |
|
|
| Client UI | React + Vite |
|
|
| Client state | TanStack Query (engine results) + Zustand (clock, preferences, map camera) |
|
|
| System map rendering | PixiJS |
|
|
| Styling | Tailwind CSS |
|
|
| Game engine | `packages/engine`: the game's commands over one save document, in the app |
|
|
| Save storage | IndexedDB (web); a JSON file via `@capacitor/filesystem` (iOS/Android) |
|
|
| Validation | Zod (route catalog and save document) |
|
|
| Time | Device clock that never runs backward, plus a developer-only offset |
|
|
| Notifications | Local notifications via `@capacitor/local-notifications` (iOS/Android); none on the web |
|
|
| PWA | `vite-plugin-pwa` (Workbox) |
|
|
| Native wrapper | Capacitor (iOS/Android) |
|
|
| Planetary positions | Fixed coordinates in content (CR-015) |
|
|
| Seeded RNG | Small pure-TS PRNG; world seeds and ids from the platform's crypto random source |
|
|
| Testing | Vitest (sim, engine, components), Playwright (end to end) |
|
|
| Deployment | Static files (Caddy on a home server, or any static host) |
|
|
|
|
---
|
|
|
|
## 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 runs in the engine (to decide outcomes) and in the screens (to show ships moving, ETAs, and contract previews). Both use the same `packages/sim`, so they can never disagree. The game started with a server as the authority; because sim already had no I/O, moving everything onto the device (CR-014) was a port rather than a rewrite.
|
|
|
|
## 2. Repo Layout
|
|
|
|
```
|
|
/apps
|
|
/web React + Vite client (also the Capacitor app); runs the engine
|
|
/packages
|
|
/sim Pure, deterministic game rules (no I/O)
|
|
/engine The game's commands over a save document; storage, clock and notifications injected
|
|
/schema Zod schemas: the command catalog (requests, responses, errors) and the save document
|
|
/content Static game data: ships, locations, goods, skills, config
|
|
```
|
|
|
|
### `packages/sim` rules
|
|
- **Pure functions only.** No `Date.now()`, no `Math.random()`, no network, no storage.
|
|
- 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.
|
|
|
|
### `packages/engine` rules
|
|
- Uses storage, the clock and the notification scheduler **only through injected interfaces**, so tests run on in-memory storage and a test clock.
|
|
- One **mutation queue**: commands run one at a time; each loads the save, changes a copy, validates and saves it.
|
|
- Every command has the same request and response schemas as the route catalog in `packages/schema`.
|
|
|
|
### `packages/content`
|
|
Ships, locations, goods, and skills live as typed data files validated by Zod. Balancing is a data edit, not a code change.
|
|
|
|
## 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 at fixed positions from content (scaled/compressed for readability — soft sci-fi).
|
|
- Ships interpolated along routes with `sim` every frame.
|
|
- Pan/zoom with touch support.
|
|
|
|
### State
|
|
- **TanStack Query** caches engine results (player state, boards) behind the same typed `ApiClient` the screens always used; mutations invalidate them.
|
|
- **Zustand** for the ticking clock, device preferences and the map camera.
|
|
|
|
### Time
|
|
`now = max(device time + developer offset, last time seen)`, with `last time seen` kept in the save. Time never runs backward, so setting the clock back can't undo progress; setting it forward to skip waits is accepted (decision 0.5.88).
|
|
|
|
### Resolution loop
|
|
On launch, on resume and every few seconds while open, the app resolves arrivals that have come due and refreshes the screens. Closing the app loses nothing: arrival times are in the save, and the next launch resolves them.
|
|
|
|
### Storage and saves
|
|
- **Web:** IndexedDB, one record for the save plus three rotating backups.
|
|
- **iOS/Android:** a JSON file in the app's data directory (written to a temp file, then renamed), included in iCloud and Android device backups.
|
|
- If the main save won't load, the newest valid backup is used.
|
|
- **Export save** gives the whole save as one text code (gzip + base64url) to copy or share as a file; **Import save** migrates and validates it. That is how a company moves between devices.
|
|
- Each save format change bumps `saveVersion` and ships with a migration.
|
|
|
|
### One tab at a time
|
|
In the browser, only one tab runs the game (Web Locks API); others show "Deadhead is open in another tab" with "Use here".
|
|
|
|
### PWA + Capacitor
|
|
- `vite-plugin-pwa` makes the web build installable and caches it for offline use. The game works the same with or without a network.
|
|
- Capacitor wraps the **same build** for the App Store and Play Store, adding native file storage and local notifications.
|
|
|
|
## 4. Notifications
|
|
|
|
| Platform | Mechanism |
|
|
|---|---|
|
|
| Capacitor iOS / Android | Local notifications (`@capacitor/local-notifications`) |
|
|
| Web / installed PWA | None |
|
|
|
|
The schedule is worked out from the save after every change: one notification per arrival, rest ending and upgrade finishing, with events in the same minute combined ("Deadhead: 2 updates"). It replaces whatever was scheduled before, so changed or cancelled events never leave a stale notification. Permission is asked right after the deal, and Settings has an on/off toggle. No push service is needed because every arrival time is known at dispatch.
|
|
|
|
## 5. Planetary Positions
|
|
|
|
The map is fixed (CR-015, decision 0.5.93): every location has a position in `packages/content` (radius from the Sun in million km and an angle), so a route's distance, travel time and pay never change. Radii are the real mean orbital radii; angles are chosen so the reach table works with clean margins.
|
|
- Child bodies (the Moon) sit at their parent's position moved a small fixed distance straight out from the Sun.
|
|
- The game clock still runs (1 real min = 1 game hour by default) for timers and the displayed in-game date.
|
|
- `astronomy-engine` was used for real ephemeris positions until CR-015; it is gone.
|
|
- For interstellar (late game), real star positions from a static dataset in `packages/content`.
|
|
|
|
## 6. Deployment
|
|
|
|
- **Web:** the static build, served by Caddy (`deploy/`, HTTPS and caching rules) or any static host that falls back to `index.html` for client routes.
|
|
- **Native:** Capacitor builds of the same web build (store release work is a later change request).
|
|
- **No server, database or backups to run:** players' saves live on their devices.
|
|
|
|
## 7. Testing
|
|
|
|
- **Vitest on `packages/sim`** is where most of the value is. Determinism tests: same dispatch + time → same result, every time.
|
|
- **Vitest on `packages/engine`:** every command against in-memory storage and a test clock (the server's integration tests, ported).
|
|
- **Balance simulations:** scripts that run simulated play through `sim` to check payout curves and progression pacing.
|
|
- **Playwright** against the web build alone, fast-forwarding with the developer clock offset; one test checks that the production build loads nothing but its own files.
|
|
|
|
## 8. 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. |
|
|
| A game server (Fastify + Postgres + pg-boss, as first built) | Single-player with outcomes derived from timestamps, so it isn't needed; it added hosting cost, outage risk and accounts (CR-014). |
|
|
| Cloud saves / cross-device sync | Out of scope for now (CR-014 §8); export/import covers moving a company. |
|
|
| SQLite on the device | One validated JSON document is enough at this size and is easy to export, back up and migrate. |
|