Add killerName to Mission model and enhance overlay components
- Introduced killerName field in the Mission model within the Prisma schema and created a corresponding migration. - Updated mission management logic to assign a killerName during mission creation. - Enhanced overlay components to display killer information, including avatar and name, in both expanded and minimized panels. - Added new CSS styles for killer display in the overlay. - Included new avatar images for various killers in the overlay assets.
This commit is contained in:
@@ -31,6 +31,7 @@ export const MissionSchema = z.object({
|
||||
difficulty: z.number().int().min(1).max(3),
|
||||
durationMinutes: MissionDurationMinutesSchema.default(20),
|
||||
status: MissionStateSchema,
|
||||
killerName: z.string().optional(),
|
||||
encounterLibraryVersion: z.string().min(1),
|
||||
nextTickAt: z.iso.datetime(),
|
||||
tickIndex: z.number().int().min(0),
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lib/encounter-library';
|
||||
export { SURVIVOR_NAMES } from './lib/survivors';
|
||||
export { KILLERS, KILLER_NAMES, killerAvatarUrl } from './lib/killers';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { EncounterDefinition } from '@fog-explorer/api-interfaces';
|
||||
import { ALLY_FIRST_NAMES } from './survivors';
|
||||
import { KILLER_NAMES } from './killers';
|
||||
import encountersData from './encounters.json';
|
||||
|
||||
interface RawEncounter {
|
||||
@@ -23,6 +24,7 @@ export interface LibraryEncounter extends EncounterDefinition {
|
||||
|
||||
export interface FlavorContext {
|
||||
success: boolean;
|
||||
killerName?: string;
|
||||
}
|
||||
|
||||
const LIBRARY_VERSION: string = encountersData.version;
|
||||
@@ -66,11 +68,15 @@ export function pickFlavor(
|
||||
): string {
|
||||
const pool = ctx.success ? encounter.flavorSuccess : encounter.flavorFailure;
|
||||
const raw = pool[Math.floor(rng() * pool.length)];
|
||||
return expandAlly(raw, rng);
|
||||
return expandTokens(raw, ctx, rng);
|
||||
}
|
||||
|
||||
function expandAlly(text: string, rng: () => number): string {
|
||||
return text.replace(/\{\{ally\}\}/g, () => {
|
||||
function expandTokens(text: string, ctx: FlavorContext, rng: () => number): string {
|
||||
let out = text.replace(/\{\{ally\}\}/g, () => {
|
||||
return ALLY_FIRST_NAMES[Math.floor(rng() * ALLY_FIRST_NAMES.length)];
|
||||
});
|
||||
out = out.replace(/\{\{killer\}\}/g, () => {
|
||||
return ctx.killerName ?? KILLER_NAMES[Math.floor(rng() * KILLER_NAMES.length)];
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.2.0",
|
||||
"version": "1.4.0",
|
||||
"encounters": [
|
||||
{
|
||||
"key": "generator_repair",
|
||||
@@ -37,26 +37,26 @@
|
||||
"tags": ["totem", "altruistic", "objectives"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"It breaks. Something screams — not here, but somewhere.",
|
||||
"The bones scatter. I feel the curse lift.",
|
||||
"One hex gone. The fog feels slightly less wrong.",
|
||||
"It comes apart like it was never solid.",
|
||||
"I don't understand what I just broke. I'm glad I broke it.",
|
||||
"{{ally}} spots it first. I make short work of it.",
|
||||
"The skull gives. Whatever watched through it is gone.",
|
||||
"{{ally}} watches my back. The cleanse is fast.",
|
||||
"It was waiting to fall. I let it.",
|
||||
"One less curse."
|
||||
"A hex totem. I hear it before I see it. I break it anyway.",
|
||||
"Just a dull totem. I cleanse it before it can become something worse.",
|
||||
"The hex shatters. Whatever was draining us stops.",
|
||||
"No curse on this one. Still worth clearing.",
|
||||
"I don't know which hex it was. It's gone now. That's enough.",
|
||||
"{{ally}} spots the hex. I make short work of it.",
|
||||
"The glow fades. The hex is gone.",
|
||||
"Dull totem. Cleansed. One less place for a hex to root itself.",
|
||||
"{{ally}} calls it out — just a dull totem, but now it's ash.",
|
||||
"One less totem in the trial."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"It pulls back. I can't finish this.",
|
||||
"Something drives me away before it's done.",
|
||||
"The hex holds. The air gets thicker.",
|
||||
"I reach for it and stop. Something warns me off.",
|
||||
"The totem hums. I'm not strong enough. Not yet.",
|
||||
"{{ally}} calls wrong. The totem burns brighter.",
|
||||
"The bones won't break. Wrong approach.",
|
||||
"{{ally}} needs me. I leave the totem standing.",
|
||||
"The hex totem pulls back. I can't finish this.",
|
||||
"I reach it but something drives me off before it breaks.",
|
||||
"The hex holds. The pressure doesn't lift.",
|
||||
"Just a dull totem, but I can't stay long enough to cleanse it.",
|
||||
"The hex hums. I'm not strong enough. Not yet.",
|
||||
"{{ally}} calls wrong. The hex burns brighter.",
|
||||
"No curse here, but no time either. I leave it standing.",
|
||||
"{{ally}} needs me. The totem stays.",
|
||||
"Halfway through. Then something changes. I run.",
|
||||
"The hex endures. I'll find another way."
|
||||
]
|
||||
@@ -160,25 +160,25 @@
|
||||
"I don't breathe. I don't think. It passes.",
|
||||
"A long silence — then the footsteps recede.",
|
||||
"I melt into the fog. Unseen.",
|
||||
"Predictable. Predictable is avoidable.",
|
||||
"{{killer}} is predictable. Predictable is avoidable.",
|
||||
"Close. Too close. But not enough.",
|
||||
"{{ally}} draws the patrol the other way. I slip through.",
|
||||
"A gap in the route. I take it.",
|
||||
"The killer looks elsewhere. I don't waste it.",
|
||||
"{{killer}} looks elsewhere. I don't waste it.",
|
||||
"Footsteps near. Then far. Then gone.",
|
||||
"The locker is cold and tight and it works."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"A wrong step. Eye contact. The chase starts.",
|
||||
"A wrong step. Eye contact. {{killer}} gives chase.",
|
||||
"I misjudge the angle. Spotted.",
|
||||
"My heartbeat is too loud. The presence is too close.",
|
||||
"My heartbeat is too loud. {{killer}} is too close.",
|
||||
"The route changed. I didn't know.",
|
||||
"No cover. Nowhere to go.",
|
||||
"{{ally}} breaks stealth nearby. We're both compromised.",
|
||||
"Every instinct says run. That instinct is wrong. I run anyway.",
|
||||
"The fog doesn't protect me here.",
|
||||
"Spotted. Everything changes.",
|
||||
"{{ally}}'s noise pulls the killer my way."
|
||||
"{{ally}}'s noise pulls {{killer}} my way."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -214,31 +214,31 @@
|
||||
{
|
||||
"key": "pallet_drop",
|
||||
"baseProbability": 0.55,
|
||||
"tags": ["survival", "escape"],
|
||||
"tags": ["survival", "chase"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"It crashes down between us. A moment bought.",
|
||||
"Wood and impact. I gain distance.",
|
||||
"The drop lands right. The chase falters.",
|
||||
"Timed right. The pallet works.",
|
||||
"They stagger. I use every second.",
|
||||
"The drop lands at the right instant.",
|
||||
"A barrier placed. The chase interrupted.",
|
||||
"It holds. That's all it needs to do.",
|
||||
"A roar behind the pallet. I'm already running.",
|
||||
"The gap widens. Use it."
|
||||
"{{killer}} is right behind me. I drop the pallet at the right instant. Distance bought.",
|
||||
"Mid-chase. The wood crashes down between us. I gain ground.",
|
||||
"I run the loop and hit the pallet at the last second. {{killer}} staggers.",
|
||||
"Timed right. The pallet interrupts the chase.",
|
||||
"{{killer}} tries to mindgame the drop. I read it. The pallet lands.",
|
||||
"The chase breaks. The pallet did its job.",
|
||||
"A roar of frustration behind me. I'm already running.",
|
||||
"The gap widens. I don't look back.",
|
||||
"{{killer}} can't close it. The pallet holds them.",
|
||||
"I break the chase. For now."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"The pallet drops wide. No gap.",
|
||||
"Too slow. It means nothing.",
|
||||
"I miscalculate. The chase continues.",
|
||||
"Too early. Wasted.",
|
||||
"They vault before the wood lands.",
|
||||
"Wrong angle. Wrong moment.",
|
||||
"The swing comes first.",
|
||||
"Not enough distance. Never was.",
|
||||
"I panic. The pallet follows.",
|
||||
"Wasted. The chase doesn't stop."
|
||||
"{{killer}} predicts the drop. I waste the pallet.",
|
||||
"Too slow — {{killer}} is through before the wood lands.",
|
||||
"I panic mid-chase. The pallet falls early. Useless.",
|
||||
"{{killer}} vaults before the drop. The chase continues.",
|
||||
"Wrong angle. The pallet saves nothing.",
|
||||
"The chase doesn't stop. I'm out of pallets.",
|
||||
"The swing comes before the wood hits the ground.",
|
||||
"I misjudge the distance. Wasted.",
|
||||
"One less pallet. The chase goes on.",
|
||||
"The drop panics. So do I."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -304,31 +304,31 @@
|
||||
{
|
||||
"key": "window_vault",
|
||||
"baseProbability": 0.60,
|
||||
"tags": ["escape", "survival"],
|
||||
"tags": ["survival", "chase"],
|
||||
"tier": 1,
|
||||
"flavorSuccess": [
|
||||
"Clean vault. The frame holds.",
|
||||
"Through and clear. Momentum kept.",
|
||||
"Tight, but usable.",
|
||||
"I make it through before they close the gap.",
|
||||
"The vault buys distance. I use it.",
|
||||
"Practiced. The window yields.",
|
||||
"I land clean on the other side.",
|
||||
"The window is a door when I need it.",
|
||||
"Fast enough. The gap opens.",
|
||||
"Through. The chase resets."
|
||||
"{{killer}} is closing in. I hit the window before they grab me.",
|
||||
"Clean vault. I reset the chase distance.",
|
||||
"In a chase and the window is right there. I take it.",
|
||||
"I make it through before {{killer}} can follow cleanly.",
|
||||
"The vault buys a second. In a chase, that's everything.",
|
||||
"Practiced. Through before {{killer}} can react.",
|
||||
"I land clean. The chase tilts my way.",
|
||||
"The window resets the loop. I use it.",
|
||||
"Fast vault. The gap opens.",
|
||||
"I break the chase line. Through and running."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"Boarded. No exit here.",
|
||||
"Too slow. I'm caught mid-vault.",
|
||||
"The frame splinters wrong.",
|
||||
"I misjudge the height. I pay for it.",
|
||||
"The window doesn't solve the problem.",
|
||||
"Too narrow.",
|
||||
"The worst possible moment for this to fail.",
|
||||
"The opening was an illusion.",
|
||||
"Bad angle. Bad outcome.",
|
||||
"The window doesn't help tonight."
|
||||
"Boarded. I lose the chase line.",
|
||||
"Too slow. {{killer}} catches me mid-vault.",
|
||||
"I misjudge the height. Mid-chase, that's fatal.",
|
||||
"The window doesn't save me here.",
|
||||
"Too narrow. Wrong call in a chase.",
|
||||
"The vault fails at the worst moment.",
|
||||
"{{killer}} reads the vault. I gain nothing.",
|
||||
"Bad angle. The chase continues on their terms.",
|
||||
"Blocked. I'm out of options on this loop.",
|
||||
"The window doesn't help. The chase ends badly."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -398,27 +398,27 @@
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"{{ally}} gets there first. Both of us run.",
|
||||
"Clean unhook. Nobody watching.",
|
||||
"Clean unhook. {{killer}} not watching.",
|
||||
"I get them down. The trial continues.",
|
||||
"Fast hands, right timing.",
|
||||
"They're off the hook. We move.",
|
||||
"{{ally}} pulls them free before the killer returns.",
|
||||
"{{ally}} pulls them free before {{killer}} returns.",
|
||||
"The rescue works. Don't stop moving.",
|
||||
"The window was there. I took it.",
|
||||
"{{ally}} gets them clear. Two of us running again.",
|
||||
"Free. Both running now."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"{{ally}} pulls back. The killer turns too soon.",
|
||||
"{{ally}} pulls back. {{killer}} turns too soon.",
|
||||
"The rescue fails. Both of us in danger.",
|
||||
"Wrong timing.",
|
||||
"{{ally}} reaches for the hook — the killer is already watching.",
|
||||
"{{ally}} reaches for the hook — {{killer}} is already watching.",
|
||||
"No safe window. I leave.",
|
||||
"The hook holds.",
|
||||
"{{ally}}'s approach is spotted.",
|
||||
"They're hooked again. Worse now.",
|
||||
"The rescue was a trap.",
|
||||
"The killer was closer than it looked."
|
||||
"{{killer}} was closer than it looked."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -578,27 +578,87 @@
|
||||
"tier": 3,
|
||||
"flavorSuccess": [
|
||||
"The instinct misfires. I'm not found.",
|
||||
"They look in the wrong place.",
|
||||
"{{killer}} looks in the wrong place.",
|
||||
"The prediction fails. I'm safe.",
|
||||
"They check elsewhere.",
|
||||
"{{killer}} checks elsewhere.",
|
||||
"Wrong corner. I breathe.",
|
||||
"They're certain. They're wrong.",
|
||||
"{{killer}} is certain. {{killer}} is wrong.",
|
||||
"The read was off. I move.",
|
||||
"Not there. Not tonight.",
|
||||
"The instinct fails them.",
|
||||
"Their focus breaks. I use it."
|
||||
"{{killer}}'s instinct fails them.",
|
||||
"{{killer}}'s focus breaks. I use it."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"They know exactly where I am.",
|
||||
"{{killer}} knows exactly where I am.",
|
||||
"Instinct and experience. I'm found.",
|
||||
"There's no hiding from something this certain.",
|
||||
"There's no hiding from {{killer}}.",
|
||||
"The read was right.",
|
||||
"My position is given away before the search starts.",
|
||||
"Exactly where expected.",
|
||||
"Their certainty was earned.",
|
||||
"{{killer}}'s certainty was earned.",
|
||||
"Found before the search begins.",
|
||||
"The instinct is right.",
|
||||
"They walk directly to me."
|
||||
"{{killer}} walks directly to me."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "chase",
|
||||
"baseProbability": 0.50,
|
||||
"tags": ["survival", "chase"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"I run the loop. {{killer}} can't close the gap. I lose them.",
|
||||
"Three pallets, two windows. I get out of the chase clean.",
|
||||
"I break line of sight in the fog. {{killer}}'s terror radius fades.",
|
||||
"I outlast the chase. {{killer}} gives up.",
|
||||
"My legs burn. I find cover. The chase ends.",
|
||||
"I played the tiles right. The chase breaks in my favour.",
|
||||
"Down to one pallet and I make it count. Chase over.",
|
||||
"I dodge into the fog. {{killer}} can't read where I went.",
|
||||
"The terror radius fades. I'm clear.",
|
||||
"I survive the chase. Barely."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"I run out of pallets. {{killer}} closes the gap.",
|
||||
"One wrong turn. Cornered.",
|
||||
"{{killer}} reads my loop before I complete it.",
|
||||
"I panic. I lose the tiles. That's it.",
|
||||
"No more pallets. No windows left. I go down.",
|
||||
"{{killer}} predicts every move I have.",
|
||||
"I run until there's nowhere left to run.",
|
||||
"Caught mid-vault. That's all it takes.",
|
||||
"The terror radius never fades. {{killer}} catches me.",
|
||||
"{{killer}} closes the distance. I can't stop it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "hook_sabotage",
|
||||
"baseProbability": 0.40,
|
||||
"tags": ["altruistic", "objectives"],
|
||||
"tier": 2,
|
||||
"flavorSuccess": [
|
||||
"I see {{killer}} carrying {{ally}}. I sprint to the nearest hook and sabo it before they arrive.",
|
||||
"The hook comes apart in my hands. {{killer}} will have to carry them further.",
|
||||
"I make it in time. One hook down.",
|
||||
"Quick hands. The hook drops before {{killer}} turns the corner.",
|
||||
"{{ally}} is being carried. The hook falls. Bought them a few more seconds.",
|
||||
"Sabotaged and gone before anyone sees me.",
|
||||
"The hook gives easily. I don't stay to watch.",
|
||||
"One less hook {{killer}} can use.",
|
||||
"I hear them struggling close by. I break the nearest hook and run.",
|
||||
"The sabo lands. {{killer}} has to reroute. Use the time."
|
||||
],
|
||||
"flavorFailure": [
|
||||
"Too slow. The hook is used before I get there.",
|
||||
"I'm spotted mid-sabo. {{killer}} gives chase.",
|
||||
"{{killer}} changes direction. They find a different hook.",
|
||||
"I reach it but I'm too late to finish the sabo.",
|
||||
"I start to sabo — then {{killer}}'s footsteps get too close.",
|
||||
"{{ally}} is hooked before I can stop it.",
|
||||
"{{killer}} anticipates it. They reroute.",
|
||||
"Not fast enough. The hook holds.",
|
||||
"{{ally}} goes up on the hook. I was too far.",
|
||||
"The sabo fails. So does the rescue."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
55
libs/encounter-library/src/lib/killers.ts
Normal file
55
libs/encounter-library/src/lib/killers.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface KillerEntry {
|
||||
name: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
export const KILLERS: KillerEntry[] = [
|
||||
{ name: 'the Trapper', file: 'K01_TheTrapper_Portrait.webp' },
|
||||
{ name: 'the Wraith', file: 'K02_TheWraith_Portrait.webp' },
|
||||
{ name: 'the Hillbilly', file: 'K03_TheHillbilly_Portrait.webp' },
|
||||
{ name: 'the Nurse', file: 'K04_TheNurse_Portrait.webp' },
|
||||
{ name: 'the Shape', file: 'K05_TheShape_Portrait.webp' },
|
||||
{ name: 'the Hag', file: 'K06_TheHag_Portrait.webp' },
|
||||
{ name: 'the Doctor', file: 'K07_TheDoctor_Portrait.webp' },
|
||||
{ name: 'the Huntress', file: 'K08_TheHuntress_Portrait.webp' },
|
||||
{ name: 'the Cannibal', file: 'K09_TheCannibal_Portrait.webp' },
|
||||
{ name: 'the Nightmare', file: 'K10_TheNightmare_Portrait.webp' },
|
||||
{ name: 'the Pig', file: 'K11_ThePig_Portrait.webp' },
|
||||
{ name: 'the Clown', file: 'K12_TheClown_Portrait.webp' },
|
||||
{ name: 'the Spirit', file: 'K13_TheSpirit_Portrait.webp' },
|
||||
{ name: 'the Legion', file: 'K14_TheLegion_Portrait.webp' },
|
||||
{ name: 'the Plague', file: 'K15_ThePlague_Portrait.webp' },
|
||||
{ name: 'the Ghost Face', file: 'K16_TheGhostFace_Portrait.webp' },
|
||||
{ name: 'the Demogorgon', file: 'K17_TheDemogorgon_Portrait.webp' },
|
||||
{ name: 'the Oni', file: 'K18_TheOni_Portrait.webp' },
|
||||
{ name: 'the Deathslinger', file: 'K19_TheDeathslinger_Portrait.webp' },
|
||||
{ name: 'the Executioner', file: 'K20_TheExecutioner_Portrait.webp' },
|
||||
{ name: 'the Blight', file: 'K21_TheBlight_Portrait.webp' },
|
||||
{ name: 'the Twins', file: 'K22_TheTwins_Portrait.webp' },
|
||||
{ name: 'the Trickster', file: 'K23_TheTrickster_Portrait.webp' },
|
||||
{ name: 'the Nemesis', file: 'K24_TheNemesis_Portrait.webp' },
|
||||
{ name: 'the Cenobite', file: 'K25_TheCenobite_Portrait.webp' },
|
||||
{ name: 'the Artist', file: 'K26_TheArtist_Portrait.webp' },
|
||||
{ name: 'the Onryō', file: 'K27_TheOnryo_Portrait.webp' },
|
||||
{ name: 'the Dredge', file: 'K28_TheDredge_Portrait.webp' },
|
||||
{ name: 'the Mastermind', file: 'K29_TheMastermind_Portrait.webp' },
|
||||
{ name: 'the Knight', file: 'K30_TheKnight_Portrait.webp' },
|
||||
{ name: 'the Skull Merchant', file: 'K31_TheSkullMerchant_Portrait.webp' },
|
||||
{ name: 'the Singularity', file: 'K32_TheSingularity_Portrait.webp' },
|
||||
{ name: 'the Xenomorph', file: 'K33_TheXenomorph_Portrait.webp' },
|
||||
{ name: 'the Good Guy', file: 'K34_TheGoodGuy_Portrait.webp' },
|
||||
{ name: 'the Unknown', file: 'K35_TheUnknown_Portrait.webp' },
|
||||
{ name: 'the Lich', file: 'K36_TheLich_Portrait.webp' },
|
||||
{ name: 'the Dark Lord', file: 'K37_TheDarkLord_Portrait.webp' },
|
||||
{ name: 'the Houndmaster', file: 'K38_TheHoundmaster_Portrait.webp' },
|
||||
{ name: 'the Ghoul', file: 'K39_TheGhoul_Portrait.webp' },
|
||||
{ name: 'the Animatronic', file: 'K40_TheAnimatronic_Portrait.webp' },
|
||||
{ name: 'the Krasue', file: 'K41_TheKrasue_Portrait.webp' },
|
||||
];
|
||||
|
||||
export const KILLER_NAMES: string[] = KILLERS.map((k) => k.name);
|
||||
|
||||
export function killerAvatarUrl(name: string): string | null {
|
||||
const entry = KILLERS.find((k) => k.name === name);
|
||||
return entry ? `avatars/${entry.file}` : null;
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"],
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
|
||||
Reference in New Issue
Block a user