feat: add signature officer tactics

This commit is contained in:
2026-07-13 07:36:39 +09:00
parent 26a4d78185
commit cd1abbe815
11 changed files with 535 additions and 44 deletions

View File

@@ -15,6 +15,12 @@ try {
} = await server.ssrLoadModule('/src/game/data/sortieRecommendations.ts');
const { sortieFormationRoles } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts');
const { sortieOrderIds } = await server.ssrLoadModule('/src/game/data/sortieOrders.ts');
const { getUnitStrategyCoverage, usableCatalog } = await server.ssrLoadModule('/src/game/data/battleUsables.ts');
const signatureStrategyByOwner = new Map(
Object.values(usableCatalog)
.filter((usable) => usable.signatureOwnerId)
.map((usable) => [usable.signatureOwnerId, usable.name])
);
assert(
JSON.stringify(sortieRecommendationPlanIds) === JSON.stringify(['order', 'resonance', 'terrain-strategy']),
@@ -58,6 +64,8 @@ try {
orderId,
context,
formationRoles,
getUnitStrategyCoverage,
signatureStrategyByOwner,
expectedCount: scenario.sortie.sortieLimit
});
evaluatedPlanCount += 1;
@@ -155,7 +163,16 @@ function terrainScoreForUnit(unit, scenario, terrainRules, unitClasses) {
: 100;
}
function validateApplicablePlan({ plan, battleId, orderId, context, formationRoles, expectedCount }) {
function validateApplicablePlan({
plan,
battleId,
orderId,
context,
formationRoles,
getUnitStrategyCoverage,
signatureStrategyByOwner,
expectedCount
}) {
const candidateById = new Map(
context.candidates
.filter((candidate) => candidate.available !== false && candidate.unit.faction === 'ally' && candidate.unit.hp > 0)
@@ -205,6 +222,30 @@ function validateApplicablePlan({ plan, battleId, orderId, context, formationRol
Object.values(plan.unitReasons).every(isNonEmptyText),
`${battleId}/${orderId}/${plan.id}: expected one reason for every selected unit: ${JSON.stringify(plan.unitReasons)}`
);
if (signatureStrategyByOwner) {
plan.selectedUnitIds.forEach((unitId) => {
const signatureName = signatureStrategyByOwner.get(unitId);
if (signatureName) {
assert(
plan.unitReasons[unitId].includes(`고유 ${signatureName}`),
`${battleId}/${orderId}/${plan.id}: signature strategy must appear in ${unitId}'s recommendation reason: ${plan.unitReasons[unitId]}`
);
}
});
}
if (getUnitStrategyCoverage) {
const expectedStrategyCoverage = getUnitStrategyCoverage(plan.selectedUnitIds);
assert(
plan.metrics.strategyCoverageCount === expectedStrategyCoverage.count &&
plan.metrics.strategyCoverageTotal === expectedStrategyCoverage.total &&
JSON.stringify(plan.metrics.coveredStrategyLabels) === JSON.stringify(expectedStrategyCoverage.covered.map((definition) => definition.label)) &&
JSON.stringify(plan.metrics.missingStrategyLabels) === JSON.stringify(expectedStrategyCoverage.missing.map((definition) => definition.label)),
`${battleId}/${orderId}/${plan.id}: recommendation metrics must match tag-based strategy coverage: ${JSON.stringify({
actual: plan.metrics,
expected: expectedStrategyCoverage
})}`
);
}
validateMetrics(plan.metrics, orderId, `${battleId}/${orderId}/${plan.id}`);
}