75 lines
2.0 KiB
TypeScript
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);
|
||
|
|
}
|