Files
fog/PROJECT_CONTEXT.md
Maurycy e8523d270e Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`.
- Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`.
- Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions.
- Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions.
- Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
2026-05-07 14:26:16 +00:00

280 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Daylightstyle 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 24 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. **13 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 13 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`.
- 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 each participant's survivor perks for SWF missions, applies combined modifiers 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 1020× 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:** Decided — Angular. See [ADR-0008](docs/adr/0008-angular-for-overlay-frontend.md).
- **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).