feat: resume exploration exactly and harden campaign saves

This commit is contained in:
2026-07-28 17:39:20 +09:00
parent a08d251098
commit 8a6c8c2cd7
24 changed files with 8958 additions and 413 deletions

View File

@@ -887,6 +887,10 @@ try {
preJianYongSave.battleHistory[firstScenario.id].bonds = preJianYongSave.battleHistory[firstScenario.id].bonds.filter((bond) => bond.id !== 'liu-bei__jian-yong');
preJianYongSave.battleHistory[firstScenario.id].campaignRewards.recruits = preJianYongSave.battleHistory[firstScenario.id].campaignRewards.recruits.filter((recruit) => recruit.unitId !== 'jian-yong');
storage.set(campaignStorageKey, JSON.stringify(preJianYongSave));
storage.set(
`${campaignStorageKey}:slot-${preJianYongSave.activeSaveSlot}`,
JSON.stringify(preJianYongSave)
);
const repairedEarlyChoiceSave = loadCampaignState();
const persistedEarlyChoiceSave = saveCampaignState(repairedEarlyChoiceSave);
assert(
@@ -1152,6 +1156,10 @@ try {
'unknown-battle'
);
storage.set(campaignStorageKey, JSON.stringify(corruptedFutureAcknowledgementSave));
storage.set(
`${campaignStorageKey}:slot-${corruptedFutureAcknowledgementSave.activeSaveSlot}`,
JSON.stringify(corruptedFutureAcknowledgementSave)
);
const normalizedFutureAcknowledgementSave = loadCampaignState();
assert(
normalizedFutureAcknowledgementSave.acknowledgedVictoryRewardBattleIds.includes(firstScenario.id) &&
@@ -1167,19 +1175,19 @@ try {
});
const revisedSettlement = getCampaignState();
assert(
revisedSettlement.gold === 140 &&
revisedSettlement.inventory.Bean === 3 &&
revisedSettlement.inventory.Wine === 1 &&
revisedSettlement.inventory['Iron Sword'] === undefined &&
revisedSettlement.inventory['Iron Armor'] === 1,
`Expected revised battle report settlement to replace previous rewards instead of stacking: ${JSON.stringify(revisedSettlement)}`
revisedSettlement.gold === 100 &&
revisedSettlement.inventory.Bean === 2 &&
revisedSettlement.inventory.Wine === undefined &&
revisedSettlement.inventory['Iron Sword'] === 1 &&
revisedSettlement.inventory['Iron Armor'] === undefined,
`Expected a revised settled report to update history without replacing or resurrecting the already-committed economy: ${JSON.stringify(revisedSettlement)}`
);
applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] }, 'repeat-visit-left');
applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] }, 'repeat-visit-right');
const repeatedVisit = getCampaignState();
assert(
repeatedVisit.gold === 150 &&
repeatedVisit.inventory.Bean === 5 &&
repeatedVisit.gold === 110 &&
repeatedVisit.inventory.Bean === 4 &&
repeatedVisit.completedCampVisits.includes('repeat-visit') &&
repeatedVisit.firstBattleReport?.completedCampVisits.includes('repeat-visit') &&
repeatedVisit.campVisitChoiceIds['repeat-visit'] === 'repeat-visit-left' &&
@@ -1198,8 +1206,8 @@ try {
applyCampVisitReward('repeat-visit', { gold: 10, itemRewards: ['Bean x2'] });
const visitResynced = getCampaignState();
assert(
visitResynced.gold === 150 &&
visitResynced.inventory.Bean === 5 &&
visitResynced.gold === 110 &&
visitResynced.inventory.Bean === 4 &&
visitResynced.completedCampVisits.includes('repeat-visit') &&
visitResynced.firstBattleReport?.completedCampVisits.includes('repeat-visit'),
`Expected report-only camp visit completion to resync without duplicate rewards: ${JSON.stringify(visitResynced)}`
@@ -1207,7 +1215,7 @@ try {
applyCampVisitReward('dirty-reward-visit', { itemRewards: ['', ' ', 'Bean -2', 'Bean +0', 'Bean x2'] });
const dirtyRewardVisit = getCampaignState();
assert(
dirtyRewardVisit.inventory.Bean === 7 &&
dirtyRewardVisit.inventory.Bean === 6 &&
dirtyRewardVisit.inventory[''] === undefined &&
dirtyRewardVisit.inventory['Bean -'] === undefined &&
dirtyRewardVisit.completedCampVisits.includes('dirty-reward-visit'),
@@ -1495,6 +1503,10 @@ try {
corruptedContextualCommandState.battleHistory[firstScenario.id].sortieOrder.command.role = 'support';
corruptedContextualCommandState.sortieOrderHistory[firstScenario.id].elite.command.usedTurn = orderBattleReport.turnNumber + 1;
storage.set(campaignStorageKey, JSON.stringify(corruptedContextualCommandState));
storage.set(
`${campaignStorageKey}:slot-1`,
JSON.stringify(corruptedContextualCommandState)
);
const normalizedContextualCommandState = loadCampaignState();
assert(
normalizedContextualCommandState.firstBattleReport?.sortieOrder?.command === undefined &&
@@ -1506,6 +1518,10 @@ try {
`Expected contextual normalization to remove ghost actors, role mismatches, and future command turns without dropping their order results: ${JSON.stringify(normalizedContextualCommandState)}`
);
storage.set(campaignStorageKey, validContextualCommandStateRaw);
storage.set(
`${campaignStorageKey}:slot-1`,
validContextualCommandStateRaw
);
loadCampaignState();
orderBattleReport.sortieOrder.progress[0].achieved = false;
@@ -1556,14 +1572,14 @@ try {
setFirstBattleReport(revisedOrderReport);
const revisedOrderSettlement = getCampaignState();
assert(
revisedOrderSettlement.gold === 320 &&
revisedOrderSettlement.inventory.Bean === 3 &&
revisedOrderSettlement.inventory.Wine === 1 &&
revisedOrderSettlement.inventory['Iron Sword'] === undefined &&
revisedOrderSettlement.inventory['Iron Armor'] === 1 &&
revisedOrderSettlement.gold === 280 &&
revisedOrderSettlement.inventory.Bean === 2 &&
revisedOrderSettlement.inventory.Wine === undefined &&
revisedOrderSettlement.inventory['Iron Sword'] === 1 &&
revisedOrderSettlement.inventory['Iron Armor'] === undefined &&
revisedOrderSettlement.inventory['상처약'] === 1 &&
revisedOrderSettlement.claimedSortieOrderRewardIds.length === 1,
`Expected ordinary reward replacement to preserve the separately claimed sortie-order reward: ${JSON.stringify(revisedOrderSettlement)}`
`Expected revised ordinary reward metadata to preserve the already-committed economy and separately claimed sortie-order reward: ${JSON.stringify(revisedOrderSettlement)}`
);
setFirstBattleReport({
@@ -1573,7 +1589,7 @@ try {
});
const secondOrderSameBattle = getCampaignState();
assert(
secondOrderSameBattle.gold === 480 &&
secondOrderSameBattle.gold === 440 &&
secondOrderSameBattle.inventory['상처약'] === 1 &&
secondOrderSameBattle.inventory['탁주'] === 1 &&
secondOrderSameBattle.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`) &&
@@ -1597,7 +1613,7 @@ try {
});
const failedEliteReplay = getCampaignState();
assert(
failedEliteReplay.gold === 480 &&
failedEliteReplay.gold === 440 &&
failedEliteReplay.inventory['상처약'] === 1 &&
failedEliteReplay.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:elite`) &&
failedEliteReplay.firstBattleReport?.sortieOrder?.achieved === false &&
@@ -1624,7 +1640,7 @@ try {
});
const sameOrderDifferentBattle = getCampaignState();
assert(
sameOrderDifferentBattle.gold === 780 &&
sameOrderDifferentBattle.gold === 740 &&
sameOrderDifferentBattle.inventory['상처약'] === 2 &&
sameOrderDifferentBattle.claimedSortieOrderRewardIds.includes(`${secondScenario.id}:elite`) &&
sameOrderDifferentBattle.claimedSortieOrderRewardIds.length === 3 &&
@@ -1654,7 +1670,7 @@ try {
const spentRewardReplay = getCampaignState();
assert(
spentRewardReplay.gold === 160 &&
spentRewardReplay.inventory['탁주'] === 2 &&
spentRewardReplay.inventory['탁주'] === 1 &&
spentRewardReplay.claimedSortieOrderRewardIds.includes(`${firstScenario.id}:mobile`) &&
spentRewardReplay.firstBattleReport?.sortieOrder?.rewardGranted === true,
`Expected a first order reward to remain intact even when previously settled ordinary gold/items were already spent: ${JSON.stringify(spentRewardReplay)}`