From d0085eeb181a5ee1558cf6126a00406bb75f7897 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sun, 5 Jul 2026 14:13:27 +0900 Subject: [PATCH] Deduplicate campaign reward references --- .../verify-campaign-save-normalization.mjs | 6 +++ src/game/state/campaignState.ts | 46 +++++++++++++------ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/scripts/verify-campaign-save-normalization.mjs b/scripts/verify-campaign-save-normalization.mjs index 00e7512..56024ee 100644 --- a/scripts/verify-campaign-save-normalization.mjs +++ b/scripts/verify-campaign-save-normalization.mjs @@ -649,11 +649,13 @@ try { reputation: ['Village Thanks', ' Village Thanks '], recruits: [ { unitId: ' guan-yu ', name: ' Guan Yu ' }, + { unitId: 'guan-yu', name: 'Duplicate Guan Yu' }, { unitId: '', name: 'Broken' }, ...Array.from({ length: 140 }, (_, index) => ({ unitId: `recruit-${index}`, name: `Recruit ${index}` })) ], unlocks: [ { battleId: ' second-battle-yellow-turban-pursuit ', title: ' Next ' }, + { battleId: 'second-battle-yellow-turban-pursuit', title: 'Duplicate Next' }, { battleId: '', title: 'Broken' }, ...Array.from({ length: 140 }, (_, index) => ({ battleId: `battle-${index}`, title: `Battle ${index}` })) ], @@ -690,8 +692,12 @@ try { malformedReport.firstBattleReport.campaignRewards?.reputation.length === 1 && malformedReport.firstBattleReport.campaignRewards?.recruits.length === 128 && malformedReport.firstBattleReport.campaignRewards?.recruits[0].unitId === 'guan-yu' && + malformedReport.firstBattleReport.campaignRewards?.recruits[0].name === 'Guan Yu' && + malformedReport.firstBattleReport.campaignRewards?.recruits.filter((recruit) => recruit.unitId === 'guan-yu').length === 1 && malformedReport.firstBattleReport.campaignRewards?.unlocks.length === 128 && malformedReport.firstBattleReport.campaignRewards?.unlocks[0].battleId === 'second-battle-yellow-turban-pursuit' && + malformedReport.firstBattleReport.campaignRewards?.unlocks[0].title === 'Next' && + malformedReport.firstBattleReport.campaignRewards?.unlocks.filter((unlock) => unlock.battleId === 'second-battle-yellow-turban-pursuit').length === 1 && malformedReport.firstBattleReport.campaignRewards?.note === 'Reward note' && malformedReport.firstBattleReport.completedCampDialogues.length === 1 && malformedReport.firstBattleReport.completedCampVisits.length === 0, diff --git a/src/game/state/campaignState.ts b/src/game/state/campaignState.ts index 5774167..12b81ae 100644 --- a/src/game/state/campaignState.ts +++ b/src/game/state/campaignState.ts @@ -905,6 +905,18 @@ function uniqueStrings(value: unknown): string[] { ].slice(0, maxCampaignStringListEntries); } +function uniqueByKey(entries: T[], keyForEntry: (entry: T) => string) { + const seen = new Set(); + return entries.filter((entry) => { + const key = keyForEntry(entry); + if (!key || seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} + function normalizeKeyString(value: unknown): string { if (typeof value !== 'string') { return ''; @@ -1658,21 +1670,27 @@ function cloneCampaignRewardSnapshot(rewards?: CampaignRewardSnapshot): Campaign supplies: uniqueStrings(rewardRecord.supplies), equipment: uniqueStrings(rewardRecord.equipment), reputation: uniqueStrings(rewardRecord.reputation), - recruits: arrayOrEmpty(rewardRecord.recruits) - .filter(isPlainObject) - .map((recruit) => ({ - unitId: normalizeKeyString(recruit.unitId), - name: normalizeDisplayString(recruit.name) - })) - .filter((recruit) => recruit.unitId && recruit.name) + recruits: uniqueByKey( + arrayOrEmpty(rewardRecord.recruits) + .filter(isPlainObject) + .map((recruit) => ({ + unitId: normalizeKeyString(recruit.unitId), + name: normalizeDisplayString(recruit.name) + })) + .filter((recruit) => recruit.unitId && recruit.name), + (recruit) => recruit.unitId + ) .slice(0, maxCampaignStringListEntries), - unlocks: arrayOrEmpty(rewardRecord.unlocks) - .filter(isPlainObject) - .map((unlock) => ({ - battleId: normalizeKeyString(unlock.battleId), - title: normalizeDisplayString(unlock.title) - })) - .filter((unlock) => unlock.battleId && unlock.title) + unlocks: uniqueByKey( + arrayOrEmpty(rewardRecord.unlocks) + .filter(isPlainObject) + .map((unlock) => ({ + battleId: normalizeKeyString(unlock.battleId), + title: normalizeDisplayString(unlock.title) + })) + .filter((unlock) => unlock.battleId && unlock.title), + (unlock) => unlock.battleId + ) .slice(0, maxCampaignStringListEntries), note: note.length > 0 && note.length <= maxCampaignRewardNoteLength ? note : undefined };