feat: connect sortie recommendations to battle results

This commit is contained in:
2026-07-13 09:50:36 +09:00
parent 7b58317f67
commit 0cadcb22b9
9 changed files with 1958 additions and 104 deletions

View File

@@ -37,6 +37,7 @@ try {
normalizeCampaignSortieItemAssignments,
normalizeCampaignSortieOrderResultSnapshot,
normalizeCampaignSortiePerformanceSnapshot,
normalizeCampaignSortieRecommendationSnapshot,
normalizeCampaignSortieReviewSnapshot,
resetCampaignState,
saveCampaignState,
@@ -73,6 +74,39 @@ try {
const empty = loadCampaignState();
assert(empty.version === 1 && empty.step === 'new', `Expected empty storage to create initial campaign state: ${JSON.stringify(empty)}`);
const normalizedRecommendation = normalizeCampaignSortieRecommendationSnapshot({
version: 1,
battleId: 'first-battle-zhuo-commandery',
planId: 'terrain-strategy',
label: '지형·전법형',
summary: '지형과 고유 전법을 함께 활용합니다.',
sortieOrderId: 'elite',
selectedUnitIds: ['liu-bei', 'guan-yu'],
formationAssignments: { 'liu-bei': 'support', 'guan-yu': 'front', 'ghost-unit': 'flank' },
unitReasons: { 'liu-bei': '고유 인덕의 호령', 'guan-yu': '고유 청룡일섬', 'ghost-unit': '제외' }
}, {
expectedBattleId: 'first-battle-zhuo-commandery',
allowedUnitIds: new Set(['liu-bei', 'guan-yu'])
});
assert(
normalizedRecommendation?.planId === 'terrain-strategy' &&
JSON.stringify(normalizedRecommendation.selectedUnitIds) === JSON.stringify(['liu-bei', 'guan-yu']) &&
Object.keys(normalizedRecommendation.unitReasons).length === 2 &&
normalizedRecommendation.formationAssignments['ghost-unit'] === undefined,
`Expected recommendation provenance to normalize with its selected roster: ${JSON.stringify(normalizedRecommendation)}`
);
assert(
normalizeCampaignSortieRecommendationSnapshot(
{ ...normalizedRecommendation, planId: 'unknown-plan' },
{ expectedBattleId: 'first-battle-zhuo-commandery' }
) === undefined &&
normalizeCampaignSortieRecommendationSnapshot(
{ ...normalizedRecommendation, unitReasons: { 'liu-bei': '', 'guan-yu': '유효' } },
{ expectedBattleId: 'first-battle-zhuo-commandery' }
) === undefined,
'Expected unknown recommendation plans and missing officer reasons to normalize away.'
);
const normalizedLaunchSupplies = normalizeCampaignSortieItemAssignments(
{
'liu-bei': { ' bean ': '9', salve: '2', stone: '4', ['x'.repeat(97)]: '1' },
@@ -195,8 +229,10 @@ try {
damageDealt: '1000000',
damageTaken: -4,
defeats: '2.9',
actions: 'not-a-number',
support: '7',
actions: 'not-a-number',
support: '7',
strategyUses: '2',
signatureStrategyUses: '9',
sortieBonusDamage: '3',
sortiePreventedDamage: '4',
sortieBonusHealing: '5',
@@ -228,8 +264,10 @@ try {
normalizedLiuBeiPerformance.damageDealt === 999999 &&
normalizedLiuBeiPerformance.damageTaken === 0 &&
normalizedLiuBeiPerformance.defeats === 2 &&
normalizedLiuBeiPerformance.actions === 0 &&
normalizedLiuBeiPerformance.support === 7 &&
normalizedLiuBeiPerformance.actions === 0 &&
normalizedLiuBeiPerformance.support === 7 &&
normalizedLiuBeiPerformance.strategyUses === 2 &&
normalizedLiuBeiPerformance.signatureStrategyUses === 2 &&
normalizedLiuBeiPerformance.sortieBonusDamage === 3 &&
normalizedLiuBeiPerformance.sortiePreventedDamage === 4 &&
normalizedLiuBeiPerformance.sortieBonusHealing === 5 &&
@@ -808,7 +846,9 @@ try {
createdAt: '2026-07-03T09:00:00.000Z'
};
const performanceUnit = alliedFirstBattleUnits.find((unit) => unit.id === 'liu-bei') ?? alliedFirstBattleUnits[0];
const recommendedButNotDeployedUnit = alliedFirstBattleUnits.find((unit) => unit.id !== performanceUnit?.id);
assert(performanceUnit, 'Expected an allied first-battle unit for sortie performance clone checks');
assert(recommendedButNotDeployedUnit, 'Expected a second allied unit for recommendation execution mismatch checks');
const performanceReport = {
...firstBattleReport,
sortiePerformance: {
@@ -824,6 +864,16 @@ try {
enemyCount: 4,
sourcePresetIds: ['elite', 'mobile']
},
sortieRecommendation: {
version: 1,
battleId: firstScenario.id,
planId: 'order',
label: '군령 달성형',
summary: '추천 실행률의 부분 달성도 결과에서 평가합니다.',
selectedUnitIds: [performanceUnit.id, recommendedButNotDeployedUnit.id],
formationAssignments: { [performanceUnit.id]: 'front', [recommendedButNotDeployedUnit.id]: 'flank' },
unitReasons: { [performanceUnit.id]: '전열을 맡습니다.', [recommendedButNotDeployedUnit.id]: '돌파를 맡습니다.' }
},
createdAt: '2026-07-03T09:05:00.000Z'
};
resetCampaignState();
@@ -839,6 +889,8 @@ try {
const clonedHistoryPerformance = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance;
const clonedReportReview = clonedPerformanceSettlement.firstBattleReport?.sortieReview;
const clonedHistoryReview = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview;
const clonedReportRecommendation = clonedPerformanceSettlement.firstBattleReport?.sortieRecommendation;
const clonedHistoryRecommendation = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieRecommendation;
assert(
JSON.stringify(clonedReportPerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) &&
clonedReportPerformance.formationAssignments[performanceUnit.id] === 'front' &&
@@ -851,8 +903,10 @@ try {
JSON.stringify(clonedReportReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) &&
clonedHistoryReview?.score === 88 &&
clonedHistoryReview.grade === 'A' &&
JSON.stringify(clonedHistoryReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']),
`Expected battle report settlement to deep-clone nested sortie performance and review input: ${JSON.stringify(clonedPerformanceSettlement)}`
JSON.stringify(clonedHistoryReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) &&
JSON.stringify(clonedReportRecommendation?.selectedUnitIds) === JSON.stringify([performanceUnit.id, recommendedButNotDeployedUnit.id]) &&
JSON.stringify(clonedHistoryRecommendation?.selectedUnitIds) === JSON.stringify([performanceUnit.id, recommendedButNotDeployedUnit.id]),
`Expected battle report settlement to deep-clone performance/review data and preserve recommendation provenance for execution mismatch evaluation: ${JSON.stringify(clonedPerformanceSettlement)}`
);
clonedReportPerformance.selectedUnitIds.push('detached-unit');
clonedReportPerformance.formationAssignments[performanceUnit.id] = 'reserve';