feat(battle-sim): implement battle simulation logic and metrics tracking

- Added WarBattleMetrics interface to track attacker and defender skills during battles.
- Enhanced WarUnit class with method to log activated skills.
- Introduced environment setup for battle simulation, including unit set loading and configuration resolution.
- Developed in-memory and Redis transport layers for handling battle simulation requests and results.
- Created types and interfaces for battle simulation payloads and responses.
- Implemented battle simulation processing logic, including logging and skill tracking.
- Added tests for battle simulation processor to ensure functionality and correctness.
This commit is contained in:
2025-12-30 11:17:28 +00:00
parent 97439f64a9
commit 56e662a7db
20 changed files with 1549 additions and 7 deletions
+151
View File
@@ -0,0 +1,151 @@
import type { WorldStateRow } from '../context.js';
import type { BattleSimJobPayload, BattleSimRequestPayload } from './types.js';
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
import type { WarEngineConfig } from '@sammo-ts/logic';
const DEFAULT_WAR_CONFIG = {
armPerPhase: 500,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
maxTrainByWar: 110,
maxAtmosByWar: 150,
};
const asRecord = (value: unknown): Record<string, unknown> => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
return value as Record<string, unknown>;
};
const resolveNumber = (
record: Record<string, unknown>,
keys: string[],
fallback: number
): number => {
for (const key of keys) {
const value = record[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
}
return fallback;
};
const resolveUnitSetName = (
worldState: WorldStateRow,
fallback: string
): string => {
const config = asRecord(worldState.config);
const environment = asRecord(config.environment ?? config.map);
const unitSet = environment.unitSet;
if (typeof unitSet === 'string' && unitSet.trim().length > 0) {
return unitSet;
}
return fallback;
};
const resolveStartYear = (worldState: WorldStateRow): number => {
const meta = asRecord(worldState.meta);
const scenarioMeta = asRecord(meta.scenarioMeta);
const startYear = scenarioMeta.startYear;
if (typeof startYear === 'number' && Number.isFinite(startYear)) {
return startYear;
}
return 0;
};
const resolveCastleCrewTypeId = (unitSet: {
crewTypes?: Array<{ id: number; name: string; requirements: Array<{ type: string }> }>;
defaultCrewTypeId?: number;
}): number => {
const crewTypes = unitSet.crewTypes ?? [];
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
if (byName) {
return byName.id;
}
const byRequirement = crewTypes.find((crewType) =>
crewType.requirements.some((requirement) => requirement.type === 'Impossible')
);
if (byRequirement) {
return byRequirement.id;
}
if (typeof unitSet.defaultCrewTypeId === 'number') {
return unitSet.defaultCrewTypeId;
}
return crewTypes[0]?.id ?? 0;
};
const resolveCastleArmType = (unitSet: {
crewTypes?: Array<{ id: number; armType: number }>;
}, castleCrewTypeId: number): number => {
const crewTypes = unitSet.crewTypes ?? [];
return crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ?? 0;
};
export const buildBattleSimJobPayload = async (
worldState: WorldStateRow,
request: BattleSimRequestPayload,
profileFallback: string
): Promise<BattleSimJobPayload> => {
const unitSetName = resolveUnitSetName(worldState, profileFallback);
const unitSet = await loadUnitSetDefinitionByName(unitSetName);
const configRecord = asRecord(worldState.config);
const constValues = asRecord(configRecord.const ?? configRecord.consts);
const castleCrewTypeId = resolveNumber(
constValues,
['castleCrewTypeId'],
resolveCastleCrewTypeId(unitSet)
);
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
const config: WarEngineConfig = {
armPerPhase: resolveNumber(
constValues,
['armPerPhase', 'armperphase'],
DEFAULT_WAR_CONFIG.armPerPhase
),
maxTrainByCommand: resolveNumber(
constValues,
['maxTrainByCommand'],
DEFAULT_WAR_CONFIG.maxTrainByCommand
),
maxAtmosByCommand: resolveNumber(
constValues,
['maxAtmosByCommand'],
DEFAULT_WAR_CONFIG.maxAtmosByCommand
),
maxTrainByWar: resolveNumber(
constValues,
['maxTrainByWar'],
DEFAULT_WAR_CONFIG.maxTrainByWar
),
maxAtmosByWar: resolveNumber(
constValues,
['maxAtmosByWar'],
DEFAULT_WAR_CONFIG.maxAtmosByWar
),
castleCrewTypeId,
armTypes: {
footman: 1,
archer: 2,
cavalry: 3,
wizard: 4,
siege: 5,
misc: 6,
castle: castleArmType,
},
};
return {
...request,
unitSet,
config,
time: {
year: request.year,
month: request.month,
startYear: resolveStartYear(worldState),
},
};
};