diff --git a/CLAUDE.md b/CLAUDE.md index 5fa0f54..c734dcd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ This is a Twitch Video Overlay extension implementing an autonomous tick-based Z - Sentence case in UI text and log messages. - Zod schemas (not just TS types) at the EBS boundary. - Pure functions for game logic. Seeded PRNG via `seedrandom`, never `Math.random()`. Persist seeds in `mission_logs`. -- Migrations from day one. No "I'll add migrations later." +- Migrations from day one. No "I'll add migrations later." After writing a migration file, always run `pnpm exec prisma migrate deploy --schema=apps/api/prisma/schema.prisma` immediately — the regenerated Prisma client will SELECT new columns on every query and cause 500s until the column exists in the database. - Round every displayed number; JS float math leaks artifacts. - Structured logging with correlation IDs (`missionId`, `tickIndex`, `channelId`, `opaqueUserId`). - Bind Nest services to `0.0.0.0`, not `127.0.0.1`. The Twitch dev rig on Windows reaches them via the WSL2/devcontainer port forwarding chain. diff --git a/apps/api/prisma/migrations/20260511000000_add_mission_killer_name/migration.sql b/apps/api/prisma/migrations/20260511000000_add_mission_killer_name/migration.sql new file mode 100644 index 0000000..8ae444b --- /dev/null +++ b/apps/api/prisma/migrations/20260511000000_add_mission_killer_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "missions" ADD COLUMN "killer_name" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 7442efb..f197587 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -39,6 +39,7 @@ model Mission { difficulty Int @db.SmallInt durationMinutes Int @default(20) @map("duration_minutes") @db.SmallInt status String @default("active") + killerName String? @map("killer_name") encounterLibraryVersion String @map("encounter_library_version") startedAt DateTime @default(now()) @map("started_at") endedAt DateTime? @map("ended_at") diff --git a/apps/api/src/app/missions/encounter.service.ts b/apps/api/src/app/missions/encounter.service.ts index 62670ed..b5b7668 100644 --- a/apps/api/src/app/missions/encounter.service.ts +++ b/apps/api/src/app/missions/encounter.service.ts @@ -79,7 +79,7 @@ export class EncounterService { const libEncounter = getEncounterById(encounter.key); const flavor = libEncounter - ? pickFlavor(libEncounter, { success: result.success }, rng) + ? pickFlavor(libEncounter, { success: result.success, killerName: mission.killerName }, rng) : result.logText; newLog.push({ ...result, logText: flavor }); diff --git a/apps/api/src/app/missions/missions.service.ts b/apps/api/src/app/missions/missions.service.ts index bbb75e0..454fe79 100644 --- a/apps/api/src/app/missions/missions.service.ts +++ b/apps/api/src/app/missions/missions.service.ts @@ -10,7 +10,8 @@ import type { Survivor, SurvivorStats, } from '@fog-explorer/api-interfaces'; -import { getLibraryVersion } from '@fog-explorer/encounter-library'; +import { getLibraryVersion, KILLER_NAMES } from '@fog-explorer/encounter-library'; +import seedrandom = require('seedrandom'); import { TwitchJwtPayload } from '../auth/twitch-jwt.guard'; import { PrismaService } from '../prisma/prisma.service'; import { MissionStoreService } from './mission-store.service'; @@ -38,6 +39,7 @@ export class MissionsService { const nextTickAt = new Date(now.getTime() + TICK_BASE_INTERVAL_MS + jitter); const stats: SurvivorStats = { objectives: 5, survival: 5, altruism: 5 }; const survivorName = characterName ?? defaultName(claims.opaque_user_id); + const killerName = pickKiller(missionId); // Upsert user and create survivor + mission in one transaction. await this.prisma.$transaction(async (tx) => { @@ -66,6 +68,7 @@ export class MissionsService { difficulty, durationMinutes, status: 'active', + killerName, encounterLibraryVersion: getLibraryVersion(), nextTickAt, participants: { @@ -98,6 +101,7 @@ export class MissionsService { difficulty, durationMinutes, status: 'active', + killerName, encounterLibraryVersion: getLibraryVersion(), nextTickAt: nextTickAt.toISOString(), tickIndex: 0, @@ -154,3 +158,8 @@ export class MissionsService { function defaultName(opaqueUserId: string): string { return `Survivor ${opaqueUserId.slice(-4)}`; } + +function pickKiller(missionId: string): string { + const rng = seedrandom(`killer:${missionId}`); + return KILLER_NAMES[Math.floor(rng() * KILLER_NAMES.length)]; +} diff --git a/apps/overlay/public/avatars/K01_TheTrapper_Portrait.webp b/apps/overlay/public/avatars/K01_TheTrapper_Portrait.webp new file mode 100755 index 0000000..5b010fd Binary files /dev/null and b/apps/overlay/public/avatars/K01_TheTrapper_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K01_charPreview_portrait.webp b/apps/overlay/public/avatars/K01_charPreview_portrait.webp new file mode 100755 index 0000000..ed24f6e Binary files /dev/null and b/apps/overlay/public/avatars/K01_charPreview_portrait.webp differ diff --git a/apps/overlay/public/avatars/K02_TheWraith_Portrait.webp b/apps/overlay/public/avatars/K02_TheWraith_Portrait.webp new file mode 100755 index 0000000..0d87671 Binary files /dev/null and b/apps/overlay/public/avatars/K02_TheWraith_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K03_TheHillbilly_Portrait.webp b/apps/overlay/public/avatars/K03_TheHillbilly_Portrait.webp new file mode 100755 index 0000000..469d9ed Binary files /dev/null and b/apps/overlay/public/avatars/K03_TheHillbilly_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K04_TheNurse_Portrait.webp b/apps/overlay/public/avatars/K04_TheNurse_Portrait.webp new file mode 100755 index 0000000..55271cc Binary files /dev/null and b/apps/overlay/public/avatars/K04_TheNurse_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K05_TheShape_Portrait.webp b/apps/overlay/public/avatars/K05_TheShape_Portrait.webp new file mode 100755 index 0000000..601eb49 Binary files /dev/null and b/apps/overlay/public/avatars/K05_TheShape_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K06_TheHag_Portrait.webp b/apps/overlay/public/avatars/K06_TheHag_Portrait.webp new file mode 100755 index 0000000..707d017 Binary files /dev/null and b/apps/overlay/public/avatars/K06_TheHag_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K07_TheDoctor_Portrait.webp b/apps/overlay/public/avatars/K07_TheDoctor_Portrait.webp new file mode 100755 index 0000000..de4ec30 Binary files /dev/null and b/apps/overlay/public/avatars/K07_TheDoctor_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K08_TheHuntress_Portrait.webp b/apps/overlay/public/avatars/K08_TheHuntress_Portrait.webp new file mode 100755 index 0000000..79d01d6 Binary files /dev/null and b/apps/overlay/public/avatars/K08_TheHuntress_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K09_TheCannibal_Portrait.webp b/apps/overlay/public/avatars/K09_TheCannibal_Portrait.webp new file mode 100755 index 0000000..9006459 Binary files /dev/null and b/apps/overlay/public/avatars/K09_TheCannibal_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K10_TheNightmare_Portrait.webp b/apps/overlay/public/avatars/K10_TheNightmare_Portrait.webp new file mode 100755 index 0000000..7a7d56e Binary files /dev/null and b/apps/overlay/public/avatars/K10_TheNightmare_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K11_ThePig_Portrait.webp b/apps/overlay/public/avatars/K11_ThePig_Portrait.webp new file mode 100755 index 0000000..786dec5 Binary files /dev/null and b/apps/overlay/public/avatars/K11_ThePig_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K12_TheClown_Portrait.webp b/apps/overlay/public/avatars/K12_TheClown_Portrait.webp new file mode 100755 index 0000000..a04f667 Binary files /dev/null and b/apps/overlay/public/avatars/K12_TheClown_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K13_TheSpirit_Portrait.webp b/apps/overlay/public/avatars/K13_TheSpirit_Portrait.webp new file mode 100755 index 0000000..fca97c7 Binary files /dev/null and b/apps/overlay/public/avatars/K13_TheSpirit_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K14_TheLegion_Portrait.webp b/apps/overlay/public/avatars/K14_TheLegion_Portrait.webp new file mode 100755 index 0000000..84753c8 Binary files /dev/null and b/apps/overlay/public/avatars/K14_TheLegion_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K15_ThePlague_Portrait.webp b/apps/overlay/public/avatars/K15_ThePlague_Portrait.webp new file mode 100755 index 0000000..d530cdf Binary files /dev/null and b/apps/overlay/public/avatars/K15_ThePlague_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K16_TheGhostFace_Portrait.webp b/apps/overlay/public/avatars/K16_TheGhostFace_Portrait.webp new file mode 100755 index 0000000..537c6d5 Binary files /dev/null and b/apps/overlay/public/avatars/K16_TheGhostFace_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K17_TheDemogorgon_Portrait.webp b/apps/overlay/public/avatars/K17_TheDemogorgon_Portrait.webp new file mode 100755 index 0000000..1c0da77 Binary files /dev/null and b/apps/overlay/public/avatars/K17_TheDemogorgon_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K18_TheOni_Portrait.webp b/apps/overlay/public/avatars/K18_TheOni_Portrait.webp new file mode 100755 index 0000000..a1a1e02 Binary files /dev/null and b/apps/overlay/public/avatars/K18_TheOni_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K19_TheDeathslinger_Portrait.webp b/apps/overlay/public/avatars/K19_TheDeathslinger_Portrait.webp new file mode 100755 index 0000000..dc0d48f Binary files /dev/null and b/apps/overlay/public/avatars/K19_TheDeathslinger_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K20_TheExecutioner_Portrait.webp b/apps/overlay/public/avatars/K20_TheExecutioner_Portrait.webp new file mode 100755 index 0000000..d6df474 Binary files /dev/null and b/apps/overlay/public/avatars/K20_TheExecutioner_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K21_TheBlight_Portrait.webp b/apps/overlay/public/avatars/K21_TheBlight_Portrait.webp new file mode 100755 index 0000000..f5b267f Binary files /dev/null and b/apps/overlay/public/avatars/K21_TheBlight_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K22_TheTwins_Portrait.webp b/apps/overlay/public/avatars/K22_TheTwins_Portrait.webp new file mode 100755 index 0000000..2054aa5 Binary files /dev/null and b/apps/overlay/public/avatars/K22_TheTwins_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K23_TheTrickster_Portrait.webp b/apps/overlay/public/avatars/K23_TheTrickster_Portrait.webp new file mode 100755 index 0000000..a00a032 Binary files /dev/null and b/apps/overlay/public/avatars/K23_TheTrickster_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K24_TheNemesis_Portrait.webp b/apps/overlay/public/avatars/K24_TheNemesis_Portrait.webp new file mode 100755 index 0000000..b105e56 Binary files /dev/null and b/apps/overlay/public/avatars/K24_TheNemesis_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K25_TheCenobite_Portrait.webp b/apps/overlay/public/avatars/K25_TheCenobite_Portrait.webp new file mode 100755 index 0000000..254713c Binary files /dev/null and b/apps/overlay/public/avatars/K25_TheCenobite_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K26_TheArtist_Portrait.webp b/apps/overlay/public/avatars/K26_TheArtist_Portrait.webp new file mode 100755 index 0000000..f9b78b1 Binary files /dev/null and b/apps/overlay/public/avatars/K26_TheArtist_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K27_TheOnryo_Portrait.webp b/apps/overlay/public/avatars/K27_TheOnryo_Portrait.webp new file mode 100755 index 0000000..f5ad464 Binary files /dev/null and b/apps/overlay/public/avatars/K27_TheOnryo_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K28_TheDredge_Portrait.webp b/apps/overlay/public/avatars/K28_TheDredge_Portrait.webp new file mode 100755 index 0000000..3053ee7 Binary files /dev/null and b/apps/overlay/public/avatars/K28_TheDredge_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K29_TheMastermind_Portrait.webp b/apps/overlay/public/avatars/K29_TheMastermind_Portrait.webp new file mode 100755 index 0000000..39f5252 Binary files /dev/null and b/apps/overlay/public/avatars/K29_TheMastermind_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K30_TheKnight_Portrait.webp b/apps/overlay/public/avatars/K30_TheKnight_Portrait.webp new file mode 100755 index 0000000..f5bf417 Binary files /dev/null and b/apps/overlay/public/avatars/K30_TheKnight_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K31_TheSkullMerchant_Portrait.webp b/apps/overlay/public/avatars/K31_TheSkullMerchant_Portrait.webp new file mode 100755 index 0000000..7514b96 Binary files /dev/null and b/apps/overlay/public/avatars/K31_TheSkullMerchant_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K32_TheSingularity_Portrait.webp b/apps/overlay/public/avatars/K32_TheSingularity_Portrait.webp new file mode 100755 index 0000000..2e515fb Binary files /dev/null and b/apps/overlay/public/avatars/K32_TheSingularity_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K33_TheXenomorph_Portrait.webp b/apps/overlay/public/avatars/K33_TheXenomorph_Portrait.webp new file mode 100755 index 0000000..b66ae1a Binary files /dev/null and b/apps/overlay/public/avatars/K33_TheXenomorph_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K34_TheGoodGuy_Portrait.webp b/apps/overlay/public/avatars/K34_TheGoodGuy_Portrait.webp new file mode 100755 index 0000000..2e98233 Binary files /dev/null and b/apps/overlay/public/avatars/K34_TheGoodGuy_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K35_TheUnknown_Portrait.webp b/apps/overlay/public/avatars/K35_TheUnknown_Portrait.webp new file mode 100755 index 0000000..85241a4 Binary files /dev/null and b/apps/overlay/public/avatars/K35_TheUnknown_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K36_TheLich_Portrait.webp b/apps/overlay/public/avatars/K36_TheLich_Portrait.webp new file mode 100755 index 0000000..e83ed4e Binary files /dev/null and b/apps/overlay/public/avatars/K36_TheLich_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K37_TheDarkLord_Portrait.webp b/apps/overlay/public/avatars/K37_TheDarkLord_Portrait.webp new file mode 100755 index 0000000..ef15dc5 Binary files /dev/null and b/apps/overlay/public/avatars/K37_TheDarkLord_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K38_TheHoundmaster_Portrait.webp b/apps/overlay/public/avatars/K38_TheHoundmaster_Portrait.webp new file mode 100755 index 0000000..a70490b Binary files /dev/null and b/apps/overlay/public/avatars/K38_TheHoundmaster_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K39_TheGhoul_Portrait.webp b/apps/overlay/public/avatars/K39_TheGhoul_Portrait.webp new file mode 100755 index 0000000..ad6d52b Binary files /dev/null and b/apps/overlay/public/avatars/K39_TheGhoul_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K40_TheAnimatronic_Portrait.webp b/apps/overlay/public/avatars/K40_TheAnimatronic_Portrait.webp new file mode 100755 index 0000000..537bd0d Binary files /dev/null and b/apps/overlay/public/avatars/K40_TheAnimatronic_Portrait.webp differ diff --git a/apps/overlay/public/avatars/K41_TheKrasue_Portrait.webp b/apps/overlay/public/avatars/K41_TheKrasue_Portrait.webp new file mode 100755 index 0000000..5a8dee1 Binary files /dev/null and b/apps/overlay/public/avatars/K41_TheKrasue_Portrait.webp differ diff --git a/apps/overlay/src/app/panel/expanded-panel.component.css b/apps/overlay/src/app/panel/expanded-panel.component.css index aa47b55..ad2c67a 100644 --- a/apps/overlay/src/app/panel/expanded-panel.component.css +++ b/apps/overlay/src/app/panel/expanded-panel.component.css @@ -160,11 +160,36 @@ text-transform: uppercase; } +.killer-vs { + display: flex; + align-items: center; + gap: 5px; + margin-left: auto; +} + +.killer-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + object-fit: cover; + object-position: top center; + opacity: 0.8; + flex-shrink: 0; +} + +.killer-name { + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--fog-text-dim); + font-style: italic; + white-space: nowrap; +} + .tick-counter { font-family: var(--font-mono); font-size: 10px; color: var(--fog-text-dim); - margin-left: auto; + margin-left: 8px; } /* ── Log ── */ diff --git a/apps/overlay/src/app/panel/expanded-panel.component.html b/apps/overlay/src/app/panel/expanded-panel.component.html index c07df4d..617ca23 100644 --- a/apps/overlay/src/app/panel/expanded-panel.component.html +++ b/apps/overlay/src/app/panel/expanded-panel.component.html @@ -41,6 +41,19 @@ } Mission + @if (killerName(); as kn) { +