17 KiB
Executable File
17 KiB
Executable File
name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fog-expedition-nx-and-mission-engine | Set up an Nx-based monorepo for the Fog Expedition Twitch extension, implement a NestJS API with a tick-based mission engine, and integrate Twitch Extension auth, persistence, and content balance libraries. |
|
false |
Fog Expedition Nx Workspace & ZPG Engine Plan
v2 note: Additions introduced in this revision are marked [NEW] throughout.
Stage 1: Workspace & Infrastructure (Nx Foundation)
- Initialize Nx workspace & tooling
- Ensure the root contains an Nx workspace with TypeScript support (
nx.json,project.json/workspace.json,tsconfig.base.json,package.json). - Add Nx plugins for Angular and NestJS (e.g.
@nx/angular,@nx/nest,@nx/node). - Configure
package.jsonscripts for running Nx tasks (e.g."nx": "nx","start": "nx serve twitch-extension-panel").
- Ensure the root contains an Nx workspace with TypeScript support (
- Generate core apps
- Create an Angular app for the Twitch Extension Panel at
[apps/twitch-extension-panel/src/main.ts]via Nx generator (standalone components, no routing complexity initially). - Create a NestJS API app at
[apps/api/src/main.ts]using Nx's Nest generator, structured with modules forAuth,Missions, andTickEngine.
- Create an Angular app for the Twitch Extension Panel at
- Shared API interfaces library
- Generate a shared TypeScript library
@fog-explorer/api-interfacesat[libs/api-interfaces/src/index.ts]. - Define core domain types in small focused files:
[libs/api-interfaces/src/lib/survivor.ts]containingSurvivor,SurvivorStats,SurvivorState(Active,Injured,Sacrificed).[libs/api-interfaces/src/lib/mission.ts]containingMission,MissionState(Lobby,InProgress,Completed,Failed),EncounterResult.[libs/api-interfaces/src/lib/perk.ts]containingPerk,PerkModifier, andTeamPerkflags for SWF mechanics.
- Re-export all public contracts from
[libs/api-interfaces/src/index.ts]for clean imports in bothtwitch-extension-panelandapi.
- Generate a shared TypeScript library
- Docker & Docker Compose for infra
- Add a root
docker-compose.ymldefining services:postgres: PostgreSQL with exposed port, DB namefog_expedition, volume, and healthcheck.redis: Redis for mission timers and lobbies.- Optional
apiservice wired to build from[apps/api/Dockerfile]and depend onpostgresandredis.
- Create Dockerfiles:
[apps/api/Dockerfile]building the NestJS API (install deps, build via Nx, run withnode dist/apps/api/main.js).[apps/twitch-extension-panel/Dockerfile]for building and serving the Angular extension bundle for local testing.
- Define environment configuration via
.env/.env.localfor DB and Redis connection strings consumed by the API app.
- Add a root
- [NEW] Local development without Twitch
- Since
window.Twitch.extis unavailable in a local browser, a missing stub causes the panel to hang silently on startup. Solve this early to keep frontend development fluid. - Create a
TwitchExtMockmodule at[apps/twitch-extension-panel/src/mocks/twitch-ext.mock.ts]that:- Exposes the same interface as
window.Twitch.ext(onAuthorized,onContext,onVisibilityChanged,listen). - Returns a hardcoded fake JWT and viewer ID so all downstream services initialise normally.
- Is swapped in via an
environment.tsflag (environment.mock = true) — never shipped in production builds.
- Exposes the same interface as
- Add a
local-devnpm script that sets the mock flag and serves the panel against a local API.
- Since
Stage 2: The Extension Frontend (Angular Panel)
- Panel shell & layout
- In
twitch-extension-panel, create aPanelShellComponent(e.g.[apps/twitch-extension-panel/src/app/panel-shell.component.ts]) that:- Hosts the Live Log area and Survivor Status area.
- Handles initial Twitch Extension context/bootstrap (mounts only after
window.Twitch.ext.onAuthorized, or mock equivalent in dev).
- Add presentational components:
LiveLogComponentfor a scrolling log of encounters/events (Progress Quest style).SurvivorStatusComponentfor current survivor(s), health/injury state, mission status, and perks.
- In
- Twitch EBS integration & auth
- Implement a
TwitchAuthService(e.g.[apps/twitch-extension-panel/src/app/services/twitch-auth.service.ts]) that:- Wraps
window.Twitch.extcalls (onAuthorized,onContext,onVisibilityChanged). - Stores the latest JWT and parsed payload (obfuscated Twitch extension user ID).
- Wraps
- Implement an
EbsApiServicethat:- Attaches the Twitch JWT as
Authorization: Bearer <token>on all HTTP calls to the NestJS API. - Exposes methods like
startMission,getMissionState,getPerkInventoryusing types from@fog-explorer/api-interfaces.
- Attaches the Twitch JWT as
- Implement a
- [NEW] EBS resilience & rate limiting
- At scale, thousands of concurrent viewers can simultaneously poll
GET /missions/state, overwhelming the API. - Cache
GET /missions/stateresponses in Redis with a 2–3 second TTL to absorb viewer polling spikes. - The panel should degrade gracefully when the EBS is unreachable — display last-known state rather than a broken UI, and surface a subtle "reconnecting…" indicator.
- Add client-side exponential back-off with jitter for all EBS fetch retries.
- At scale, thousands of concurrent viewers can simultaneously poll
- State & real-time mission display
- Create a
MissionStateStoreusing Angular signals (or RxJSBehaviorSubjectas a fallback) to hold:- Current mission snapshot (
MissionState+ encounter history). - Survivor state(s) and perk inventory.
- Current mission snapshot (
- Wire the store to:
- Pull initial state from the API (
getMissionState). - Subscribe to Twitch PubSub events (via the Twitch JS helper) to apply incremental updates (new log lines, state transitions).
- Pull initial state from the API (
- Bind
PanelShellComponent,LiveLogComponent, andSurvivorStatusComponentto this store usingcomputedsignals orasyncpipe for efficient change detection.
- Create a
- [NEW] PubSub message size guard
- Twitch Extension PubSub enforces a 5 KB per-message limit. Unchecked log growth silently drops messages.
- Send only the latest N log lines per PubSub message (recommend N = 10–15).
- If the panel misses messages (detected via a sequence counter), trigger a full
getMissionStatere-fetch as a fallback.
Stage 3: The ZPG Mission Engine ("Corso")
- Global heartbeat & tick worker
- In the NestJS API, add a
TickEngineModule(e.g.[apps/api/src/app/tick-engine/tick-engine.module.ts]) and aTickServicethat:- Uses
@nestjs/schedule(cron) or a background worker to trigger a global tick every 60 seconds. - Acquires a Redis-based distributed lock (e.g.
SETNX tick_lock) to ensure only one instance runs the tick. - Scans active missions from Redis and dispatches them to the encounter resolver.
- Uses
- In the NestJS API, add a
- [NEW] Tick engine resilience
- Two failure modes need explicit answers before production: long ticks and process restarts.
- Set the Redis lock TTL to 50 seconds (10-second buffer before the next tick fires). If a tick is still running when the lock expires, emit a structured warning log — do not silently skip.
- Make encounter resolution idempotent: store a tick sequence number per mission in Redis and skip re-processing if the current tick index has already been resolved. This protects against double-application on restart mid-tick.
- Log tick start, duration, and number of missions processed as structured fields on every execution.
- Encounter resolver (stateless core)
- Implement a pure, stateless function in
[libs/api-interfaces/src/lib/encounter-resolver.ts]or a new logic lib (e.g.libs/mission-logic):- Signature like
resolveEncounter(survivor: SurvivorStats, perks: PerkModifier[], difficulty: Difficulty): EncounterResult. - Uses RNG with a pluggable seed source for deterministic tests.
- Applies modifiers as P{success} = Base + Σ Modifiers clamped to sane bounds.
- Signature like
- In the API, create an
EncounterServicethat:- Calls the resolver for each mission tick.
- Updates mission state (success/fail, injury, sacrifice) and logs events.
- Implement a pure, stateless function in
- Group logic & SWF mechanics
- Introduce a
GroupSynergyServicethat:- Accepts a group of 2–4 survivors and their perks.
- Aggregates "Team Perk" modifiers, applying them to all relevant rolls.
- Computes group-level outcomes (e.g., shared progress, split rewards, synchronized failure states).
- Extend the encounter resolver inputs to accept a
groupContextthat influences difficulty modifiers for group missions.
- Introduce a
- Redis for active missions & lobbies
- Define Redis data structures in the API layer:
active_mission:{missionId}: JSON or hash storing mission state, participant IDs, nextTickAt.mission_lobby:{lobbyId}: state for lobby members, mission template, and ready flags.
- Implement a
MissionStorerepository that abstracts Redis operations (get/set/expire/lists) from the core mission logic.
- Define Redis data structures in the API layer:
Stage 4: Twitch Extension Persistence (EBS)
- Extension action handlers instead of chat commands
- In the NestJS
MissionsModule, add controllers like[apps/api/src/app/missions/missions.controller.ts]exposing:POST /missions/startto replace!explore(triggered by the panel "Start Mission" button).GET /missions/statefor current mission snapshot.POST /missions/choose-perk(future) or similar progression actions.
- Use DTOs shaped by
@fog-explorer/api-interfacestypes for request/response contracts.
- In the NestJS
- Twitch JWT verification & identity mapping
- Implement a
TwitchAuthModuleandTwitchAuthGuardthat:- Verifies incoming JWTs from the extension using the Twitch extension secret.
- Extracts the obfuscated Twitch user ID and extension channel ID, attaching them to the Nest request context.
- Implement a
- [NEW] JWT secret rotation handling
- Twitch can rotate the extension secret, causing all JWT verification to fail globally until the API is redeployed.
- On a JWT verification failure, the guard should attempt to re-fetch the current secret from the Twitch API before returning 401.
- Cache the fetched secret with a reasonable TTL (e.g. 5 minutes) so rotation recovery is near-instant.
- Emit a structured alert log whenever a secret re-fetch occurs — this is a high-signal operational event.
- Postgres schema
- Design Postgres tables (via migrations/TypeORM/Prisma) in the API project:
users(internal ID, Twitch extension user ID, created_at).survivors(FK tousers, stats, perk slots, current state: active/injured/sacrificed). Use soft-delete /sacrificed_attimestamp rather than hard deletion to support history and leaderboards.missions(FK tosurvivorsor group, status, difficulty, timestamps).mission_logs(FK tomissions, tick index, encounter key, rendered text, RNG details if needed).
- Design Postgres tables (via migrations/TypeORM/Prisma) in the API project:
- [NEW] mission_logs retention strategy
mission_logscan grow unboundedly — a channel with high traffic can generate millions of rows within weeks.- Add an
archived_atnullable column tomission_logsfrom day one. - Implement a lightweight nightly archival job that marks logs for completed/failed missions older than N days as archived (or bulk-moves them to an archive table).
- Add a Postgres index on
(mission_id, archived_at)to keep active-mission log queries fast regardless of historical volume.
- Twitch PubSub integration for ticks
- Create a
TwitchPubSubServicein the API that:- Signs messages to Twitch's Extension PubSub endpoint using the extension client ID and secret.
- Publishes log updates and mission state deltas on each tick for relevant channels.
- On the frontend, extend
TwitchAuthServiceor a dedicatedPubSubServiceto:- Subscribe to mission update topics.
- Forward received events into the
MissionStateStore, appending log lines and updating survivor/mission state.
- Create a
- [NEW] Broadcaster configuration panel
- Broadcasters frequently need to configure extension behaviour (difficulty presets, max survivors, opt-in features). This is often needed earlier than expected.
- Create a separate Angular app (or a route within the panel) for the Twitch Broadcaster Config view.
- Add a
POST /channel/configEBS endpoint that accepts channel-level settings and stores them against the channel ID in Postgres. - The config schema should at minimum cover: mission difficulty preset, max party size, and feature flags for beta mechanics.
- Apply these settings in
MissionsModulewhen resolving encounters for that channel.
Stage 5: Content Library & Balance
- JSON encounter library
- Create a dedicated library
@fog-explorer/encounter-libraryat[libs/encounter-library/src/lib/encounters.json]to hold all flavor text and base parameters. - Structure encounters as JSON records:
- Keys like
"cleanse_hex","escape_hatch", etc. - Fields for base success chance, difficulty tags, flavor text variants, and perk tags.
- Keys like
- Add a small TypeScript wrapper in
[libs/encounter-library/src/lib/encounter-library.ts]to:- Load encounters.
- Expose helper functions (e.g.
getEncounterById,getRandomEncounterByTier).
- Create a dedicated library
- [NEW] Encounter JSON schema versioning
- Adding new fields to
encounters.jsonsilently breaks records that predate those fields, causing runtime surprises. - Define a JSON Schema (or Zod schema) for the encounter record format and commit it alongside
encounters.json. - Add a build-time validation step (Nx target) that validates all encounter records against the schema on every build.
- Increment a
schemaVersionfield whenever breaking fields are added, and provide a migration helper for existing records.
- Adding new fields to
- Perk system & balance utilities
- In
@fog-explorer/api-interfaces(or a dedicated@fog-explorer/perkslib), formalize perk modifier types:- Additive and multiplicative modifiers, flat reroll mechanics, group-boosting perks.
- Implement a
PerkMathhelper that:- Aggregates modifiers into a final P{success} value.
- Separates survivor-level modifiers from team-level modifiers to keep SWF behaviour clear.
- Provide a small test harness (unit tests in the mission logic lib) to:
- Validate that typical perk loadouts produce reasonable success/fail/injury rates.
- Guard against accidental balance regressions.
- In
- [NEW] Simulation CLI for balance tuning
- Unit tests confirm correctness but are too coarse for tuning probability distributions. A headless simulator is far more useful.
- Add an Nx target:
nx run mission-logic:simulate -- --runs=10000 --perkSet=flashlight,spine_chill - The simulator should output: mean success rate, injury rate, sacrifice rate, and a percentile breakdown of mission lengths.
- Wire it into CI with a fixed seed so regressions in outcome distributions surface as test failures without flakiness.
[NEW] Cross-Cutting Concerns
These topics were absent from the original plan but span all stages and should be established early.
- Observability
- Install Pino as the NestJS logger (structured JSON output). Replace all
console.logusage with pino log calls. - Define a standard log schema with fields:
traceId,channelId,missionId,tickIndex,durationMs,event. - Instrument the tick engine specifically: log tick start, active mission count, tick duration, and any per-mission errors as structured fields on every execution.
- Add basic application metrics (tick processing time, encounter outcomes, error rates) — even a simple in-memory counter exported to a
/metricsendpoint is sufficient to start.
- Install Pino as the NestJS logger (structured JSON output). Replace all
High-Level Architecture Diagram
flowchart LR
viewerPanel["Twitch Panel (Angular)"] -->|"JWT via window.Twitch.ext"| apiGateway["NestJS API (EBS)"]
apiGateway -->|"REST: /missions/*"| missionEngine["Mission Engine / Tick Service"]
missionEngine -->|"state + logs"| postgresDb["Postgres"]
missionEngine -->|"timers + lobbies"| redisStore["Redis"]
missionEngine -->|"Tick updates"| twitchPubSub["Twitch Extension PubSub"]
twitchPubSub -->|"Push events"| viewerPanel
apiGateway -->|"channel config"| postgresDb
broadcasterConfig["Broadcaster Config Panel"] -->|"POST /channel/config"| apiGateway