feat: add sortie formation history
This commit is contained in:
@@ -36,6 +36,7 @@ try {
|
||||
normalizeCampaignSortieFormationPresets,
|
||||
normalizeCampaignSortieItemAssignments,
|
||||
normalizeCampaignSortiePerformanceSnapshot,
|
||||
normalizeCampaignSortieReviewSnapshot,
|
||||
resetCampaignState,
|
||||
saveCampaignState,
|
||||
setCampaignState,
|
||||
@@ -256,6 +257,49 @@ try {
|
||||
normalizeCampaignSortiePerformanceSnapshot({ selectedUnitIds: [], unitStats: [] }) === undefined,
|
||||
`Expected sortie performance snapshots to stay capped and empty snapshots to be discarded: ${JSON.stringify(cappedSortiePerformance)}`
|
||||
);
|
||||
const normalizedSortieReview = normalizeCampaignSortieReviewSnapshot(
|
||||
{
|
||||
version: 1,
|
||||
score: '88.9',
|
||||
grade: 'D',
|
||||
activeBondCount: '999',
|
||||
enemyCount: '1000',
|
||||
sourcePresetIds: ['siege', 'elite', 'siege', 'ghost', 'mobile', 7],
|
||||
sortieItemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
normalizedSortiePerformance
|
||||
);
|
||||
assert(
|
||||
normalizedSortieReview?.version === 1 &&
|
||||
normalizedSortieReview.score === 88 &&
|
||||
normalizedSortieReview.grade === 'A' &&
|
||||
normalizedSortieReview.activeBondCount === 96 &&
|
||||
normalizedSortieReview.enemyCount === 96 &&
|
||||
JSON.stringify(normalizedSortieReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile', 'siege']) &&
|
||||
!Object.prototype.hasOwnProperty.call(normalizedSortieReview, 'sortieItemAssignments'),
|
||||
`Expected sortie reviews to clamp evaluator inputs, derive grade from score, canonicalize known preset ids, and exclude unrelated fields: ${JSON.stringify(normalizedSortieReview)}`
|
||||
);
|
||||
assert(
|
||||
normalizeCampaignSortieReviewSnapshot({ ...normalizedSortieReview, version: 2 }, normalizedSortiePerformance) === undefined &&
|
||||
normalizeCampaignSortieReviewSnapshot({ score: 88, grade: 'A', sourcePresetIds: ['elite'] }, normalizedSortiePerformance) === undefined &&
|
||||
normalizeCampaignSortieReviewSnapshot(normalizedSortieReview) === undefined &&
|
||||
normalizeCampaignSortieReviewSnapshot(normalizedSortieReview, {
|
||||
selectedUnitIds: ['liu-bei'],
|
||||
formationAssignments: {},
|
||||
unitStats: []
|
||||
}) === undefined,
|
||||
'Expected sortie reviews with unsupported/missing nested versions or missing/incomplete performance snapshots to be discarded'
|
||||
);
|
||||
const sortieReviewGradeBoundaries = [[90, 'S'], [75, 'A'], [60, 'B'], [45, 'C'], [44, 'D']];
|
||||
assert(
|
||||
sortieReviewGradeBoundaries.every(([score, grade]) => (
|
||||
normalizeCampaignSortieReviewSnapshot(
|
||||
{ ...normalizedSortieReview, score, grade: 'D' },
|
||||
normalizedSortiePerformance
|
||||
)?.grade === grade
|
||||
)),
|
||||
`Expected persisted sortie review grades to match evaluator boundaries: ${JSON.stringify(sortieReviewGradeBoundaries)}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(
|
||||
@@ -413,6 +457,14 @@ try {
|
||||
formationAssignments: { [performanceUnit.id]: 'front' },
|
||||
unitStats: [sortieStatsFixture(performanceUnit.id, { hpBefore: performanceUnit.maxHp, maxHpBefore: performanceUnit.maxHp, damageDealt: 24 })]
|
||||
},
|
||||
sortieReview: {
|
||||
version: 1,
|
||||
score: 88,
|
||||
grade: 'A',
|
||||
activeBondCount: 2,
|
||||
enemyCount: 4,
|
||||
sourcePresetIds: ['elite', 'mobile']
|
||||
},
|
||||
createdAt: '2026-07-03T09:05:00.000Z'
|
||||
};
|
||||
resetCampaignState();
|
||||
@@ -420,29 +472,51 @@ try {
|
||||
performanceReport.sortiePerformance.selectedUnitIds.push('ghost-unit');
|
||||
performanceReport.sortiePerformance.formationAssignments[performanceUnit.id] = 'support';
|
||||
performanceReport.sortiePerformance.unitStats[0].damageDealt = 999;
|
||||
performanceReport.sortieReview.score = 1;
|
||||
performanceReport.sortieReview.grade = 'D';
|
||||
performanceReport.sortieReview.sourcePresetIds.push('siege');
|
||||
const clonedPerformanceSettlement = getCampaignState();
|
||||
const clonedReportPerformance = clonedPerformanceSettlement.firstBattleReport?.sortiePerformance;
|
||||
const clonedHistoryPerformance = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance;
|
||||
const clonedReportReview = clonedPerformanceSettlement.firstBattleReport?.sortieReview;
|
||||
const clonedHistoryReview = clonedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview;
|
||||
assert(
|
||||
JSON.stringify(clonedReportPerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) &&
|
||||
clonedReportPerformance.formationAssignments[performanceUnit.id] === 'front' &&
|
||||
clonedReportPerformance.unitStats[0].damageDealt === 24 &&
|
||||
JSON.stringify(clonedHistoryPerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) &&
|
||||
clonedHistoryPerformance.formationAssignments[performanceUnit.id] === 'front' &&
|
||||
clonedHistoryPerformance.unitStats[0].damageDealt === 24,
|
||||
`Expected battle report settlement to deep-clone nested sortie performance input: ${JSON.stringify(clonedPerformanceSettlement)}`
|
||||
clonedHistoryPerformance.unitStats[0].damageDealt === 24 &&
|
||||
clonedReportReview?.score === 88 &&
|
||||
clonedReportReview.grade === 'A' &&
|
||||
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)}`
|
||||
);
|
||||
clonedReportPerformance.selectedUnitIds.push('detached-unit');
|
||||
clonedReportPerformance.formationAssignments[performanceUnit.id] = 'reserve';
|
||||
clonedReportPerformance.unitStats[0].damageDealt = 777;
|
||||
clonedReportReview.score = 2;
|
||||
clonedReportReview.grade = 'D';
|
||||
clonedReportReview.sourcePresetIds.push('siege');
|
||||
const unmutatedPerformanceSettlement = getCampaignState();
|
||||
assert(
|
||||
clonedHistoryPerformance.unitStats[0].damageDealt === 24 &&
|
||||
clonedHistoryReview.score === 88 &&
|
||||
clonedHistoryReview.grade === 'A' &&
|
||||
JSON.stringify(clonedHistoryReview.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) &&
|
||||
JSON.stringify(unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.selectedUnitIds) === JSON.stringify([performanceUnit.id]) &&
|
||||
unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.formationAssignments[performanceUnit.id] === 'front' &&
|
||||
unmutatedPerformanceSettlement.firstBattleReport?.sortiePerformance?.unitStats[0].damageDealt === 24 &&
|
||||
unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance?.unitStats[0].damageDealt === 24,
|
||||
`Expected returned report, history, and persisted sortie performance snapshots to remain deeply detached: ${JSON.stringify(unmutatedPerformanceSettlement)}`
|
||||
unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortiePerformance?.unitStats[0].damageDealt === 24 &&
|
||||
unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.score === 88 &&
|
||||
unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.grade === 'A' &&
|
||||
JSON.stringify(unmutatedPerformanceSettlement.firstBattleReport?.sortieReview?.sourcePresetIds) === JSON.stringify(['elite', 'mobile']) &&
|
||||
unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview?.score === 88 &&
|
||||
JSON.stringify(unmutatedPerformanceSettlement.battleHistory[firstScenario.id]?.sortieReview?.sourcePresetIds) === JSON.stringify(['elite', 'mobile']),
|
||||
`Expected returned report, history, and persisted sortie performance/review snapshots to remain deeply detached: ${JSON.stringify(unmutatedPerformanceSettlement)}`
|
||||
);
|
||||
resetCampaignState();
|
||||
const reserveDrillTemplate = campaignRecruitUnits.find((unit) => unit.id === 'jian-yong');
|
||||
@@ -501,8 +575,10 @@ try {
|
||||
);
|
||||
assert(
|
||||
repeatedSettlement.firstBattleReport?.sortiePerformance === undefined &&
|
||||
repeatedSettlement.battleHistory[firstScenario.id]?.sortiePerformance === undefined,
|
||||
`Expected legacy reports without sortie performance to remain compatible in both latest report and history: ${JSON.stringify(repeatedSettlement)}`
|
||||
repeatedSettlement.battleHistory[firstScenario.id]?.sortiePerformance === undefined &&
|
||||
repeatedSettlement.firstBattleReport?.sortieReview === undefined &&
|
||||
repeatedSettlement.battleHistory[firstScenario.id]?.sortieReview === undefined,
|
||||
`Expected legacy reports without sortie performance/review to remain compatible in both latest report and history: ${JSON.stringify(repeatedSettlement)}`
|
||||
);
|
||||
assert(
|
||||
repeatedSettlement.firstBattleReport?.campaignRewards?.unlocks[0]?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
@@ -1132,6 +1208,15 @@ try {
|
||||
],
|
||||
sortieItemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
sortieReview: {
|
||||
version: 1,
|
||||
score: '88.9',
|
||||
grade: 'D',
|
||||
activeBondCount: '999',
|
||||
enemyCount: '1000',
|
||||
sourcePresetIds: ['siege', 'elite', 'siege', 'ghost', 'mobile', 7],
|
||||
itemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
itemRewards: ['Bean', 'Bean', 'Bean -2', 'Bean +0', 10, 'x'.repeat(97)],
|
||||
campaignRewards: {
|
||||
supplies: [' Bean ', 'Bean', 'Bean -2', 'Bean +0', 20, 'x'.repeat(97)],
|
||||
@@ -1196,6 +1281,7 @@ try {
|
||||
`Expected malformed report fields to be normalized without discarding the report: ${JSON.stringify(malformedReport)}`
|
||||
);
|
||||
const malformedReportPerformance = malformedReport.firstBattleReport?.sortiePerformance;
|
||||
const malformedReportReview = malformedReport.firstBattleReport?.sortieReview;
|
||||
assert(
|
||||
JSON.stringify(malformedReportPerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) &&
|
||||
JSON.stringify(malformedReportPerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) &&
|
||||
@@ -1211,6 +1297,10 @@ try {
|
||||
!Object.prototype.hasOwnProperty.call(malformedReportPerformance, 'sortieItemAssignments'),
|
||||
`Expected malformed report sortie performance to normalize numbers, dedupe entries, filter stale roster ids, and exclude supplies: ${JSON.stringify(malformedReportPerformance)}`
|
||||
);
|
||||
assert(
|
||||
malformedReportReview === undefined,
|
||||
`Expected report reviews to be discarded when roster recovery removes part of their recorded sortie: ${JSON.stringify(malformedReportReview)}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(
|
||||
@@ -1345,6 +1435,15 @@ try {
|
||||
],
|
||||
itemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
sortieReview: {
|
||||
version: 1,
|
||||
score: '101.2',
|
||||
grade: 'D',
|
||||
activeBondCount: '999',
|
||||
enemyCount: '-4',
|
||||
sourcePresetIds: ['mobile', 'elite', 'mobile', 'ghost'],
|
||||
sortieItemAssignments: { 'liu-bei': { bean: 2 } }
|
||||
},
|
||||
reserveTraining: [
|
||||
{
|
||||
unitId: 'guan-yu',
|
||||
@@ -1445,6 +1544,7 @@ try {
|
||||
`Expected nested battle history progress arrays to filter invalid entries and normalize numbers: ${JSON.stringify(malformedHistory)}`
|
||||
);
|
||||
const malformedHistoryPerformance = malformedHistory.battleHistory['first-battle-zhuo-commandery']?.sortiePerformance;
|
||||
const malformedHistoryReview = malformedHistory.battleHistory['first-battle-zhuo-commandery']?.sortieReview;
|
||||
assert(
|
||||
JSON.stringify(malformedHistoryPerformance?.selectedUnitIds) === JSON.stringify(['liu-bei']) &&
|
||||
JSON.stringify(malformedHistoryPerformance?.formationAssignments) === JSON.stringify({ 'liu-bei': 'front' }) &&
|
||||
@@ -1460,6 +1560,10 @@ try {
|
||||
!Object.prototype.hasOwnProperty.call(malformedHistoryPerformance, 'itemAssignments'),
|
||||
`Expected malformed historical sortie performance to normalize and filter roster references without persisting supplies: ${JSON.stringify(malformedHistoryPerformance)}`
|
||||
);
|
||||
assert(
|
||||
malformedHistoryReview === undefined,
|
||||
`Expected historical reviews to be discarded when roster recovery removes part of their recorded sortie: ${JSON.stringify(malformedHistoryReview)}`
|
||||
);
|
||||
|
||||
storage.clear();
|
||||
storage.set(campaignStorageKey, JSON.stringify({ version: 99, step: 'sixty-sixth-camp' }));
|
||||
@@ -1544,7 +1648,7 @@ try {
|
||||
`Expected explicitly loaded corrupted slot timestamp to be normalized: ${JSON.stringify(corruptedSlot)}`
|
||||
);
|
||||
|
||||
console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/sortie-preset/sortie-performance/latest-history/detail recovery, sortie performance legacy/deep-clone safety, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.');
|
||||
console.log('Verified campaign save normalization, reward resettlement, camp reward idempotence, malformed-field/roster-equipment/bond-growth/bond-roster/report-reference/sortie-roster/sortie-preset/sortie-performance/sortie-review/latest-history/detail recovery, sortie performance/review legacy/deep-clone safety, timestamp-safe history/slot recovery, unknown-step/report/history recovery, unsupported-version rejection, and slotted fallback.');
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user