feat: preserve camp exploration resume context

This commit is contained in:
2026-07-28 13:22:32 +09:00
parent 3c817175f0
commit 7b30f0f60b
14 changed files with 2004 additions and 22 deletions

View File

@@ -53,6 +53,13 @@ try {
verifyActivityGates(dataModule);
verifyTargetBattleMemoryIsolation(dataModule);
verifyBattleLifecycleContract(dataModule);
verifyExplorationIntroSeenLifecycle(
dataModule,
explorationDataModule,
explorationActionModule,
campaignModule,
battleScenarios
);
verifyGuardedPersistentOverwrite(
dataModule,
actionModule,
@@ -78,7 +85,7 @@ try {
);
console.log(
'Third-camp preparation verification passed (activity/selection separation, explicit confirmation, formation compatibility, guarded overwrite, fourth-battle isolation, three-turn lifecycle, and old-save safety).'
'Third-camp preparation verification passed (intro completion separation, activity/selection separation, explicit confirmation, formation compatibility, guarded overwrite, fourth-battle isolation, three-turn lifecycle, and old-save safety).'
);
} finally {
await server.close();
@@ -159,6 +166,137 @@ function verifyCanonicalDefinitions(dataModule, battleScenarios) {
assert.equal(fresh.effect.accuracyBonus, 10);
}
function verifyExplorationIntroSeenLifecycle(
dataModule,
explorationDataModule,
explorationActionModule,
campaignModule,
battleScenarios
) {
assert.equal(
explorationDataModule.thirdCampExplorationIntroSeenId,
'third-guangzong-sortie-camp:intro-seen',
'The persisted intro marker must remain a stable save-data identifier.'
);
prepareThirdVictory(campaignModule, battleScenarios, dataModule);
assert.equal(
explorationActionModule.hasSeenThirdCampExplorationIntro(),
false
);
assert.equal(
explorationActionModule.thirdCampExplorationProgress().introSeen,
false
);
const entry = explorationActionModule.enterThirdCampExploration();
assert.equal(entry.ok, true);
assert.equal(entry.changed, true);
assert.equal(
entry.campaign.completedCampVisits.includes(
explorationDataModule.thirdCampExplorationVisitId
),
true,
'Entering the exploration must preserve the existing visit marker.'
);
assert.equal(
entry.campaign.completedCampVisits.includes(
explorationDataModule.thirdCampExplorationIntroSeenId
),
false,
'Entering alone must not mark the introduction as fully read.'
);
let reloaded = campaignModule.loadCampaignState();
assert.equal(
explorationActionModule.hasSeenThirdCampExplorationIntro(reloaded),
false,
'Reloading during the introduction must leave it eligible to replay.'
);
const completed =
explorationActionModule.completeThirdCampExplorationIntro();
assert.equal(completed.ok, true);
assert.equal(completed.changed, true);
assert.equal(
explorationActionModule.hasSeenThirdCampExplorationIntro(
completed.campaign
),
true
);
assert.equal(
completed.campaign.completedCampVisits.filter(
(id) =>
id ===
explorationDataModule.thirdCampExplorationVisitId
).length,
1
);
assert.equal(
completed.campaign.completedCampVisits.filter(
(id) =>
id ===
explorationDataModule.thirdCampExplorationIntroSeenId
).length,
1
);
assert.equal(
completed.campaign.firstBattleReport?.completedCampVisits.includes(
explorationDataModule.thirdCampExplorationIntroSeenId
),
true,
'The intro marker must remain mirrored in the report-backed visit history.'
);
reloaded = campaignModule.loadCampaignState();
assert.equal(
explorationActionModule.hasSeenThirdCampExplorationIntro(reloaded),
true
);
assert.equal(
explorationActionModule.thirdCampExplorationProgress(reloaded).introSeen,
true
);
const repeated =
explorationActionModule.completeThirdCampExplorationIntro();
assert.equal(repeated.ok, true);
assert.equal(repeated.changed, false);
prepareThirdVictory(campaignModule, battleScenarios, dataModule);
explorationActionModule.enterThirdCampExploration();
rejectStorageWrites = true;
let failedCompletion;
try {
failedCompletion =
explorationActionModule.completeThirdCampExplorationIntro();
} finally {
rejectStorageWrites = false;
}
assert.equal(failedCompletion?.ok, false);
assert.equal(failedCompletion?.reason, 'save-unavailable');
assert.equal(
explorationActionModule.hasSeenThirdCampExplorationIntro(),
false,
'A failed write must not leak an in-memory intro completion.'
);
assert.equal(
campaignModule.getCampaignState().completedCampVisits.includes(
explorationDataModule.thirdCampExplorationVisitId
),
true,
'Rolling back the intro marker must preserve the earlier entry marker.'
);
const wrongStep = campaignModule.getCampaignState();
wrongStep.step = 'fourth-battle';
campaignModule.setCampaignState(wrongStep);
assert.deepEqual(
explorationActionModule.completeThirdCampExplorationIntro(),
{ ok: false, reason: 'invalid-campaign' }
);
}
function verifyActivityGates(dataModule) {
const base = literalCampaign(dataModule);
let availability =