feat: turn first victory recall into camp preparation
This commit is contained in:
334
scripts/verify-first-battle-camp-followup.mjs
Normal file
334
scripts/verify-first-battle-camp-followup.mjs
Normal file
@@ -0,0 +1,334 @@
|
||||
import { createServer } from 'vite';
|
||||
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
server: { middlewareMode: true },
|
||||
appType: 'custom'
|
||||
});
|
||||
|
||||
try {
|
||||
const followup = await server.ssrLoadModule(
|
||||
'/src/game/data/firstBattleCampFollowup.ts'
|
||||
);
|
||||
const narrative = await server.ssrLoadModule(
|
||||
'/src/game/data/firstBattleNarrativeMemory.ts'
|
||||
);
|
||||
const orders = await server.ssrLoadModule('/src/game/data/sortieOrders.ts');
|
||||
const failures = [];
|
||||
|
||||
const missedCases = [
|
||||
{
|
||||
orderId: 'elite',
|
||||
criterionId: 'elite-grade',
|
||||
criterionLabel: '편성 평가 A 이상',
|
||||
mentorUnitId: 'liu-bei',
|
||||
mentorName: '유비',
|
||||
targetDialogueId: 'liu-guan-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'elite',
|
||||
criterionId: 'elite-survival',
|
||||
criterionLabel: '출전 전원 생존',
|
||||
mentorUnitId: 'guan-yu',
|
||||
mentorName: '관우',
|
||||
targetDialogueId: 'liu-guan-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'mobile',
|
||||
criterionId: 'mobile-quick',
|
||||
criterionLabel: '제한 턴 이내 승리',
|
||||
mentorUnitId: 'zhang-fei',
|
||||
mentorName: '장비',
|
||||
targetDialogueId: 'liu-zhang-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'mobile',
|
||||
criterionId: 'mobile-breakthrough',
|
||||
criterionLabel: '돌파 기여 1회',
|
||||
mentorUnitId: 'zhang-fei',
|
||||
mentorName: '장비',
|
||||
targetDialogueId: 'liu-zhang-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'siege',
|
||||
criterionId: 'siege-objective',
|
||||
criterionLabel: '거점·보너스 목표 달성',
|
||||
mentorUnitId: 'guan-yu',
|
||||
mentorName: '관우',
|
||||
targetDialogueId: 'guan-zhang-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'siege',
|
||||
criterionId: 'siege-front',
|
||||
criterionLabel: '전열 기여 1 이상',
|
||||
mentorUnitId: 'guan-yu',
|
||||
mentorName: '관우',
|
||||
targetDialogueId: 'guan-zhang-after-first-battle'
|
||||
},
|
||||
{
|
||||
orderId: 'siege',
|
||||
criterionId: 'siege-support',
|
||||
criterionLabel: '후원 기여 1 이상',
|
||||
mentorUnitId: 'guan-yu',
|
||||
mentorName: '관우',
|
||||
targetDialogueId: 'guan-zhang-after-first-battle'
|
||||
}
|
||||
];
|
||||
|
||||
for (const testCase of missedCases) {
|
||||
const report = makeReport(
|
||||
narrative.firstBattleNarrativeBattleId,
|
||||
orders.sortieOrderCheckIdsByOrder,
|
||||
testCase.orderId,
|
||||
false,
|
||||
[testCase.criterionId]
|
||||
);
|
||||
const snapshot = JSON.stringify(report);
|
||||
deepFreeze(report);
|
||||
const result = followup.resolveFirstBattleCampFollowup(report);
|
||||
const label = `${testCase.orderId}/${testCase.criterionId}`;
|
||||
|
||||
assert(result.source === 'campaign', `${label}: campaign result required`, failures);
|
||||
if (result.source !== 'campaign') {
|
||||
continue;
|
||||
}
|
||||
assert(result.fallbackReason === null, `${label}: fallback reason must be null`, failures);
|
||||
assert(result.originalOrderId === testCase.orderId, `${label}: original order`, failures);
|
||||
assert(result.achieved === false, `${label}: missed status`, failures);
|
||||
assert(result.focusCriterionId === testCase.criterionId, `${label}: focus criterion`, failures);
|
||||
assert(result.focusCriterionLabel === testCase.criterionLabel, `${label}: focus label`, failures);
|
||||
assert(result.mentorUnitId === testCase.mentorUnitId, `${label}: mentor unit`, failures);
|
||||
assert(result.mentorName === testCase.mentorName, `${label}: mentor name`, failures);
|
||||
assert(
|
||||
result.targetDialogueId === testCase.targetDialogueId,
|
||||
`${label}: dialogue mapping`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.recommendedNextOrderId === testCase.orderId,
|
||||
`${label}: missed order must be retried`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.recommendationReason.includes(testCase.criterionLabel),
|
||||
`${label}: recommendation must name missed criterion`,
|
||||
failures
|
||||
);
|
||||
verifyPresentation(result, label, failures);
|
||||
assert(JSON.stringify(report) === snapshot, `${label}: source report must remain immutable`, failures);
|
||||
}
|
||||
|
||||
const expectedDialogueByOrder = {
|
||||
elite: 'liu-guan-after-first-battle',
|
||||
mobile: 'liu-zhang-after-first-battle',
|
||||
siege: 'guan-zhang-after-first-battle'
|
||||
};
|
||||
for (const orderId of orders.sortieOrderIds) {
|
||||
const report = makeReport(
|
||||
narrative.firstBattleNarrativeBattleId,
|
||||
orders.sortieOrderCheckIdsByOrder,
|
||||
orderId,
|
||||
true,
|
||||
[]
|
||||
);
|
||||
const result = followup.resolveFirstBattleCampFollowup(report);
|
||||
const label = `${orderId}/achieved`;
|
||||
|
||||
assert(result.source === 'campaign', `${label}: campaign result required`, failures);
|
||||
if (result.source !== 'campaign') {
|
||||
continue;
|
||||
}
|
||||
assert(result.achieved === true, `${label}: achieved status`, failures);
|
||||
assert(
|
||||
result.recommendedNextOrderId === 'mobile',
|
||||
`${label}: pursuit context must recommend mobile`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.focusCriterionId === 'mobile-quick',
|
||||
`${label}: pursuit focus must be turn limit`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.targetDialogueId === expectedDialogueByOrder[orderId],
|
||||
`${label}: original order dialogue mapping`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.recommendationReason.includes('잔당 추격전'),
|
||||
`${label}: recommendation must explain the next battle context`,
|
||||
failures
|
||||
);
|
||||
verifyPresentation(result, label, failures);
|
||||
}
|
||||
|
||||
const firstMissedReport = makeReport(
|
||||
narrative.firstBattleNarrativeBattleId,
|
||||
orders.sortieOrderCheckIdsByOrder,
|
||||
'siege',
|
||||
false,
|
||||
['siege-objective', 'siege-support']
|
||||
);
|
||||
const firstMissed = followup.resolveFirstBattleCampFollowup(firstMissedReport);
|
||||
assert(
|
||||
firstMissed.source === 'campaign' &&
|
||||
firstMissed.focusCriterionId === 'siege-objective',
|
||||
'multiple misses must focus the first authored criterion',
|
||||
failures
|
||||
);
|
||||
|
||||
const malformedCases = [
|
||||
{
|
||||
label: 'missing report',
|
||||
report: undefined,
|
||||
reason: 'missing-report'
|
||||
},
|
||||
{
|
||||
label: 'old save without sortie order',
|
||||
report: {
|
||||
battleId: narrative.firstBattleNarrativeBattleId,
|
||||
outcome: 'victory'
|
||||
},
|
||||
reason: 'missing-order'
|
||||
},
|
||||
{
|
||||
label: 'different battle',
|
||||
report: {
|
||||
battleId: 'second-battle-yellow-turban-pursuit',
|
||||
outcome: 'victory'
|
||||
},
|
||||
reason: 'battle-mismatch'
|
||||
},
|
||||
{
|
||||
label: 'defeat',
|
||||
report: {
|
||||
battleId: narrative.firstBattleNarrativeBattleId,
|
||||
outcome: 'defeat'
|
||||
},
|
||||
reason: 'non-victory'
|
||||
},
|
||||
{
|
||||
label: 'missing progress criterion',
|
||||
report: {
|
||||
battleId: narrative.firstBattleNarrativeBattleId,
|
||||
outcome: 'victory',
|
||||
sortieOrder: {
|
||||
orderId: 'elite',
|
||||
achieved: false,
|
||||
progress: [{ id: 'victory', achieved: true }]
|
||||
}
|
||||
},
|
||||
reason: 'invalid-order-progress'
|
||||
},
|
||||
{
|
||||
label: 'inconsistent achieved flag',
|
||||
report: makeReport(
|
||||
narrative.firstBattleNarrativeBattleId,
|
||||
orders.sortieOrderCheckIdsByOrder,
|
||||
'mobile',
|
||||
true,
|
||||
['mobile-quick']
|
||||
),
|
||||
reason: 'invalid-order-progress'
|
||||
},
|
||||
{
|
||||
label: 'inconsistent missed flag',
|
||||
report: makeReport(
|
||||
narrative.firstBattleNarrativeBattleId,
|
||||
orders.sortieOrderCheckIdsByOrder,
|
||||
'siege',
|
||||
false,
|
||||
[]
|
||||
),
|
||||
reason: 'invalid-order-progress'
|
||||
}
|
||||
];
|
||||
|
||||
for (const testCase of malformedCases) {
|
||||
const snapshot = JSON.stringify(testCase.report);
|
||||
if (testCase.report) {
|
||||
deepFreeze(testCase.report);
|
||||
}
|
||||
const result = followup.resolveFirstBattleCampFollowup(testCase.report);
|
||||
assert(result.source === 'fallback', `${testCase.label}: fallback required`, failures);
|
||||
assert(
|
||||
result.source === 'fallback' && result.fallbackReason === testCase.reason,
|
||||
`${testCase.label}: expected ${testCase.reason}`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(testCase.report) === snapshot,
|
||||
`${testCase.label}: fallback must not mutate report`,
|
||||
failures
|
||||
);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(
|
||||
`First-battle camp follow-up verification failed:\n- ${failures.join('\n- ')}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`First-battle camp follow-up verification passed (${missedCases.length} missed mappings, ${orders.sortieOrderIds.length} achieved pursuit mappings).`
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
function makeReport(
|
||||
battleId,
|
||||
checkIdsByOrder,
|
||||
orderId,
|
||||
achieved,
|
||||
missedCriterionIds
|
||||
) {
|
||||
return {
|
||||
battleId,
|
||||
outcome: 'victory',
|
||||
turnNumber: 6,
|
||||
mvp: { unitId: 'guan-yu', name: '관우' },
|
||||
sortieOrder: {
|
||||
orderId,
|
||||
achieved,
|
||||
progress: checkIdsByOrder[orderId].map((id) => ({
|
||||
id,
|
||||
achieved: id === 'victory' || !missedCriterionIds.includes(id),
|
||||
value: id === 'victory' || !missedCriterionIds.includes(id) ? 1 : 0,
|
||||
target: 1
|
||||
}))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function verifyPresentation(result, label, failures) {
|
||||
assert(result.dialogueTitle.trim().length >= 4, `${label}: dialogue title`, failures);
|
||||
assert(result.dialogueLines.length === 3, `${label}: exactly three dialogue lines`, failures);
|
||||
assert(
|
||||
result.dialogueLines.every((line) => line.includes(':') && line.length <= 72),
|
||||
`${label}: concise speaker-labelled dialogue`,
|
||||
failures
|
||||
);
|
||||
assert(
|
||||
result.dialogueLines[0].startsWith(`${result.mentorName}:`),
|
||||
`${label}: mentor must open the dialogue`,
|
||||
failures
|
||||
);
|
||||
assert(result.recommendationTitle.trim().length >= 4, `${label}: recommendation title`, failures);
|
||||
assert(result.recommendationReason.trim().length >= 12, `${label}: recommendation reason`, failures);
|
||||
assert(result.statusLabel.trim().length >= 4, `${label}: status label`, failures);
|
||||
}
|
||||
|
||||
function assert(condition, message, failures) {
|
||||
if (!condition) {
|
||||
failures.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
function deepFreeze(value) {
|
||||
if (!value || typeof value !== 'object' || Object.isFrozen(value)) {
|
||||
return value;
|
||||
}
|
||||
Object.freeze(value);
|
||||
Object.values(value).forEach(deepFreeze);
|
||||
return value;
|
||||
}
|
||||
@@ -2667,6 +2667,138 @@ try {
|
||||
`Expected camp tab hover styling to reset on pointerout: ${JSON.stringify(firstCampTabResetState?.campTabs)}`
|
||||
);
|
||||
|
||||
await page.mouse.click(firstCampDialogueTabPoint.x, firstCampDialogueTabPoint.y);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.camp()?.activeTab === 'dialogue',
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const firstVictoryDialogueProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const followup = state?.firstVictoryFollowup;
|
||||
const renderedDialogueText = followup?.dialogueLines?.join('\n') ?? null;
|
||||
const activeTexts = (scene?.contentObjects ?? []).filter(
|
||||
(object) => object?.type === 'Text' && object.active && object.visible
|
||||
);
|
||||
const boundsFor = (object) => {
|
||||
const bounds = object?.getBounds?.();
|
||||
return bounds
|
||||
? { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height }
|
||||
: null;
|
||||
};
|
||||
return {
|
||||
state,
|
||||
dialogueTabBadgeText:
|
||||
scene?.tabButtons?.find((button) => button.tab === 'dialogue')?.newBadge?.text ?? null,
|
||||
rowMarkers: activeTexts
|
||||
.filter((object) => object.text === '회고')
|
||||
.map((object) => ({ text: object.text, bounds: boundsFor(object) })),
|
||||
renderedDialogueBlocks: renderedDialogueText
|
||||
? activeTexts
|
||||
.filter((object) => object.text === renderedDialogueText)
|
||||
.map((object) => ({ text: object.text, bounds: boundsFor(object) }))
|
||||
: []
|
||||
};
|
||||
});
|
||||
const firstVictoryFollowup = firstVictoryDialogueProbe.state?.firstVictoryFollowup;
|
||||
const firstVictorySelectedDialogue = firstVictoryDialogueProbe.state?.selectedDialogue;
|
||||
assert(
|
||||
firstVictoryDialogueProbe.state?.activeTab === 'dialogue' &&
|
||||
firstVictoryDialogueProbe.state?.campTabs?.find((tab) => tab.id === 'dialogue')?.newBadgeVisible === true &&
|
||||
firstVictoryDialogueProbe.dialogueTabBadgeText === '회고' &&
|
||||
firstVictoryFollowup?.available === true &&
|
||||
firstVictoryFollowup.sourceBattleId === persistedFirstReport?.battleId &&
|
||||
firstVictoryFollowup.originalOrderId === persistedFirstReport?.sortieOrder?.orderId &&
|
||||
firstVictoryFollowup.originalOrderId === 'elite' &&
|
||||
firstVictoryFollowup.achieved === persistedFirstReport?.sortieOrder?.achieved &&
|
||||
firstVictoryFollowup.achieved === true &&
|
||||
firstVictoryFollowup.targetDialogueId === 'liu-guan-after-first-battle' &&
|
||||
firstVictoryFollowup.recommendedOrderId === 'mobile' &&
|
||||
firstVictoryFollowup.dialogueLines?.length === 3 &&
|
||||
firstVictoryFollowup.dialogueLines.some((line) => line.includes('정예 군령')) &&
|
||||
firstVictoryFollowup.dialogueLines.some((line) => line.includes('빠르게 추격')) &&
|
||||
firstVictorySelectedDialogue?.id === firstVictoryFollowup.targetDialogueId &&
|
||||
firstVictorySelectedDialogue.firstVictoryFollowup === true &&
|
||||
sameJsonValue(firstVictorySelectedDialogue.unitIds, ['liu-bei', 'guan-yu']) &&
|
||||
sameJsonValue(firstVictorySelectedDialogue.lines, firstVictoryFollowup.dialogueLines) &&
|
||||
firstVictoryDialogueProbe.renderedDialogueBlocks.length === 1 &&
|
||||
firstVictoryDialogueProbe.rowMarkers.length === 1 &&
|
||||
boundsInside(
|
||||
firstVictoryDialogueProbe.rowMarkers[0]?.bounds,
|
||||
firstVictoryFollowup.dialogueRowBounds
|
||||
) &&
|
||||
firstVictorySelectedDialogue.choices?.length === 2 &&
|
||||
isFiniteBounds(firstVictorySelectedDialogue.bodyBounds) &&
|
||||
firstVictorySelectedDialogue.bodyBounds.y + firstVictorySelectedDialogue.bodyBounds.height <=
|
||||
firstVictorySelectedDialogue.choices[0]?.bounds?.y - 8 &&
|
||||
firstVictorySelectedDialogue.choices.every((choice) => boundsWithinFhdViewport(choice.bounds)),
|
||||
`Expected the first-camp dialogue tab to render one marked Liu Bei/Guan Yu follow-up from the persisted elite victory: ${JSON.stringify(firstVictoryDialogueProbe)}`
|
||||
);
|
||||
await page.screenshot({
|
||||
path: `${screenshotDir}/rc-first-camp-victory-followup-dialogue.png`,
|
||||
fullPage: true
|
||||
});
|
||||
await assertCanvasPainted(page, 'first camp victory follow-up dialogue');
|
||||
const firstVictoryChoice = firstVictorySelectedDialogue.choices[0];
|
||||
const firstVictoryChoicePoint = await readFirstVictoryFollowupControlPoint(
|
||||
page,
|
||||
'choice',
|
||||
firstVictoryChoice.id
|
||||
);
|
||||
await page.mouse.click(firstVictoryChoicePoint.x, firstVictoryChoicePoint.y);
|
||||
await page.waitForFunction(
|
||||
({ dialogueId, choiceId }) => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp();
|
||||
return (
|
||||
camp?.firstVictoryFollowup?.dialogueCompleted === true &&
|
||||
camp?.selectedDialogue?.completed === true &&
|
||||
camp?.campaign?.campDialogueChoiceIds?.[dialogueId] === choiceId
|
||||
);
|
||||
},
|
||||
{
|
||||
dialogueId: firstVictoryFollowup.targetDialogueId,
|
||||
choiceId: firstVictoryChoice.id
|
||||
},
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const completedFirstVictoryDialogueProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
return {
|
||||
state: window.__HEROS_DEBUG__?.camp(),
|
||||
remainingRowMarkerCount: (scene?.contentObjects ?? []).filter(
|
||||
(object) => object?.type === 'Text' && object.active && object.visible && object.text === '회고'
|
||||
).length
|
||||
};
|
||||
});
|
||||
const completedFirstVictoryDialogueSave = await readCampaignSave(page);
|
||||
const firstVictoryDialoguePersisted = (save) => (
|
||||
save?.completedCampDialogues?.includes(firstVictoryFollowup.targetDialogueId) &&
|
||||
save?.firstBattleReport?.completedCampDialogues?.includes(firstVictoryFollowup.targetDialogueId) &&
|
||||
save?.campDialogueChoiceIds?.[firstVictoryFollowup.targetDialogueId] === firstVictoryChoice.id
|
||||
);
|
||||
assert(
|
||||
completedFirstVictoryDialogueProbe.state?.firstVictoryFollowup?.dialogueCompleted === true &&
|
||||
completedFirstVictoryDialogueProbe.state?.selectedDialogue?.completed === true &&
|
||||
completedFirstVictoryDialogueProbe.state?.completedAvailableDialogues?.includes(
|
||||
firstVictoryFollowup.targetDialogueId
|
||||
) &&
|
||||
completedFirstVictoryDialogueProbe.state?.campaign?.campDialogueChoiceIds?.[
|
||||
firstVictoryFollowup.targetDialogueId
|
||||
] === firstVictoryChoice.id &&
|
||||
completedFirstVictoryDialogueProbe.state?.campTabs?.find(
|
||||
(tab) => tab.id === 'dialogue'
|
||||
)?.newBadgeVisible === false &&
|
||||
completedFirstVictoryDialogueProbe.remainingRowMarkerCount === 0 &&
|
||||
firstVictoryDialoguePersisted(completedFirstVictoryDialogueSave.current) &&
|
||||
firstVictoryDialoguePersisted(completedFirstVictoryDialogueSave.slot1),
|
||||
`Expected the actual first-victory choice to persist in current and slot saves and clear both follow-up markers: ${JSON.stringify({
|
||||
probe: completedFirstVictoryDialogueProbe,
|
||||
save: completedFirstVictoryDialogueSave,
|
||||
choiceId: firstVictoryChoice.id
|
||||
})}`
|
||||
);
|
||||
|
||||
const firstCampEvaluation = firstCampProbe.state?.reportFormationEvaluation;
|
||||
const firstCampHistory = firstCampProbe.state?.reportFormationHistory;
|
||||
const firstCampEvaluationSaveBefore = await readCampaignSave(page);
|
||||
@@ -3459,12 +3591,86 @@ try {
|
||||
firstSortieBriefingState?.sortiePrimaryAction?.lineWidth >= 2 &&
|
||||
firstSortieBriefingState?.sortieNextActionGuide?.label?.includes('무장 편성으로 이동') &&
|
||||
boundsWithinFhdViewport(firstSortieBriefingState?.sortieNextActionGuide?.bounds) &&
|
||||
boundsWithinFhdViewport(firstSortieBriefingState?.sortiePrimaryAction?.bounds),
|
||||
boundsWithinFhdViewport(firstSortieBriefingState?.sortiePrimaryAction?.bounds) &&
|
||||
firstSortieBriefingState?.firstVictoryFollowup?.originalOrderId === 'elite' &&
|
||||
firstSortieBriefingState.firstVictoryFollowup.achieved === true &&
|
||||
firstSortieBriefingState.firstVictoryFollowup.recommendedOrderId === 'mobile' &&
|
||||
firstSortieBriefingState.firstVictoryFollowup.ctaInteractive === true &&
|
||||
boundsWithinFhdViewport(firstSortieBriefingState.firstVictoryFollowup.ctaBounds) &&
|
||||
boundsInside(
|
||||
firstSortieBriefingState.firstVictoryFollowup.handoffSummaryBounds,
|
||||
firstSortieBriefingState.firstVictoryFollowup.handoffCardBounds
|
||||
) &&
|
||||
boundsInside(
|
||||
firstSortieBriefingState.firstVictoryFollowup.ctaBounds,
|
||||
firstSortieBriefingState.firstVictoryFollowup.handoffCardBounds
|
||||
),
|
||||
`Expected the first sortie to open at the staged battlefield briefing: ${JSON.stringify(firstSortieBriefingState)}`
|
||||
);
|
||||
await advanceSortiePrepStep(page, 'formation');
|
||||
const firstFollowupOrderCtaPoint = await readFirstVictoryFollowupControlPoint(page, 'cta');
|
||||
await page.mouse.click(firstFollowupOrderCtaPoint.x, firstFollowupOrderCtaPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp?.();
|
||||
return (
|
||||
camp?.sortiePrepStep === 'formation' &&
|
||||
camp?.sortieOperationOrder?.open === true
|
||||
);
|
||||
}, undefined, { timeout: 30000 });
|
||||
const firstFollowupOrderState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const firstFollowupRecommendedCards = firstFollowupOrderState?.sortieOperationOrder?.cards?.filter(
|
||||
(card) => card.recommended
|
||||
) ?? [];
|
||||
const firstFollowupMobileCard = firstFollowupOrderState?.sortieOperationOrder?.cards?.find(
|
||||
(card) => card.id === 'mobile'
|
||||
);
|
||||
assert(
|
||||
firstFollowupOrderState?.sortiePrepStep === 'formation' &&
|
||||
firstFollowupOrderState?.sortieOperationOrder?.open === true &&
|
||||
firstFollowupOrderState.sortieOperationOrder.formationPanelMode === 'order' &&
|
||||
firstFollowupOrderState.sortieOperationOrder.recommendedId === 'mobile' &&
|
||||
firstFollowupOrderState.sortieOperationOrder.selectedId === null &&
|
||||
firstFollowupOrderState.sortieOperationOrder.complete === false &&
|
||||
firstFollowupRecommendedCards.length === 1 &&
|
||||
firstFollowupRecommendedCards[0]?.id === 'mobile' &&
|
||||
firstFollowupMobileCard?.recommended === true &&
|
||||
firstFollowupMobileCard.selected === false &&
|
||||
boundsWithinFhdViewport(firstFollowupMobileCard.cardBounds) &&
|
||||
firstFollowupOrderState?.firstVictoryFollowup?.recommendedOrderId === 'mobile' &&
|
||||
firstFollowupOrderState.firstVictoryFollowup.recommendedOrderSelected === false &&
|
||||
sameJsonValue(
|
||||
firstFollowupOrderState.firstVictoryFollowup.recommendedOrderCardBounds,
|
||||
firstFollowupMobileCard.cardBounds
|
||||
),
|
||||
`Expected the briefing follow-up to open three unselected order choices with only mobile highlighted: ${JSON.stringify(firstFollowupOrderState?.sortieOperationOrder)}`
|
||||
);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-sortie-formation.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'first camp sortie formation');
|
||||
const firstFollowupMobileCardPoint = await readSortieOrderControlPoint(page, 'card', 'mobile');
|
||||
await page.mouse.click(firstFollowupMobileCardPoint.x, firstFollowupMobileCardPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const camp = window.__HEROS_DEBUG__?.camp?.();
|
||||
return (
|
||||
camp?.sortieOperationOrder?.selectedId === 'mobile' &&
|
||||
camp?.sortieOperationOrder?.complete === true
|
||||
);
|
||||
}, undefined, { timeout: 30000 });
|
||||
const selectedFirstFollowupOrderSave = await readCampaignSave(page);
|
||||
const firstFollowupOrderPersisted = (save) => (
|
||||
save?.sortieOrderSelection?.battleId === 'second-battle-yellow-turban-pursuit' &&
|
||||
save?.sortieOrderSelection?.orderId === 'mobile'
|
||||
);
|
||||
assert(
|
||||
firstFollowupOrderPersisted(selectedFirstFollowupOrderSave.current) &&
|
||||
firstFollowupOrderPersisted(selectedFirstFollowupOrderSave.slot1),
|
||||
`Expected the highlighted mobile order to persist for the second battle in current and slot saves: ${JSON.stringify(selectedFirstFollowupOrderSave)}`
|
||||
);
|
||||
const firstFollowupOrderClosePoint = await readSortieOrderControlPoint(page, 'close');
|
||||
await page.mouse.click(firstFollowupOrderClosePoint.x, firstFollowupOrderClosePoint.y);
|
||||
await page.waitForFunction(
|
||||
() => window.__HEROS_DEBUG__?.camp?.()?.sortieOperationOrder?.open === false,
|
||||
undefined,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const firstSortieFormationState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const firstSortieSynergy = firstSortieFormationState?.sortieSynergyPreview;
|
||||
assert(
|
||||
@@ -3476,7 +3682,11 @@ try {
|
||||
firstSortieSynergy?.coveredRoleCount === 3 &&
|
||||
firstSortieSynergy?.strongestBond?.damageBonus === 9 &&
|
||||
firstSortieSynergy?.strongestBond?.chainRate === 18 &&
|
||||
firstSortieSynergy?.firstPursuitTrinityConfigured === true,
|
||||
firstSortieSynergy?.firstPursuitTrinityConfigured === true &&
|
||||
firstSortieFormationState?.sortieOperationOrder?.open === false &&
|
||||
firstSortieFormationState.sortieOperationOrder.selectedId === 'mobile' &&
|
||||
firstSortieFormationState.sortieOperationOrder.complete === true &&
|
||||
firstSortieFormationState?.firstVictoryFollowup?.recommendedOrderSelected === true,
|
||||
`Expected first sortie formation to expose its next action and preview three bonds, the strongest effect, and trinity: ${JSON.stringify(firstSortieFormationState)}`
|
||||
);
|
||||
|
||||
@@ -3915,12 +4125,12 @@ try {
|
||||
const secondBattleProbe = await readBattleDoctrineProbe(page);
|
||||
assert(secondBattleProbe?.battleId === 'second-battle-yellow-turban-pursuit', `Expected first camp sortie to enter second battle: ${JSON.stringify(secondBattleProbe)}`);
|
||||
assert(
|
||||
secondBattleProbe?.sortieOperationOrder?.selectedId === 'elite' && secondBattleProbe.sortieOperationOrder.result === null,
|
||||
`Expected the explicitly declared elite order to reach the next battle unchanged: ${JSON.stringify(secondBattleProbe?.sortieOperationOrder)}`
|
||||
secondBattleProbe?.sortieOperationOrder?.selectedId === 'mobile' && secondBattleProbe.sortieOperationOrder.result === null,
|
||||
`Expected the explicitly accepted mobile follow-up order to reach the next battle unchanged: ${JSON.stringify(secondBattleProbe?.sortieOperationOrder)}`
|
||||
);
|
||||
await assertLiveSortieOrderHud(page, {
|
||||
battleId: 'second-battle-yellow-turban-pursuit',
|
||||
orderId: 'elite',
|
||||
orderId: 'mobile',
|
||||
context: 'second battle'
|
||||
});
|
||||
assertSameMembers(
|
||||
@@ -8574,7 +8784,21 @@ async function assertLiveSortieOrderHud(page, { battleId, orderId, context }) {
|
||||
}, { expectedBattleId: battleId, expectedOrderId: orderId });
|
||||
|
||||
const hud = probe.hud;
|
||||
const requiredCriteriaIds = ['elite-grade', 'elite-survival'];
|
||||
const orderExpectation = {
|
||||
elite: {
|
||||
label: '정예',
|
||||
criteriaIds: ['elite-grade', 'elite-survival']
|
||||
},
|
||||
mobile: {
|
||||
label: '기동',
|
||||
criteriaIds: ['mobile-quick', 'mobile-breakthrough']
|
||||
},
|
||||
siege: {
|
||||
label: '공성',
|
||||
criteriaIds: ['siege-objective', 'siege-front', 'siege-support']
|
||||
}
|
||||
}[orderId];
|
||||
const requiredCriteriaIds = orderExpectation?.criteriaIds ?? [];
|
||||
const criterionIds = hud?.criteria?.map((entry) => entry.id) ?? [];
|
||||
assert(
|
||||
probe.battleId === battleId &&
|
||||
@@ -8588,7 +8812,7 @@ async function assertLiveSortieOrderHud(page, { battleId, orderId, context }) {
|
||||
assert(
|
||||
hud?.visible === true &&
|
||||
hud.orderId === orderId &&
|
||||
typeof hud.label === 'string' && hud.label.includes('정예') &&
|
||||
typeof hud.label === 'string' && hud.label.includes(orderExpectation?.label ?? '') &&
|
||||
typeof hud.victoryLabel === 'string' && hud.victoryLabel.length > 0 &&
|
||||
typeof hud.victoryTone === 'string' && hud.victoryTone.length > 0 &&
|
||||
typeof hud.status === 'string' && hud.status.length > 0 &&
|
||||
@@ -9824,13 +10048,18 @@ async function readCampStaticTextControlPoint(page, label) {
|
||||
async function readCampTabControlPoint(page, tab) {
|
||||
const probe = await page.evaluate((targetTab) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp?.();
|
||||
const canvas = document.querySelector('canvas');
|
||||
const button = scene?.tabButtons?.find((candidate) => candidate.tab === targetTab);
|
||||
const target = button?.bg;
|
||||
if (!scene || !canvas || !target?.active || !target.visible || !target.input?.enabled) {
|
||||
return { ready: false, tab: targetTab, availableTabs: scene?.tabButtons?.map((candidate) => candidate.tab) ?? [] };
|
||||
const tab = state?.campTabs?.find((candidate) => candidate.id === targetTab);
|
||||
const bounds = tab?.bounds;
|
||||
if (!scene || !canvas || !tab?.interactive || !bounds) {
|
||||
return {
|
||||
ready: false,
|
||||
tab: targetTab,
|
||||
availableTabs: state?.campTabs?.map((candidate) => candidate.id) ?? [],
|
||||
bounds: bounds ?? null
|
||||
};
|
||||
}
|
||||
const bounds = target.getBounds();
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
@@ -9849,6 +10078,49 @@ async function readCampTabControlPoint(page, tab) {
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readFirstVictoryFollowupControlPoint(page, control, choiceId) {
|
||||
const probe = await page.evaluate(({ control, choiceId }) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const state = window.__HEROS_DEBUG__?.camp?.();
|
||||
const canvas = document.querySelector('canvas');
|
||||
const followup = state?.firstVictoryFollowup;
|
||||
const choice = state?.selectedDialogue?.choices?.find((candidate) => candidate.id === choiceId);
|
||||
const bounds = control === 'cta' ? followup?.ctaBounds : choice?.bounds;
|
||||
const interactive = control === 'cta'
|
||||
? followup?.ctaInteractive === true
|
||||
: Boolean(choice?.bounds);
|
||||
if (!scene || !canvas || !interactive || !bounds) {
|
||||
return {
|
||||
ready: false,
|
||||
control,
|
||||
choiceId: choiceId ?? null,
|
||||
bounds: bounds ?? null,
|
||||
followup: followup ?? null,
|
||||
availableChoiceIds: state?.selectedDialogue?.choices?.map((candidate) => candidate.id) ?? []
|
||||
};
|
||||
}
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
ready: true,
|
||||
control,
|
||||
choiceId: choiceId ?? null,
|
||||
bounds,
|
||||
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, { control, choiceId });
|
||||
assert(
|
||||
probe.ready === true &&
|
||||
Number.isFinite(probe.x) &&
|
||||
Number.isFinite(probe.y) &&
|
||||
boundsWithinFhdViewport(probe.bounds),
|
||||
`Expected an interactive first-victory ${control} control from debug bounds: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readSortieTextControlPoint(page, label) {
|
||||
const probe = await page.evaluate((targetLabel) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
|
||||
@@ -23,6 +23,7 @@ const checks = [
|
||||
'scripts/verify-camp-reward-data.mjs',
|
||||
'scripts/verify-city-stay-data.mjs',
|
||||
'scripts/verify-prologue-village-data.mjs',
|
||||
'scripts/verify-first-battle-camp-followup.mjs',
|
||||
'scripts/verify-prologue-exploration-asset-data.mjs',
|
||||
'scripts/verify-exploration-character-asset-data.mjs',
|
||||
'scripts/verify-prologue-dialogue-portrait-data.mjs',
|
||||
|
||||
Reference in New Issue
Block a user