feat: add sortie operation orders

This commit is contained in:
2026-07-11 01:23:35 +09:00
parent 0920039b9c
commit 23bd55735b
9 changed files with 2112 additions and 49 deletions

View File

@@ -15,6 +15,12 @@ try {
sortieBondBonusForLevel,
sortieRoleEffectSummaries
} = await server.ssrLoadModule('/src/game/data/sortieSynergy.ts');
const {
evaluateSortieOrder,
sortieOrderDefinitions,
sortieOrderRewardClaimId,
summarizeSortieOrderHistory
} = await server.ssrLoadModule('/src/game/data/sortieOrders.ts');
const expectedRoleEffectSummaries = {
front: '일반 공격 피해 -8%(최소 1) · 반격 +20%',
@@ -114,7 +120,148 @@ try {
);
assert(JSON.stringify({ members, bonds }) === frozenInputs, 'Expected synergy evaluation not to mutate its inputs.');
console.log('Verified generic sortie role effects, synergy previews, bond thresholds, role coverage, and first-pursuit trinity.');
const orderPerformance = {
selectedUnitIds: ['front-unit', 'flank-unit', 'support-unit'],
formationAssignments: {
'front-unit': 'front',
'flank-unit': 'flank',
'support-unit': 'support'
},
unitStats: [
sortieOrderStats('front-unit', { sortiePreventedDamage: 1 }),
sortieOrderStats('flank-unit', { damageDealt: 1 }),
sortieOrderStats('support-unit', { sortieBonusHealing: 1 })
]
};
const orderInput = (overrides = {}) => ({
victory: true,
score: 75,
turnNumber: 12,
turnLimit: 12,
survivorCount: 3,
deployedCount: 3,
bonusObjectiveTotal: 2,
bonusObjectiveAchieved: 1,
terrainObjectiveTotal: 1,
terrainObjectiveAchieved: 1,
performance: orderPerformance,
...overrides
});
const frozenOrderPerformance = JSON.stringify(orderPerformance);
const eliteBoundary = evaluateSortieOrder('elite', orderInput());
assert(
eliteBoundary.achieved && eliteBoundary.score === 75 && eliteBoundary.progress.every((entry) => entry.achieved),
`Expected elite order to pass exactly at A/75 with full survival: ${JSON.stringify(eliteBoundary)}`
);
const eliteBelowGrade = evaluateSortieOrder('elite', orderInput({ score: 74 }));
const eliteCasualty = evaluateSortieOrder('elite', orderInput({ survivorCount: 2 }));
const eliteDefeat = evaluateSortieOrder('elite', orderInput({ victory: false, score: 100 }));
assert(
!eliteBelowGrade.achieved &&
eliteBelowGrade.progress.find((entry) => entry.id === 'elite-grade')?.achieved === false &&
!eliteCasualty.achieved &&
eliteCasualty.progress.find((entry) => entry.id === 'elite-survival')?.achieved === false &&
!eliteDefeat.achieved,
`Expected every elite boundary failure to block completion: ${JSON.stringify({ eliteBelowGrade, eliteCasualty, eliteDefeat })}`
);
const mobileBoundary = evaluateSortieOrder('mobile', orderInput());
const mobileLate = evaluateSortieOrder('mobile', orderInput({ turnNumber: 13 }));
const intentOnlyPerformance = {
...orderPerformance,
unitStats: orderPerformance.unitStats.map((stats) => ({
...stats,
damageDealt: 0,
defeats: 0,
intentGuardSuccesses: stats.unitId === 'front-unit' ? 1 : 0
}))
};
const mobileIntentOnly = evaluateSortieOrder('mobile', orderInput({ performance: intentOnlyPerformance }));
const inertPerformance = {
...intentOnlyPerformance,
unitStats: intentOnlyPerformance.unitStats.map((stats) => ({ ...stats, intentGuardSuccesses: 0 }))
};
const mobileWithoutBreakthrough = evaluateSortieOrder('mobile', orderInput({ performance: inertPerformance }));
assert(
mobileBoundary.achieved &&
!mobileLate.achieved &&
mobileLate.progress.find((entry) => entry.id === 'mobile-quick')?.achieved === false &&
mobileIntentOnly.achieved &&
!mobileWithoutBreakthrough.achieved &&
mobileWithoutBreakthrough.progress.find((entry) => entry.id === 'mobile-breakthrough')?.achieved === false,
`Expected mobile order to honor exact turn, flank-impact, and intent-counterplay boundaries: ${JSON.stringify({ mobileBoundary, mobileLate, mobileIntentOnly, mobileWithoutBreakthrough })}`
);
const siegeBoundary = evaluateSortieOrder('siege', orderInput());
const siegeBonusFallback = evaluateSortieOrder('siege', orderInput({
terrainObjectiveTotal: 0,
terrainObjectiveAchieved: 0,
bonusObjectiveAchieved: 1
}));
const siegeTerrainPrecedence = evaluateSortieOrder('siege', orderInput({
terrainObjectiveTotal: 1,
terrainObjectiveAchieved: 0,
bonusObjectiveAchieved: 2
}));
const siegeWithoutFront = evaluateSortieOrder('siege', orderInput({
performance: {
...orderPerformance,
unitStats: orderPerformance.unitStats.map((stats) => stats.unitId === 'front-unit'
? { ...stats, sortiePreventedDamage: 0, sortieBonusDamage: 0 }
: { ...stats })
}
}));
const siegeWithoutSupport = evaluateSortieOrder('siege', orderInput({
performance: {
...orderPerformance,
unitStats: orderPerformance.unitStats.map((stats) => stats.unitId === 'support-unit'
? { ...stats, sortieBonusDamage: 0, sortieBonusHealing: 0, sortieExtendedBondTriggers: 0 }
: { ...stats })
}
}));
assert(
siegeBoundary.achieved &&
siegeBonusFallback.achieved &&
!siegeTerrainPrecedence.achieved &&
!siegeWithoutFront.achieved &&
!siegeWithoutSupport.achieved,
`Expected siege order to prefer terrain objectives and require front/support contributions: ${JSON.stringify({ siegeBoundary, siegeBonusFallback, siegeTerrainPrecedence, siegeWithoutFront, siegeWithoutSupport })}`
);
const rewardByOrder = Object.fromEntries(sortieOrderDefinitions.map((definition) => [definition.id, definition]));
assert(
rewardByOrder.elite?.rewardGold === 180 && JSON.stringify(rewardByOrder.elite.rewardItems) === JSON.stringify(['상처약 1']) &&
rewardByOrder.mobile?.rewardGold === 160 && JSON.stringify(rewardByOrder.mobile.rewardItems) === JSON.stringify(['탁주 1']) &&
rewardByOrder.siege?.rewardGold === 200 && JSON.stringify(rewardByOrder.siege.rewardItems) === JSON.stringify(['콩 2']) &&
sortieOrderRewardClaimId('battle-a', 'mobile') === 'battle-a:mobile',
`Expected fixed sortie-order rewards and claim ids: ${JSON.stringify(rewardByOrder)}`
);
const eliteFailure = evaluateSortieOrder('elite', orderInput({ score: 74 }));
const eliteSuccess = evaluateSortieOrder('elite', orderInput({ score: 91 }));
const eliteSummary = summarizeSortieOrderHistory(
{
'battle-3': { elite: eliteSuccess },
'battle-1': { elite: eliteSuccess },
'battle-2': { elite: eliteFailure }
},
'elite',
['battle-1', 'battle-2', 'battle-3']
);
assert(
eliteSummary.attempts === 3 &&
eliteSummary.successes === 2 &&
eliteSummary.successRate === 67 &&
eliteSummary.currentStreak === 1 &&
eliteSummary.bestStreak === 1 &&
eliteSummary.bestScore === 91 &&
eliteSummary.firstRewardClaimed,
`Expected deterministic sortie-order history summary in supplied battle order: ${JSON.stringify(eliteSummary)}`
);
assert(JSON.stringify(orderPerformance) === frozenOrderPerformance, 'Expected sortie-order evaluation not to mutate performance input.');
console.log('Verified generic sortie role effects, synergy previews, bond thresholds, role coverage, first-pursuit trinity, and sortie-order evaluation/reward/history boundaries.');
} finally {
await server.close();
}
@@ -124,3 +271,24 @@ function assert(condition, message) {
throw new Error(message);
}
}
function sortieOrderStats(unitId, overrides = {}) {
return {
unitId,
hpBefore: 30,
maxHpBefore: 30,
damageDealt: 0,
damageTaken: 0,
defeats: 0,
actions: 1,
support: 0,
sortieBonusDamage: 0,
sortiePreventedDamage: 0,
sortieBonusHealing: 0,
sortieExtendedBondTriggers: 0,
intentDefeats: 0,
intentLineBreaks: 0,
intentGuardSuccesses: 0,
...overrides
};
}