import seedrandom = require('seedrandom'); import type { EncounterDefinition, EncounterResult, Perk, SurvivorState, SurvivorStats, } from '@fog-explorer/api-interfaces'; import { applyPerkModifiers } from './perk-math'; export interface ResolverInput { seed: string; missionId: string; tickIndex: number; difficulty: number; encounter: EncounterDefinition; survivor: { id: string; state: SurvivorState; stats: SurvivorStats; perkSlots: Perk[]; hookCount: number; }; } const OBJECTIVES_BONUS = 0.02; const ALTRUISM_BONUS = 0.02; const DIFFICULTY_PENALTY = 0.12; const ALTRUISM_TAG = 'altruistic'; 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 { probability: finalProbability, applied } = applyPerkModifiers( probability, input.survivor.perkSlots, input.encounter.tags ); const success = rng() < finalProbability; 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: applied, logText: buildLogText(input.encounter.key, success, survivorStateChange), }; } 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; }