- 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.
119 lines
3.1 KiB
TypeScript
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;
|
|
}
|