import type { ModifierApplication, Perk, PerkModifierTarget } from '@fog-explorer/api-interfaces'; export interface AppliedModifier extends ModifierApplication { readonly preApplyProbability: number; readonly postApplyProbability: number; } export interface PerkMathResult { readonly probability: number; readonly applied: AppliedModifier[]; } const P_MIN = 0; const P_MAX = 1; /** * Applies all perk modifiers targeting `successChance` from the given perk * slots, filtered by encounter tags. Additives are summed first, then * multiplicatives applied in order. Result is clamped to [0, 1]. * * Separating this from the resolver keeps the probability pipeline testable * independently and makes modifier-overflow edge cases visible. */ export function applyPerkModifiers( baseProbability: number, perks: Perk[], encounterTags: string[], target: PerkModifierTarget = 'successChance' ): PerkMathResult { let probability = baseProbability; const applied: AppliedModifier[] = []; // Two-pass: collect additive sum, then apply multiplicatives. // This matches standard RPG convention: add-then-multiply prevents // multiplicative stacking from compounding excessively. let additiveSum = 0; const multiplicativePerks: Array<{ perk: Perk; amount: number }> = []; for (const perk of perks) { for (const mod of perk.modifiers) { if (mod.target !== target) continue; if (!conditionMatches(mod.condition, encounterTags)) continue; if (mod.type === 'additive') { additiveSum += mod.amount; } else { multiplicativePerks.push({ perk, amount: mod.amount }); } } } // Apply additive sum in one step. if (additiveSum !== 0) { const pre = probability; probability += additiveSum; // Record one entry per contributing perk. for (const perk of perks) { for (const mod of perk.modifiers) { if (mod.target !== target || mod.type !== 'additive') continue; if (!conditionMatches(mod.condition, encounterTags)) continue; applied.push({ perkKey: perk.key, target: mod.target, type: mod.type, effectiveAmount: mod.amount, preApplyProbability: pre, postApplyProbability: probability, }); } } } // Apply multiplicatives individually so each can be logged. for (const { perk, amount } of multiplicativePerks) { const pre = probability; probability *= 1 + amount; applied.push({ perkKey: perk.key, target, type: 'multiplicative', effectiveAmount: amount, preApplyProbability: pre, postApplyProbability: probability, }); } return { probability: clamp(probability, P_MIN, P_MAX), applied }; } function conditionMatches( condition: { encounterTags: string[] } | undefined, encounterTags: string[] ): boolean { if (!condition) return true; return condition.encounterTags.some((t) => encounterTags.includes(t)); } function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); }