Files
fog/libs/mission-logic/src/lib/perk-math.ts

102 lines
3.0 KiB
TypeScript
Raw Normal View History

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));
}