Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`. - Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`. - Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions. - Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions. - Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
This commit is contained in:
156
libs/mission-logic/src/lib/encounter-resolver.ts
Normal file
156
libs/mission-logic/src/lib/encounter-resolver.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import seedrandom = require('seedrandom');
|
||||
import type {
|
||||
EncounterDefinition,
|
||||
EncounterResult,
|
||||
ModifierApplication,
|
||||
Perk,
|
||||
SurvivorState,
|
||||
SurvivorStats,
|
||||
} from '@fog-explorer/api-interfaces';
|
||||
|
||||
export interface ResolverInput {
|
||||
seed: string;
|
||||
missionId: string;
|
||||
tickIndex: number;
|
||||
difficulty: number;
|
||||
encounter: EncounterDefinition;
|
||||
survivor: {
|
||||
id: string;
|
||||
state: SurvivorState;
|
||||
stats: SurvivorStats;
|
||||
perkSlots: Perk[];
|
||||
hookCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Probability adjustments per stat point and per difficulty level.
|
||||
const OBJECTIVES_BONUS = 0.02;
|
||||
const ALTRUISM_BONUS = 0.02;
|
||||
const DIFFICULTY_PENALTY = 0.12;
|
||||
|
||||
// Tag that enables the altruism stat bonus.
|
||||
const ALTRUISM_TAG = 'altruistic';
|
||||
|
||||
// Injury probability floor/ceiling when active survivor fails.
|
||||
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 modifiersApplied = applyPerkModifiers(probability, input);
|
||||
probability = modifiersApplied.adjusted;
|
||||
const appliedList = modifiersApplied.applied;
|
||||
|
||||
probability = Math.max(0, Math.min(1, probability));
|
||||
|
||||
const success = rng() < probability;
|
||||
|
||||
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: appliedList,
|
||||
logText: buildLogText(input.encounter.key, success, survivorStateChange),
|
||||
};
|
||||
}
|
||||
|
||||
function applyPerkModifiers(
|
||||
startProbability: number,
|
||||
input: ResolverInput
|
||||
): { adjusted: number; applied: ModifierApplication[] } {
|
||||
let probability = startProbability;
|
||||
const applied: ModifierApplication[] = [];
|
||||
|
||||
for (const perk of input.survivor.perkSlots) {
|
||||
for (const mod of perk.modifiers) {
|
||||
if (mod.target !== 'successChance') continue;
|
||||
|
||||
if (mod.condition) {
|
||||
const tagMatch = mod.condition.encounterTags.some((t) =>
|
||||
input.encounter.tags.includes(t)
|
||||
);
|
||||
if (!tagMatch) continue;
|
||||
}
|
||||
|
||||
if (mod.type === 'additive') {
|
||||
probability += mod.amount;
|
||||
} else {
|
||||
probability *= 1 + mod.amount;
|
||||
}
|
||||
|
||||
applied.push({
|
||||
perkKey: perk.key,
|
||||
target: mod.target,
|
||||
type: mod.type,
|
||||
effectiveAmount: mod.amount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { adjusted: probability, applied };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user