Files
fog-explorer/libs/mission-logic/src/lib/encounter-resolver.ts
2026-04-12 16:43:45 +01:00

75 lines
2.0 KiB
TypeScript

import {
EncounterResult,
MissionDifficulty,
MissionState,
ResolveEncounterInput,
SurvivorStats
} from '@fog-explorer/api-interfaces';
import { applyPerkModifiers, clamp } from './perk-math';
const DIFFICULTY_BASE: Record<MissionDifficulty, number> = {
[MissionDifficulty.Easy]: 0.72,
[MissionDifficulty.Normal]: 0.6,
[MissionDifficulty.Hard]: 0.48,
[MissionDifficulty.Nightmare]: 0.38
};
export function resolveEncounter(input: ResolveEncounterInput): EncounterResult {
const baseChance = DIFFICULTY_BASE[input.difficulty];
const perkBoost = input.perkIds.length * 0.015;
const statBoost =
input.stats.stealth * 0.002 + input.stats.teamwork * 0.002 + input.stats.luck * 0.002;
const successChance = clamp(
applyPerkModifiers(baseChance + perkBoost + statBoost, []),
0.05,
0.95
);
const roll = seededRoll(input.seed ?? input.tickIndex);
if (roll <= successChance) {
return {
outcome: 'success',
text: 'The survivor outplayed the fog and advanced.',
successChance,
roll,
nextState: MissionState.InProgress,
nextStats: { ...input.stats, health: Math.min(100, input.stats.health + 1) }
};
}
const injuryThreshold = successChance + 0.25;
if (roll <= injuryThreshold) {
const injured = applyDamage(input.stats, 14);
return {
outcome: 'injury',
text: 'A close call leaves the survivor injured.',
successChance,
roll,
nextState: injured.health <= 0 ? MissionState.Failed : MissionState.InProgress,
nextStats: injured
};
}
return {
outcome: 'sacrifice',
text: 'The fog claims another soul.',
successChance,
roll,
nextState: MissionState.Failed,
nextStats: { ...input.stats, health: 0 }
};
}
function applyDamage(stats: SurvivorStats, amount: number): SurvivorStats {
return {
...stats,
health: Math.max(0, stats.health - amount)
};
}
function seededRoll(seed: number): number {
const normalized = Math.abs(Math.sin(seed * 99991)) * 10000;
return normalized - Math.floor(normalized);
}