import seedrandom = require('seedrandom'); import type { EncounterDefinition, EncounterResult, ModifierApplication, Perk, SurvivorState, SurvivorStats, } from '@fog-explorer/api-interfaces'; export interface ResolverInput { seed: string; missionId: string; tickIndex: number; difficulty: number; encounter: EncounterDefinition; survivor: { id: string; state: SurvivorState; stats: SurvivorStats; perkSlots: Perk[]; hookCount: number; }; } // Probability adjustments per stat point and per difficulty level. const OBJECTIVES_BONUS = 0.02; const ALTRUISM_BONUS = 0.02; const DIFFICULTY_PENALTY = 0.12; // Tag that enables the altruism stat bonus. const ALTRUISM_TAG = 'altruistic'; // Injury probability floor/ceiling when active survivor fails. const INJURY_CHANCE_BASE = 0.8; const INJURY_CHANCE_FLOOR = 0.2; const SURVIVAL_INJURY_REDUCTION = 0.05; export function resolveEncounter(input: ResolverInput): EncounterResult { const rng = seedrandom(input.seed); let probability = input.encounter.baseProbability; probability -= (input.difficulty - 1) * DIFFICULTY_PENALTY; probability += input.survivor.stats.objectives * OBJECTIVES_BONUS; if (input.encounter.tags.includes(ALTRUISM_TAG)) { probability += input.survivor.stats.altruism * ALTRUISM_BONUS; } const modifiersApplied = applyPerkModifiers(probability, input); probability = modifiersApplied.adjusted; const appliedList = modifiersApplied.applied; probability = Math.max(0, Math.min(1, probability)); const success = rng() < probability; const survivorStateChange = success ? null : computeStateChange(rng, input.survivor.state, input.survivor.stats.survival, input.survivor.hookCount); return { missionId: input.missionId, survivorId: input.survivor.id, encounterKey: input.encounter.key, tickIndex: input.tickIndex, seed: input.seed, success, survivorStateChange, modifiersApplied: appliedList, logText: buildLogText(input.encounter.key, success, survivorStateChange), }; } function applyPerkModifiers( startProbability: number, input: ResolverInput ): { adjusted: number; applied: ModifierApplication[] } { let probability = startProbability; const applied: ModifierApplication[] = []; for (const perk of input.survivor.perkSlots) { for (const mod of perk.modifiers) { if (mod.target !== 'successChance') continue; if (mod.condition) { const tagMatch = mod.condition.encounterTags.some((t) => input.encounter.tags.includes(t) ); if (!tagMatch) continue; } if (mod.type === 'additive') { probability += mod.amount; } else { probability *= 1 + mod.amount; } applied.push({ perkKey: perk.key, target: mod.target, type: mod.type, effectiveAmount: mod.amount, }); } } return { adjusted: probability, applied }; } function computeStateChange( rng: seedrandom.PRNG, currentState: SurvivorState, survivalStat: number, hookCount: number ): EncounterResult['survivorStateChange'] { if (currentState === 'active') { const injuryChance = Math.max( INJURY_CHANCE_FLOOR, INJURY_CHANCE_BASE - survivalStat * SURVIVAL_INJURY_REDUCTION ); if (rng() < injuryChance) { return { from: 'active', to: 'injured' }; } return null; } if (currentState === 'injured') { return { from: 'injured', to: 'downed' }; } if (currentState === 'downed' && hookCount >= 2) { return { from: 'downed', to: 'sacrificed' }; } return null; } const STATE_CHANGE_TEXT: Record = { 'active->injured': 'Survivor was injured.', 'injured->downed': 'Survivor was downed.', 'downed->sacrificed': 'Survivor was sacrificed.', }; function buildLogText( encounterKey: string, success: boolean, stateChange: EncounterResult['survivorStateChange'] ): string { const outcome = success ? 'succeeded' : 'failed'; const base = `${encounterKey}: ${outcome}.`; if (!stateChange) return base; const extra = STATE_CHANGE_TEXT[`${stateChange.from}->${stateChange.to}`]; return extra ? `${base} ${extra}` : base; }