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

17 KiB
Raw Blame History

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.
  • 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).