Files
fog/libs/mission-logic/src/lib/encounter-resolver.ts
Maurycy 21f1a5319f Stage 5: Add Prisma integration and enhance mission management
- Introduced Prisma as a dependency in package.json and updated pnpm-lock.yaml.
- Created Prisma module and service for database interactions.
- Added initial Prisma schema and migration for user, survivor, mission, and related entities.
- Implemented throttling in the API using @nestjs/throttler for rate limiting.
- Enhanced mission management logic to utilize Prisma for database transactions.
- Updated missions controller and service to handle mission state and participant management.
- Added Twitch PubSub service for real-time updates on mission states.
2026-05-07 15:42:52 +00:00

119 lines
3.1 KiB
TypeScript

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<string, string> = {
'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;
}