feat: make guidance recoverable across camp and battle

This commit is contained in:
2026-07-28 05:57:42 +09:00
parent 495f98372c
commit 5f6490b2ae
7 changed files with 1300 additions and 44 deletions

View File

@@ -40,11 +40,12 @@ try {
verifyDominantSelection(memoryModule);
verifyMemoryAndBonus(memoryModule);
verifyCampGuidanceAndSortieGuard(memoryModule);
verifyCampaignRoundTrip(memoryModule, campaignModule, battleScenarios);
verifyBattleSaveRoundTrip(battleSaveModule);
console.log(
'First-battle camaraderie memory verification passed (selection, campaign settlement, legacy battle save, and +5%p gate).'
'First-battle camaraderie memory verification passed (selection, camp guidance, one-warning sortie confirmation, campaign settlement, legacy battle save, and +5%p gate).'
);
} finally {
await server.close();
@@ -202,6 +203,206 @@ function verifyMemoryAndBonus(memoryModule) {
);
}
function verifyCampGuidanceAndSortieGuard(memoryModule) {
const snapshot = {
version: 1,
bondId: 'liu-bei__zhang-fei',
unitIds: ['liu-bei', 'zhang-fei'],
attempts: 1,
successes: 1,
additionalDamage: 7
};
const baseCampaign = {
battleHistory: {
[memoryModule.firstBattleCamaraderieSourceBattleId]: {
battleId: memoryModule.firstBattleCamaraderieSourceBattleId,
outcome: 'victory',
units: [
{ unitId: 'liu-bei', name: '유비' },
{ unitId: 'zhang-fei', name: '장비' }
],
sortieCooperation: snapshot
}
},
completedCampDialogues: []
};
const incompleteMemory =
memoryModule.resolveFirstBattleCamaraderieMemory(baseCampaign);
const completedMemory =
memoryModule.resolveFirstBattleCamaraderieMemory({
...baseCampaign,
completedCampDialogues: [
memoryModule.firstBattleCamaraderieDialogueId
]
});
const targetBattleId =
memoryModule.firstBattleCamaraderieTargetBattleId;
const guidanceCases = [
{
label: '기억 없음',
options: {
memory: undefined,
nextBattleId: targetBattleId,
selectedCoreBondId: undefined
},
expected: {
kind: 'sortie',
label: '출진 준비',
targetId: null
}
},
{
label: '기억 미완료',
options: {
memory: incompleteMemory,
nextBattleId: targetBattleId,
selectedCoreBondId: undefined
},
expected: {
kind: 'dialogue',
label: '전우 기억 되짚기',
targetId: memoryModule.firstBattleCamaraderieDialogueId
}
},
{
label: '기억 완료 + 핵심조 미지정',
options: {
memory: completedMemory,
nextBattleId: targetBattleId,
selectedCoreBondId: undefined
},
expected: {
kind: 'sortie',
label: '전우 조 편성하기',
targetId: null
}
},
{
label: '기억 완료 + 정확한 핵심조 지정',
options: {
memory: completedMemory,
nextBattleId: targetBattleId,
selectedCoreBondId: snapshot.bondId
},
expected: {
kind: 'sortie',
label: '출진 준비',
targetId: null
}
}
];
guidanceCases.forEach((testCase) => {
const actual =
memoryModule.resolveFirstCampCamaraderieGuidedAction(
testCase.options
);
assert(
JSON.stringify(actual) === JSON.stringify(testCase.expected),
`${testCase.label}: 안내 동작이 예상과 다릅니다. ${JSON.stringify(actual)}`
);
});
const commonGuardOptions = {
nextBattleId: targetBattleId,
selectedUnitIds: ['zhang-fei', 'guan-yu', 'liu-bei']
};
const noMemoryGuard =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: undefined
});
assert(
noMemoryGuard.applies === false &&
noMemoryGuard.shouldWarn === false &&
noMemoryGuard.confirmedByRepeat === false,
`기억 없음: 첫 출진을 막으면 안 됩니다. ${JSON.stringify(noMemoryGuard)}`
);
const incompleteFirst =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: incompleteMemory
});
const incompleteSecond =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: incompleteMemory,
selectedUnitIds: ['liu-bei', 'guan-yu', 'zhang-fei'],
previousSignature: incompleteFirst.signature ?? undefined
});
assert(
incompleteFirst.applies === true &&
incompleteFirst.shouldWarn === true &&
incompleteFirst.notice.includes('전우 기억 미완료') &&
incompleteFirst.notice.includes('유비↔장비') &&
incompleteFirst.notice.includes('+5%p') &&
incompleteFirst.notice.includes('출진을 한 번 더') &&
incompleteSecond.applies === true &&
incompleteSecond.shouldWarn === false &&
incompleteSecond.confirmedByRepeat === true &&
incompleteSecond.signature === incompleteFirst.signature,
`기억 미완료: 첫 클릭 경고 뒤 같은 조건의 둘째 클릭은 통과해야 합니다. ${JSON.stringify({
first: incompleteFirst,
second: incompleteSecond
})}`
);
const unselectedFirst =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: completedMemory
});
const unselectedSecond =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: completedMemory,
previousSignature: unselectedFirst.signature ?? undefined
});
assert(
unselectedFirst.applies === true &&
unselectedFirst.shouldWarn === true &&
unselectedFirst.notice.includes('전우 기억 미지정') &&
unselectedFirst.notice.includes('유비↔장비') &&
unselectedFirst.notice.includes('+5%p') &&
unselectedFirst.notice.includes('출진을 한 번 더') &&
unselectedSecond.applies === true &&
unselectedSecond.shouldWarn === false &&
unselectedSecond.confirmedByRepeat === true &&
unselectedSecond.signature === unselectedFirst.signature,
`기억 완료 + 핵심조 미지정: 첫 클릭 경고 뒤 같은 조건의 둘째 클릭은 통과해야 합니다. ${JSON.stringify({
first: unselectedFirst,
second: unselectedSecond
})}`
);
const exactPairGuard =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: completedMemory,
selectedCoreBondId: snapshot.bondId
});
assert(
exactPairGuard.applies === false &&
exactPairGuard.shouldWarn === false &&
exactPairGuard.confirmedByRepeat === false,
`기억 완료 + 정확한 핵심조 지정: 첫 출진을 막으면 안 됩니다. ${JSON.stringify(exactPairGuard)}`
);
const changedRosterGuard =
memoryModule.resolveFirstBattleCamaraderieSortieGuard({
...commonGuardOptions,
memory: completedMemory,
selectedUnitIds: ['liu-bei', 'guan-yu'],
previousSignature: unselectedFirst.signature ?? undefined
});
assert(
changedRosterGuard.shouldWarn === true &&
changedRosterGuard.signature !== unselectedFirst.signature,
`첫 경고 뒤 편성이 달라지면 오래된 확인을 재사용하면 안 됩니다. ${JSON.stringify(changedRosterGuard)}`
);
}
function verifyCampaignRoundTrip(
memoryModule,
campaignModule,