280 lines
17 KiB
Markdown
280 lines
17 KiB
Markdown
|
|
# Fog Expedition — Project Context
|
|||
|
|
|
|||
|
|
A Twitch extension that runs an autonomous, tick-based ZPG (zero-player game) inspired by Progress Quest, themed around a Dead by Daylight–style survival fiction. Viewers watch (and lightly direct) a survivor venturing into "the fog" while a streamer plays.
|
|||
|
|
|
|||
|
|
This document is the canonical context for the project. Read it before starting any non-trivial work. AI assistants (Claude Code, Cursor, etc.) should treat this as authoritative — if something here conflicts with assumptions in your training data, this document wins.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Product summary
|
|||
|
|
|
|||
|
|
- **What it is:** A Twitch Video Overlay extension. A small "lantern" sits on the streamer's broadcast; viewers see a survivor's mission unfold as a live log of encounters resolved every 60 seconds.
|
|||
|
|
- **Why it exists:** Ambient stream entertainment. The streamer plays their game; the extension layers a parallel narrative driven by viewer participation without competing for attention.
|
|||
|
|
- **Core loop:** Viewer's survivor enters a mission → server resolves an encounter every 60s using survivor stats + perks + RNG → log line streams to all viewers' overlays → mission ends in success/injury/sacrifice → progression updates persist.
|
|||
|
|
- **Multiplayer:** "SWF" (Survive With Friends) groups of 2–4 survivors run shared missions with team perks and synchronized outcomes.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Architecture overview
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Twitch Video Overlay (panel) NestJS API (EBS)
|
|||
|
|
├── Lantern + ticker (minimised) ├── /missions/* REST endpoints
|
|||
|
|
├── Ambient event card ├── Twitch JWT auth guard
|
|||
|
|
└── Expanded survivor + log card ├── Tick engine (60s heartbeat)
|
|||
|
|
├── Encounter resolver (pure)
|
|||
|
|
├── Group synergy service
|
|||
|
|
└── PubSub publisher
|
|||
|
|
|
|||
|
|
│ │
|
|||
|
|
└────────── HTTPS + JWT ────────────────┘
|
|||
|
|
│
|
|||
|
|
┌─────────────┴─────────────┐
|
|||
|
|
│ │
|
|||
|
|
Postgres Redis
|
|||
|
|
(durable state: (active missions,
|
|||
|
|
users, survivors, lobbies, tick locks,
|
|||
|
|
missions, logs) rate limiting)
|
|||
|
|
│
|
|||
|
|
Twitch Extension PubSub
|
|||
|
|
(server → viewer push)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Stack
|
|||
|
|
|
|||
|
|
- **Monorepo:** Nx workspace, pnpm package manager.
|
|||
|
|
- **Backend:** NestJS (Node 20+, TypeScript). Modules: Auth, Missions, TickEngine, PubSub.
|
|||
|
|
- **Frontend (overlay):** **Reconsider the original Angular choice** — the overlay UI is a single component with three states. Vanilla TypeScript + Lit (or hand-rolled) is likely a better fit. Smaller bundle = easier Twitch review. Decide before starting Stage 2.
|
|||
|
|
- **Datastores:** Postgres (durable), Redis (ephemeral mission state, locks, lobbies).
|
|||
|
|
- **Infra:** Docker Compose for local dev. Devcontainer for editor environment.
|
|||
|
|
- **Shared types:** `@fog-explorer/api-interfaces` library, consumed by both apps. Use Zod schemas, not just TS types — runtime validation matters at the EBS boundary.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Twitch-specific constraints (read carefully — these shape architecture)
|
|||
|
|
|
|||
|
|
### Extension type
|
|||
|
|
|
|||
|
|
This is a **Video Overlay extension**, not a Panel extension. Configure the manifest accordingly. Streamers configure position; default to bottom-left.
|
|||
|
|
|
|||
|
|
### JWT and auth
|
|||
|
|
|
|||
|
|
- Twitch extension JWTs use **HS256** with the base64-decoded shared secret. Not RS256.
|
|||
|
|
- Roles in the JWT: `viewer`, `broadcaster`, `external` (server-to-server). Treat them differently. Reject `broadcaster` for write actions unless explicitly streamer-only.
|
|||
|
|
- `opaque_user_id` starts with `U` for logged-in viewers, `A` for anonymous. Anonymous viewers cannot have persistent survivors — they get a read-only view of the active mission.
|
|||
|
|
|
|||
|
|
### PubSub
|
|||
|
|
|
|||
|
|
- **1 message/sec per channel** for broadcaster messages. **Max 5KB payload.** **No delivery guarantees.**
|
|||
|
|
- Treat PubSub as a "hint to refresh" rather than authoritative state. Viewers who load mid-tick must be able to fetch state via REST.
|
|||
|
|
- Batch all updates for a channel into a single PubSub message per tick.
|
|||
|
|
- Frontend store must reconcile, not just append.
|
|||
|
|
|
|||
|
|
### HTTPS requirement
|
|||
|
|
|
|||
|
|
The EBS must serve HTTPS, even locally. Use Caddy or mkcert + Node TLS for dev.
|
|||
|
|
|
|||
|
|
### Review process
|
|||
|
|
|
|||
|
|
- Twitch reviews every version manually. **1–3 week cycles.** Plan for this; build for the review checklist from day one.
|
|||
|
|
- CSP rules: no inline scripts, explicit allowlist for the EBS domain.
|
|||
|
|
- No fingerprinting. GDPR-compliant handling of obfuscated user IDs.
|
|||
|
|
- Working logged-out viewer experience is mandatory.
|
|||
|
|
- Overlays must not obscure meaningful gameplay pixels. Streamer must have a kill switch.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Design direction
|
|||
|
|
|
|||
|
|
### Aesthetic
|
|||
|
|
|
|||
|
|
"Field journal in the fog." Muted desaturated palette, single warm accent (lantern amber `#E8A547`), cold red for danger (`#C03A3A`). Flat, printed, slightly weathered. No gradients, no drop shadows.
|
|||
|
|
|
|||
|
|
### Typography
|
|||
|
|
|
|||
|
|
- Survivor names: condensed serif or slab (e.g. Cormorant). Grim-tarot feel.
|
|||
|
|
- Live log: monospace (e.g. JetBrains Mono, IBM Plex Mono). Reinforces "transmission from the fog."
|
|||
|
|
- UI chrome: clean sans.
|
|||
|
|
|
|||
|
|
### Three overlay states
|
|||
|
|
|
|||
|
|
**1. Minimised (default, ~95% of viewing time) — ~290×56px**
|
|||
|
|
- Circular lantern badge (~48px), state-coloured ring: amber=active, ochre=injured, red=sacrificed.
|
|||
|
|
- Adjacent ticker showing the **single latest log line** in monospace, ellipsis truncation, no scrolling feed.
|
|||
|
|
- Background: `rgba(15, 18, 22, 0.88)` — opacity ≥80% is non-negotiable for legibility over arbitrary stream content.
|
|||
|
|
|
|||
|
|
**2. Ambient event (~290×92px, ~4s auto-dismiss)**
|
|||
|
|
- Triggers only on significant events: injury, sacrifice, mission complete, perk acquired. **Not every tick.**
|
|||
|
|
- Slides up from the lantern with the event glyph and a one-line description.
|
|||
|
|
- Use sparingly — overuse drives streamers to disable.
|
|||
|
|
|
|||
|
|
**3. Expanded (lantern click) — ~320×440px**
|
|||
|
|
- Survivor card (portrait, name, state, perk slots).
|
|||
|
|
- Mission strip (name, difficulty as 1–3 fog-glyphs, T+N tick counter, draining progress line).
|
|||
|
|
- Live log with progressive opacity (older lines fade).
|
|||
|
|
- Single primary action button at the bottom when relevant; hidden when no action available.
|
|||
|
|
- Backdrop dim/blur behind it to read against any stream content.
|
|||
|
|
|
|||
|
|
### Motion budget
|
|||
|
|
|
|||
|
|
Almost zero. Slow tick-drain animation, fade-in per log line, brief pulse on state changes, summon/unfold for the expanded card. Nothing else. Twitch viewers mute or hide animated panels.
|
|||
|
|
|
|||
|
|
### Accessibility
|
|||
|
|
|
|||
|
|
- Never encode state in colour alone. Vertical bar on survivor card changes shape (solid/dashed/crosshatched) per state. Log line colours are backed by glyphs.
|
|||
|
|
- All custom monochrome glyphs (no emoji). Three glyphs minimum: difficulty pip, injury wound, perk-triggered.
|
|||
|
|
|
|||
|
|
### Group/SWF mode
|
|||
|
|
|
|||
|
|
Expanded card collapses the survivor section to a horizontal row of 4 portraits with state-coloured borders. The currently-resolving survivor highlights when their log line appears. Tap a portrait to expand detail. **Not yet mocked — design before Stage 3 group logic.**
|
|||
|
|
|
|||
|
|
### Streamer config screen
|
|||
|
|
|
|||
|
|
Separate from the viewer overlay. Live preview of the lantern in all four corner positions, one-click positioning, kill switch. Streamers who feel in control keep extensions installed.
|
|||
|
|
|
|||
|
|
### Empty/onboarding states
|
|||
|
|
|
|||
|
|
- **First-time viewer:** moody intro card, fog-shrouded silhouette, "The fog stirs. Awaiting a survivor." + Begin button. Only chance to explain what the extension is before they bounce.
|
|||
|
|
- **Anonymous viewer:** desaturated lantern, no ticker, click to learn more. Don't hide the panel.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Build stages
|
|||
|
|
|
|||
|
|
### Stage 1 — Workspace & infrastructure
|
|||
|
|
|
|||
|
|
- Initialise Nx workspace with TypeScript, `@nx/angular` (or `@nx/js` if dropping Angular), `@nx/nest`, `@nx/node`.
|
|||
|
|
- Generate apps: overlay frontend, NestJS API.
|
|||
|
|
- Generate `@fog-explorer/api-interfaces` lib with Zod schemas for `Survivor`, `SurvivorState`, `Mission`, `MissionState`, `EncounterResult`, `Perk`, `PerkModifier`, `TeamPerk`.
|
|||
|
|
- Configure `@nx/enforce-module-boundaries` with project tags (`scope:shared`, `scope:api`, `scope:overlay`). AI assistants love to reach across libraries with relative imports — enforce early.
|
|||
|
|
- Root `docker-compose.yml`: Postgres (named `fog_expedition`, healthcheck), Redis, optional API service.
|
|||
|
|
- `.devcontainer/` directory with config (see Section 7).
|
|||
|
|
|
|||
|
|
### Stage 2 — Overlay frontend
|
|||
|
|
|
|||
|
|
- Decide framework first (Angular vs Lit vs vanilla). Smaller is better for review.
|
|||
|
|
- `PanelShellComponent` mounting only after `window.Twitch.ext.onAuthorized` fires.
|
|||
|
|
- `LiveLogComponent`, `SurvivorStatusComponent`, lantern + ticker components for the three states.
|
|||
|
|
- `TwitchAuthService`: wraps `onAuthorized`, `onContext`, `onVisibilityChanged`. Stores JWT and parsed payload.
|
|||
|
|
- `EbsApiService`: attaches `Authorization: Bearer <jwt>` to all calls. Uses `@fog-explorer/api-interfaces` types.
|
|||
|
|
- `MissionStateStore`: signals or BehaviorSubject. Pulls initial state from REST, subscribes to PubSub for deltas.
|
|||
|
|
- **Visibility lifecycle:** when overlay is hidden via `onVisibilityChanged`, stop PubSub processing and polling, but retain local state for instant re-expand.
|
|||
|
|
- **Ambient event queue:** if events fire while collapsed, drop them and show current state on expand. Don't replay old drama.
|
|||
|
|
|
|||
|
|
### Stage 3 — Mission engine
|
|||
|
|
|
|||
|
|
- `TickEngineModule` with `TickService` using `@nestjs/schedule`. Global heartbeat per server, but **jitter `nextTickAt` per mission** to avoid thundering herd. Each mission has its own due time; worker picks up due ones.
|
|||
|
|
- Distributed lock pattern: `SET key value NX PX <ttl>` with unique token, verify token before release. Or use Redlock. Naive `SETNX` without TTL = stuck tick on crash.
|
|||
|
|
- **Encounter resolver — pure function** in `libs/mission-logic`. Signature `resolveEncounter(survivor, perks, difficulty, seed) → EncounterResult`. Use seeded PRNG (`seedrandom`), not `Math.random()`. Store seed per tick in `mission_logs` for replay/debug.
|
|||
|
|
- `EncounterService` calls the resolver, updates state, emits log events.
|
|||
|
|
- `GroupSynergyService` aggregates team perks for SWF missions, applies to all relevant rolls.
|
|||
|
|
- `MissionStore` repository abstracting Redis. Keys: `active_mission:{id}`, `mission_lobby:{id}`.
|
|||
|
|
|
|||
|
|
### Stage 4 — EBS persistence
|
|||
|
|
|
|||
|
|
- `MissionsController`: `POST /missions/start`, `GET /missions/state`, `POST /missions/choose-perk`. DTOs from `@fog-explorer/api-interfaces`.
|
|||
|
|
- `TwitchAuthGuard`: HS256 JWT verification, attach `opaqueUserId` and `channelId` to request. Reject expired or wrong-role tokens.
|
|||
|
|
- Postgres schema (TypeORM or Prisma — pick one and stick to it):
|
|||
|
|
- `users` (id, twitch_opaque_user_id, created_at)
|
|||
|
|
- `survivors` (id, user_id FK, stats JSONB, perk_slots JSONB, state, created_at)
|
|||
|
|
- `missions` (id, group_id nullable, difficulty, status, started_at, ended_at)
|
|||
|
|
- `mission_logs` (id, mission_id FK, tick_index, encounter_key, rendered_text, seed, modifiers_applied JSONB)
|
|||
|
|
- **Migrations from day one.** No "I'll add migrations later."
|
|||
|
|
- `TwitchPubSubService`: signs messages with extension client ID + secret, publishes per-channel updates per tick.
|
|||
|
|
- **Rate limit `/missions/state`** keyed off JWT `user_id`. A buggy panel could hammer it.
|
|||
|
|
|
|||
|
|
### Stage 5 — Content library & balance
|
|||
|
|
|
|||
|
|
- `@fog-explorer/encounter-library` with `encounters.json`. Records keyed by ID (`cleanse_hex`, `escape_hatch`, etc.) with: base success chance, difficulty tags, perk tags, **flavor text variants** (target 200+ per encounter type — this is the editorial product).
|
|||
|
|
- TS wrapper: `getEncounterById`, `getRandomEncounterByTier`, `pickFlavor(encounter, context)`.
|
|||
|
|
- **Version the encounter content.** In-flight missions use the version they started with; don't hot-swap.
|
|||
|
|
- `PerkMath` helper: aggregates additive/multiplicative modifiers, separates survivor-level from team-level. Clamp `P{success}` to sane bounds.
|
|||
|
|
- **Property-based tests** with `fast-check` on the resolver. Example tests pass while edge cases (modifier overflow, negative probabilities, empty perks) silently break.
|
|||
|
|
- **Monte Carlo balance harness** in CI. Simulate 10k missions across perk loadouts. Snapshot test on success/injury/sacrifice distributions. Catches AI-generated balance numbers that *feel* reasonable but produce degenerate gameplay.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Cross-cutting concerns
|
|||
|
|
|
|||
|
|
### Observability
|
|||
|
|
|
|||
|
|
- Structured logging (pino) with correlation IDs: `missionId`, `tickIndex`, `channelId`, `opaqueUserId`. From day one.
|
|||
|
|
- OpenTelemetry on the Nest side. Cheap to add upfront, painful later.
|
|||
|
|
- Tag every log line emitted by the encounter resolver with the seed used. Debugging mission desyncs without this is misery.
|
|||
|
|
|
|||
|
|
### Data retention
|
|||
|
|
|
|||
|
|
- `mission_logs` will grow fast. Plan archival: partition by month, or TTL completed missions older than N days.
|
|||
|
|
|
|||
|
|
### Local dev without Twitch
|
|||
|
|
|
|||
|
|
- Add a `NODE_ENV=development` flag where the overlay mints a fake JWT and the API accepts it. 10× faster iteration than rigging the Twitch local test harness for every change. **Strictly dev-only — assert and fail loudly if seen in prod.**
|
|||
|
|
|
|||
|
|
### Monetisation seam
|
|||
|
|
|
|||
|
|
- Bits/transaction JWTs use a different flow. Even if not implementing now, stub the seam. Future Bits-driven mission events will be a small addition rather than a refactor.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Dev environment
|
|||
|
|
|
|||
|
|
### Host setup
|
|||
|
|
|
|||
|
|
- **Windows + WSL2 + devcontainer.** No dual-boot needed. Stack has no kernel/GPU dependencies; bare-metal Linux gives no measurable benefit and adds context-switch friction.
|
|||
|
|
- **Code lives in WSL2 filesystem** (`~/code/fog-expedition`), not on `/mnt/c/`. The 9P boundary is 10–20× slower for filesystem ops; Nx with thousands of `node_modules` files will feel sluggish if mounted from Windows.
|
|||
|
|
- VS Code with Dev Containers extension. Open the repo via Remote-WSL, then "Reopen in Container."
|
|||
|
|
|
|||
|
|
### Devcontainer config
|
|||
|
|
|
|||
|
|
- `.devcontainer/devcontainer.json` references the root `docker-compose.yml` plus a `docker-compose.dev.yml` overlay that adds the workspace service.
|
|||
|
|
- Features: `docker-outside-of-docker` (lets the container run Docker commands against the host daemon — better than docker-in-docker), `github-cli`.
|
|||
|
|
- `forwardPorts`: 3000 (API), 4200 (overlay dev server), 5432 (Postgres), 6379 (Redis).
|
|||
|
|
- `postCreateCommand: pnpm install`.
|
|||
|
|
- VS Code extensions to preinstall: Nx Console, ESLint, Prettier, Jest Runner.
|
|||
|
|
|
|||
|
|
### Twitch dev rig integration
|
|||
|
|
|
|||
|
|
- The Twitch dev rig is an Electron app on the Windows side. It needs to reach the API.
|
|||
|
|
- Bind Nest to `0.0.0.0`, not `127.0.0.1`. WSL2's localhost forwarding handles WSL2 → Windows; `forwardPorts` handles container → WSL2.
|
|||
|
|
- HTTPS for EBS even locally. Add a `caddy` service to dev compose for TLS termination — three lines of config, avoids fiddling with Node TLS.
|
|||
|
|
|
|||
|
|
### File watching
|
|||
|
|
|
|||
|
|
- Nx file watchers use inotify. Inotify is unreliable on `/mnt/c/`. Inside WSL2 / devcontainer Linux filesystem, it works fine. This is another reason to keep code off the Windows drive.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. AI-assisted build conventions
|
|||
|
|
|
|||
|
|
This project is being built with heavy AI assistance. A few conventions to keep things sane:
|
|||
|
|
|
|||
|
|
- **Lock the contracts before generating implementations.** Finish `@fog-explorer/api-interfaces` (Zod schemas, not just TS types) before generating controllers/services. AI is great at filling in implementations against tight contracts, terrible at maintaining consistency when types drift.
|
|||
|
|
- **OpenAPI generation from Nest decorators.** Keeps the overlay client in sync automatically. Worth setting up Stage 1.
|
|||
|
|
- **Property-based tests on pure functions.** Especially the encounter resolver. AI-generated example tests pass while edge cases break.
|
|||
|
|
- **Monte Carlo balance harness as a snapshot test.** AI invents plausible-looking numbers that produce degenerate gameplay. Catch it in CI.
|
|||
|
|
- **Module boundary enforcement.** `@nx/enforce-module-boundaries` with project tags. AI loves cross-library relative imports.
|
|||
|
|
- **No hidden state in conversations.** Anything decided in chat that affects the codebase goes into a doc — this one, an ADR, or a code comment. Future-you and future-AI both need it.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Open decisions
|
|||
|
|
|
|||
|
|
Things deliberately not decided yet — flag them when you reach them:
|
|||
|
|
|
|||
|
|
- **Overlay framework:** Angular (per original plan) vs Lit vs vanilla TS. Decide before Stage 2 starts.
|
|||
|
|
- **ORM:** TypeORM vs Prisma vs Drizzle. Pick one before any DB code is written.
|
|||
|
|
- **Group/SWF expanded UI:** designed conceptually, not mocked.
|
|||
|
|
- **Streamer config screen:** designed conceptually, not mocked.
|
|||
|
|
- **Custom glyph set:** placeholder Tabler icons used in mocks; commission three monochrome glyphs (difficulty pip, injury wound, perk-triggered) before submitting for review.
|
|||
|
|
- **Hosted EBS domain:** required for Twitch review. Decide hosting (Fly.io, Railway, AWS, etc.) before Stage 4.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Out of scope (for v1)
|
|||
|
|
|
|||
|
|
- Bits-driven mission events (stub the seam, don't build).
|
|||
|
|
- Multiple survivors per user (one survivor per user, who replaces them on death).
|
|||
|
|
- Cross-channel persistence (survivors are scoped to the channel they were created on, for now).
|
|||
|
|
- Mobile app (the Twitch mobile app renders extensions, but design-validate at desktop first).
|
|||
|
|
- Streamer-customisable encounter content (single canonical content library for v1).
|