feat: add sortie operation orders
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user