feat: add walkable Guangzong sortie camp

This commit is contained in:
2026-07-27 22:37:55 +09:00
parent 43e5533515
commit 9d67e0ae1d
17 changed files with 3519 additions and 558 deletions

View File

@@ -14,6 +14,9 @@ const selectionRecordId = 'third-camp-preparation-priority';
const trackedEnemyUnitId = 'guangzong-main-vanguard-a';
const priorityDialogueId = 'guan-zhang-after-guangzong-road';
const priorityDialogueBondId = 'guan-yu__zhang-fei';
const explorationVisitId = 'third-guangzong-sortie-camp';
const explorationActivityMarkerPrefix =
`${explorationVisitId}:activity:`;
const activeThroughTurn = 3;
const priorityFixtures = [
@@ -126,10 +129,11 @@ async function verifyRenderer(browser, baseUrl, rendererFixture) {
await waitForDebugApi(page);
await assertFhdViewport(page, rendererFixture);
if (rendererFixture.renderer === 'webgl') {
await verifyCampPreparationUi(page);
await assertFhdViewport(page, rendererFixture);
}
await verifyCampPreparationUi(
page,
rendererFixture.renderer
);
await assertFhdViewport(page, rendererFixture);
for (const priority of priorityFixtures) {
await verifyBattlePriority(
@@ -163,7 +167,11 @@ async function verifyRenderer(browser, baseUrl, rendererFixture) {
}
}
async function verifyCampPreparationUi(page) {
async function verifyCampPreparationUi(page, renderer) {
// Let the initial no-save title/story transition finish before replacing
// the campaign fixture, otherwise its already queued scene start can race
// the first exploration round trip.
await page.waitForTimeout(1500);
const seeded = await seedThirdVictory(page, {
acknowledgeActivities: false,
selectPriorityId: null
@@ -190,22 +198,19 @@ async function verifyCampPreparationUi(page) {
});
let camp = await waitForPreparationToggle(page);
assert.equal(camp.sortieVisible, true);
assert.equal(
preparationPriority(camp, 'information').selectable,
true,
'Opening the briefing must count as reviewing the unlocked battle information.'
);
assert.equal(
preparationPriority(camp, 'information').evidence?.category,
'unlocks'
);
assert.equal(
preparationPriority(camp, 'equipment').selectable,
false
);
assert.equal(
preparationPriority(camp, 'companion').selectable,
false
for (const priority of priorityFixtures) {
assert.equal(
preparationPriority(camp, priority.id).selectable,
false,
`Opening the sortie briefing must not falsely complete ${priority.id}.`
);
}
assert.equal(camp.thirdCampExploration.available, true);
assert.equal(camp.thirdCampExploration.entered, false);
assert.equal(camp.thirdCampExploration.mode, 'explore');
assert.deepEqual(
camp.thirdCampExploration.completedActivityIds,
[]
);
assertFiniteBounds(
camp.thirdCampPreparation.toggleButtonBounds,
@@ -220,18 +225,8 @@ async function verifyCampPreparationUi(page) {
assertPreparationBrowserLayout(camp, 'initial browser');
await capture(
page,
'dist/verification-third-camp-preparation-webgl-browser.png'
`dist/verification-third-camp-exploration-${renderer}-locked.png`
);
await selectPreparationCard(page, 'information');
camp = await waitForCampSelection(page, 'information');
assertPreparationSelection(camp, 'information');
await assertCampaignSelectionPersisted(
page,
'information',
'information selection'
);
const equipmentLocked =
preparationPriority(camp, 'equipment');
assert.equal(equipmentLocked.selectable, false);
@@ -240,136 +235,368 @@ async function verifyCampPreparationUi(page) {
'CampScene',
equipmentLocked.actionButtonBounds
);
await page.waitForFunction(
() => {
const state = window.__HEROS_DEBUG__?.camp?.();
return (
state?.activeTab === 'equipment' &&
state?.sortieVisible === false &&
state?.thirdCampPreparation?.priorities?.find(
({ id }) => id === 'equipment'
)?.selectable === true
);
},
undefined,
{ timeout: 30000 }
);
camp = await readCamp(page);
assert.equal(camp.activeTab, 'equipment');
let exploration = await waitForThirdCampExploration(page);
assert.equal(exploration.locationId, explorationVisitId);
assert.equal(exploration.viewport.width, 1920);
assert.equal(exploration.viewport.height, 1080);
assert.equal(
preparationPriority(camp, 'equipment').evidence?.category,
'equipment'
exploration.background.key,
'third-guangzong-sortie-camp-background'
);
assertPreparationSelection(camp, 'information');
camp = await openPreparationBrowser(page);
assertPreparationBrowserLayout(camp, 'equipment unlocked');
await selectPreparationCard(page, 'equipment');
camp = await waitForCampSelection(page, 'equipment');
assertPreparationSelection(camp, 'equipment');
await assertCampaignSelectionPersisted(
page,
'equipment',
'equipment selection'
assert.equal(exploration.background.ready, true);
assert.equal(exploration.background.fallback, false);
assert.equal(exploration.background.sourceWidth, 1920);
assert.equal(exploration.background.sourceHeight, 1080);
assert.equal(exploration.requiredTexturesReady, true);
assert.deepEqual(
exploration.visit.completedActivityIds,
[]
);
const companionLocked =
preparationPriority(camp, 'companion');
assert.equal(companionLocked.selectable, false);
await clickSceneBounds(
page,
'CampScene',
companionLocked.actionButtonBounds
);
await page.waitForFunction(
(dialogueId) => {
const state = window.__HEROS_DEBUG__?.camp?.();
return (
state?.activeTab === 'dialogue' &&
state?.sortieVisible === false &&
state?.selectedDialogue?.id === dialogueId &&
state.selectedDialogue.completed === false &&
state.selectedDialogue.choices?.length > 0
);
},
priorityDialogueId,
{ timeout: 30000 }
);
camp = await readCamp(page);
assert.equal(
camp.selectedDialogue.thirdBattlePriorityReturn,
true,
'The companion activity must route to the exact result-priority return dialogue.'
exploration.visit.hudActivityTitle,
'자유 준비 0 / 3'
);
const dialogueChoice = camp.selectedDialogue.choices[0];
assertFiniteBounds(
dialogueChoice.bounds,
`dialogue choice ${dialogueChoice.id}`
assert.equal(
exploration.visit.hudPriorityLocation,
'아직 정하지 않음'
);
assertBoundsInsideViewport(
dialogueChoice.bounds,
`dialogue choice ${dialogueChoice.id}`
assert.equal(exploration.visit.selectedPriorityId, null);
assert.equal(exploration.storyContext.sourceBattleId, sourceBattleId);
assert.equal(exploration.storyContext.targetBattleId, targetBattleId);
assert.equal(
exploration.storyContext.companionDialogueId,
priorityDialogueId
);
await clickSceneBounds(
page,
'CampScene',
dialogueChoice.bounds
assert.equal(
exploration.storyContext.companionBondId,
priorityDialogueBondId
);
await page.waitForFunction(
({ dialogueId, choiceId }) => {
const state = window.__HEROS_DEBUG__?.camp?.();
return (
state?.selectedDialogue?.id === dialogueId &&
state.selectedDialogue.completed === true &&
state.campaign?.campDialogueChoiceIds?.[dialogueId] ===
choiceId &&
state?.thirdCampPreparation?.priorities?.find(
({ id }) => id === 'companion'
)?.selectable === true
);
},
{
dialogueId: priorityDialogueId,
choiceId: dialogueChoice.id
},
{ timeout: 30000 }
assert.deepEqual(
exploration.actors.map(({ id }) => id).sort(),
[
'companion-guan-yu',
'companion-zhang-fei',
'quartermaster-equipment',
'zou-jing-operations'
]
);
assert(
exploration.actors.every(({ moved }) => moved === false),
'Third-camp NPCs must begin at stable authored positions.'
);
assert.equal(exploration.dialogue.active, true);
assert.equal(exploration.dialogue.portraitVisible, true);
await advanceExplorationDialogue(page);
exploration = await readThirdCampExploration(page);
assert.equal(exploration.dialogue.active, false);
camp = await openPreparationBrowser(page);
assertPreparationBrowserLayout(camp, 'companion unlocked');
const companion = preparationPriority(camp, 'companion');
assert.equal(companion.evidenceKind, 'priority-return-dialogue');
assert.equal(companion.evidence.dialogueId, priorityDialogueId);
assert.equal(companion.evidence.bondId, priorityDialogueBondId);
await selectPreparationCard(page, 'companion');
camp = await waitForCampSelection(page, 'companion');
assertPreparationSelection(camp, 'companion');
await assertCampaignSelectionPersisted(
page,
'companion',
'companion selection'
);
await capture(
page,
'dist/verification-third-camp-preparation-webgl-selected.png'
`dist/verification-third-camp-exploration-${renderer}-arrival.png`
);
const exitStarted = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.('camp-exit') ?? false;
});
assert.equal(
exitStarted,
true,
'The camp exit must remain usable before any optional activity.'
);
await page.waitForFunction(
() =>
window.__HEROS_DEBUG__?.campVisitExploration?.()
?.dialogue?.active === true
);
await advanceExplorationDialogue(page);
try {
await waitForCamp(page, 15000);
} catch (error) {
const state = await page.evaluate(() => ({
exploration:
window.__HEROS_DEBUG__?.campVisitExploration?.(),
camp: window.__HEROS_DEBUG__?.camp?.(),
activeScenes:
window.__HEROS_GAME__?.scene
.getScenes(true)
.map((scene) => scene.scene.key) ?? []
}));
throw new Error(
`Optional third-camp exit did not return to Camp: ${JSON.stringify(
state
)}`,
{ cause: error }
);
}
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
scene?.showSortiePrep?.();
});
camp = await waitForPreparationToggle(page);
const optionalChecklist = camp.sortieChecklist.find(
({ label }) => label === '출진 우선 준비'
);
assert.equal(optionalChecklist?.complete, true);
assert(
optionalChecklist?.detail.includes('보너스 없이 출진 가능'),
'The sortie checklist must explicitly allow sortie without a preparation bonus.'
);
assert.equal(camp.thirdCampPreparation.selectedPriorityId, null);
exploration = await openThirdCampExplorationFromCamp(page);
assert.equal(
exploration.dialogue.active,
false,
'The introduction must not repeat after the first recorded entry.'
);
exploration = await completeThirdCampNpcActivity(
page,
'quartermaster-equipment',
'equipment'
);
assert.equal(exploration.visit.completedActivityCount, 1);
assert.equal(exploration.visit.selectedPriorityId, 'equipment');
assert.equal(
exploration.visit.hudActivityTitle,
'자유 준비 1 / 3'
);
assert.equal(
exploration.visit.hudPriorityLocation,
'장비 정비'
);
assertThirdCampWorldFeedback(exploration, ['equipment']);
assert(
exploration.campaign.completedCampVisits.includes(
`${explorationActivityMarkerPrefix}equipment`
)
);
const partialExit = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.('camp-exit') ?? false;
});
assert.equal(partialExit, true);
await waitForCamp(page);
await page.reload({
waitUntil: 'domcontentloaded',
timeout: 90000
});
await waitForDebugApi(page);
await page.waitForTimeout(1500);
await page.evaluate(async () => {
const game = window.__HEROS_GAME__;
for (const scene of game?.scene.getScenes(true) ?? []) {
game.scene.stop(scene.scene.key);
}
await window.__HEROS_DEBUG__.goToCamp();
});
await waitForCamp(page);
camp = await openPreparationBrowser(page);
assertPreparationBrowserLayout(camp, 're-entered browser');
assertPreparationSelection(camp, 'companion');
await assertCampaignSelectionPersisted(
exploration = await openThirdCampExplorationFromCamp(page);
assert.deepEqual(
exploration.visit.completedActivityIds,
['equipment'],
'A partial activity must survive a Camp round trip and page reload.'
);
assert.equal(exploration.visit.selectedPriorityId, 'equipment');
assertThirdCampWorldFeedback(exploration, ['equipment']);
exploration = await completeThirdCampNpcActivity(
page,
'companion',
're-entered companion selection'
'zou-jing-operations',
'information'
);
assert.deepEqual(
[...exploration.visit.completedActivityIds].sort(),
['equipment', 'information']
);
assert.equal(exploration.visit.selectedPriorityId, 'information');
assertThirdCampWorldFeedback(
exploration,
['equipment', 'information']
);
const companionActor = exploration.actors.find(
({ activityId }) => activityId === 'companion'
);
assert(companionActor, 'Expected a result-specific companion actor.');
const bondExpBefore = await readCampaignBondExp(
page,
exploration.storyContext.companionBondId
);
const companionStarted = await page.evaluate((actorId) => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.(actorId) ?? false;
}, companionActor.id);
assert.equal(companionStarted, true);
await advanceExplorationDialogue(page);
exploration = await readThirdCampExploration(page);
assert.equal(exploration.choice.open, true);
assert.equal(exploration.choice.mode, 'third-companion');
assert.equal(exploration.choice.sourceNpcId, companionActor.id);
assert(exploration.choice.choices.length > 0);
await page.waitForFunction(
() =>
window.__HEROS_DEBUG__?.campVisitExploration?.()
?.choice?.ready === true
);
exploration = await readThirdCampExploration(page);
const companionChoice = exploration.choice.choices[0];
assertFiniteBounds(
companionChoice.bounds,
'third-camp companion choice'
);
assertBoundsInsideViewport(
companionChoice.bounds,
'third-camp companion choice'
);
await clickSceneBounds(
page,
'CampVisitExplorationScene',
companionChoice.bounds
);
await page.waitForFunction(
() => {
const state =
window.__HEROS_DEBUG__?.campVisitExploration?.();
return (
state?.visit?.completedActivityCount === 3 &&
state?.visit?.selectedPriorityId === 'companion'
);
}
);
await advanceExplorationDialogue(page);
exploration = await readThirdCampExploration(page);
assert.deepEqual(
[...exploration.visit.completedActivityIds].sort(),
['companion', 'equipment', 'information']
);
assert.equal(
exploration.visit.hudActivityTitle,
'자유 준비 3 / 3'
);
assert.equal(
exploration.visit.hudPriorityLocation,
'동료 공명'
);
assertThirdCampWorldFeedback(
exploration,
['companion', 'equipment', 'information']
);
assert(
exploration.actors.every(({ moved }) => moved === false),
'Talking to a companion must not teleport any camp actor.'
);
const bondExpAfter = await readCampaignBondExp(
page,
exploration.storyContext.companionBondId
);
assert(
bondExpAfter > bondExpBefore,
'The first companion choice must award its bond experience.'
);
await page.waitForFunction(
() => {
const state =
window.__HEROS_DEBUG__?.campVisitExploration?.();
return (
state?.dialogue?.active === false &&
state?.choice?.open === false &&
state?.navigationPending === false
);
}
);
const companionRepeatStarted = await page.evaluate((actorId) => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.(actorId) ?? false;
}, companionActor.id);
assert.equal(companionRepeatStarted, true);
await advanceExplorationDialogue(page);
const bondExpAfterRepeat = await readCampaignBondExp(
page,
exploration.storyContext.companionBondId
);
assert.equal(
bondExpAfterRepeat,
bondExpAfter,
'Repeating the companion activity must not duplicate bond rewards.'
);
await capture(
page,
`dist/verification-third-camp-exploration-${renderer}-complete.png`
);
const completedExit = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.('camp-exit') ?? false;
});
assert.equal(completedExit, true);
await waitForCamp(page);
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
scene?.showSortiePrep?.();
});
camp = await waitForPreparationToggle(page);
camp = await openPreparationBrowser(page);
assertPreparationBrowserLayout(camp, 'all exploration activities');
for (const priority of priorityFixtures) {
const availability = preparationPriority(camp, priority.id);
assert.equal(availability.selectable, true);
assert.equal(
availability.evidenceKind,
'exploration-activity',
`${priority.id} must be unlocked by its explicit walkable-camp activity marker.`
);
assert.equal(
availability.evidence.visitId,
explorationVisitId
);
assert.equal(
availability.evidence.activityId,
priority.id
);
}
assertPreparationSelection(camp, 'companion');
assertFiniteBounds(
camp.thirdCampPreparation.clearButtonBounds,
'preparation clear button'
);
await clickSceneBounds(
page,
'CampScene',
camp.thirdCampPreparation.clearButtonBounds
);
await page.waitForFunction(
() => {
const state = window.__HEROS_DEBUG__?.camp?.();
return (
state?.thirdCampPreparation?.browserOpen === true &&
state.thirdCampPreparation.selectedPriorityId === null &&
state.thirdCampPreparation.ready === false
);
}
);
camp = await readCamp(page);
const clearedChecklist = camp.sortieChecklist.find(
({ label }) => label === '출진 우선 준비'
);
assert.equal(clearedChecklist?.complete, true);
assert(
clearedChecklist?.detail.includes('보너스 없이 출진 가능')
);
assert.equal(
camp.campaign.completedCampVisits.includes(
@@ -378,6 +605,192 @@ async function verifyCampPreparationUi(page) {
false,
'A mutable preparation selection must not masquerade as a completed camp visit.'
);
assert.equal(
camp.campaign.completedCampVisits.filter((visitId) =>
visitId.startsWith(explorationActivityMarkerPrefix)
).length,
3
);
await assertCampaignSelectionPersisted(
page,
null,
'cleared optional preparation'
);
}
async function waitForThirdCampExploration(page) {
try {
await page.waitForFunction(
(visitId) => {
const state =
window.__HEROS_DEBUG__?.campVisitExploration?.();
return (
state?.scene === 'CampVisitExplorationScene' &&
state?.ready === true &&
state?.locationId === visitId
);
},
explorationVisitId,
{ timeout: 30000 }
);
} catch (error) {
const state = await page.evaluate(() => ({
exploration:
window.__HEROS_DEBUG__?.campVisitExploration?.(),
camp: window.__HEROS_DEBUG__?.camp?.(),
activeScenes:
window.__HEROS_GAME__?.scene
.getScenes(true)
.map((scene) => scene.scene.key) ?? []
}));
throw new Error(
`Third-camp exploration did not become ready: ${JSON.stringify(
state
)}`,
{ cause: error }
);
}
return readThirdCampExploration(page);
}
async function openThirdCampExplorationFromCamp(page) {
let camp = await readCamp(page);
if (camp?.sortieVisible) {
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
scene?.hideSortiePrep?.();
});
}
camp = await readCamp(page);
if (camp?.activeTab !== 'visit') {
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
if (!scene) {
return;
}
scene.activeTab = 'visit';
scene.render();
});
}
await page.waitForFunction(
() => {
const state = window.__HEROS_DEBUG__?.camp?.();
return (
state?.activeTab === 'visit' &&
state?.thirdCampExploration?.available === true &&
state.thirdCampExploration.explorationInteractive === true &&
Boolean(
state.thirdCampExploration.explorationButtonBounds
)
);
}
);
camp = await readCamp(page);
await clickSceneBounds(
page,
'CampScene',
camp.thirdCampExploration.explorationButtonBounds
);
const exploration = await waitForThirdCampExploration(page);
const activeScenes = await page.evaluate(() =>
window.__HEROS_GAME__?.scene
.getScenes(true)
.map((scene) => scene.scene.key) ?? []
);
assert.equal(
activeScenes.includes('CampScene'),
false,
'The walkable visit must replace CampScene instead of rendering over it.'
);
return exploration;
}
async function readThirdCampExploration(page) {
return page.evaluate(() =>
window.__HEROS_DEBUG__.campVisitExploration()
);
}
async function advanceExplorationDialogue(page) {
for (let step = 0; step < 20; step += 1) {
const state = await page.evaluate(() =>
window.__HEROS_DEBUG__?.campVisitExploration?.()
);
if (
state?.active === false ||
state?.choice?.open ||
state?.dialogue?.active === false
) {
return state;
}
const advanced = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugAdvanceDialogue?.() ?? false;
});
assert.equal(
advanced,
true,
'Expected the active exploration dialogue to advance.'
);
}
assert.fail('Exploration dialogue did not finish within 20 advances.');
}
async function completeThirdCampNpcActivity(
page,
actorId,
activityId
) {
const started = await page.evaluate((targetId) => {
const scene = window.__HEROS_GAME__?.scene.getScene(
'CampVisitExplorationScene'
);
return scene?.debugInteractWith?.(targetId) ?? false;
}, actorId);
assert.equal(started, true, `Expected ${actorId} interaction to start.`);
await page.waitForFunction(
() =>
window.__HEROS_DEBUG__?.campVisitExploration?.()
?.dialogue?.active === true
);
await advanceExplorationDialogue(page);
await page.waitForFunction(
(expectedActivityId) =>
window.__HEROS_DEBUG__?.campVisitExploration?.()
?.visit?.completedActivityIds?.includes(
expectedActivityId
),
activityId
);
return readThirdCampExploration(page);
}
function assertThirdCampWorldFeedback(state, completedIds) {
const completed = new Set(completedIds);
for (const feedback of state.worldFeedback) {
assert.equal(
feedback.visible,
completed.has(feedback.activityId),
`${feedback.activityId} world feedback visibility`
);
}
}
async function readCampaignBondExp(page, bondId) {
return page.evaluate(async (targetBondId) => {
const campaignModule = await import(
'/heros_web/src/game/state/campaignState.ts'
);
return (
campaignModule
.getCampaignState()
.bonds.find(({ id }) => id === targetBondId)?.exp ?? 0
);
}, bondId);
}
async function verifyBattlePriority(
@@ -660,6 +1073,12 @@ async function seedThirdVictory(
const actionModule = await import(
'/heros_web/src/game/state/thirdCampPreparationActions.ts'
);
const explorationActionModule = await import(
'/heros_web/src/game/state/thirdCampExplorationActions.ts'
);
const explorationDataModule = await import(
'/heros_web/src/game/data/thirdCampExploration.ts'
);
const preparationModule = await import(
'/heros_web/src/game/data/thirdCampPreparation.ts'
);
@@ -749,23 +1168,42 @@ async function seedThirdVictory(
campaignModule.completeCampaignAftermath(sourceBattleId);
if (acknowledgeActivities) {
campaignModule.acknowledgeCampaignVictoryReward(
sourceBattleId,
['unlocks', 'equipment']
);
const dialogueReport = campaignModule.applyCampBondExp(
priorityDialogueId,
priorityDialogueBondId,
1,
'verify-third-camp-preparation'
);
if (
!dialogueReport?.completedCampDialogues.includes(
priorityDialogueId
)
) {
const entry =
explorationActionModule.enterThirdCampExploration();
if (!entry.ok) {
throw new Error(
'Failed to complete the priority return dialogue seed.'
`Failed to enter third-camp exploration: ${entry.reason}`
);
}
for (const activityId of ['information', 'equipment']) {
const activity =
explorationActionModule.recordThirdCampExplorationActivity(
activityId
);
if (!activity.ok) {
throw new Error(
`Failed to complete ${activityId}: ${activity.reason}`
);
}
}
const definition =
explorationDataModule.resolveThirdCampExplorationDefinition(
campaignModule.getCampaignState()
);
const companionChoiceId =
definition?.companionDialogue.choices[0]?.id;
if (!companionChoiceId) {
throw new Error(
'Failed to resolve the priority return companion choice.'
);
}
const companion =
explorationActionModule.completeThirdCampCompanionActivity(
companionChoiceId
);
if (!companion.ok) {
throw new Error(
`Failed to complete companion activity: ${companion.reason}`
);
}
}
@@ -1128,7 +1566,7 @@ async function waitForDebugApi(page) {
);
}
async function waitForCamp(page) {
async function waitForCamp(page, timeout = 90000) {
await page.waitForFunction(
() => {
const state = window.__HEROS_DEBUG__?.camp?.();
@@ -1143,7 +1581,7 @@ async function waitForCamp(page) {
);
},
undefined,
{ timeout: 90000 }
{ timeout }
);
return readCamp(page);
}
@@ -1169,42 +1607,54 @@ async function waitForPreparationToggle(page) {
}
async function openPreparationBrowser(page) {
const before = await readCamp(page);
if (before?.thirdCampPreparation?.browserOpen) {
return before;
}
if (!before?.sortieVisible) {
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
scene.showSortiePrep();
});
}
const ready = await waitForPreparationToggle(page);
await clickSceneBounds(
page,
'CampScene',
ready.thirdCampPreparation.toggleButtonBounds
);
await page.waitForFunction(
() => {
const preparation =
window.__HEROS_DEBUG__?.camp?.()
?.thirdCampPreparation;
return (
preparation?.browserOpen === true &&
Boolean(preparation.closeButtonBounds) &&
preparation.priorities?.length === 3 &&
preparation.priorities.every(
({ cardBounds, actionButtonBounds }) =>
Boolean(cardBounds && actionButtonBounds)
)
let lastState = await readCamp(page);
for (let attempt = 0; attempt < 2; attempt += 1) {
if (!lastState?.thirdCampPreparation?.browserOpen) {
if (!lastState?.sortieVisible) {
await page.evaluate(() => {
const scene =
window.__HEROS_GAME__?.scene.getScene('CampScene');
scene.showSortiePrep();
});
}
const ready = await waitForPreparationToggle(page);
await clickSceneBounds(
page,
'CampScene',
ready.thirdCampPreparation.toggleButtonBounds
);
},
undefined,
{ timeout: 30000 }
}
await page.waitForFunction(
() => {
const preparation =
window.__HEROS_DEBUG__?.camp?.()
?.thirdCampPreparation;
return (
preparation?.browserOpen === true &&
Boolean(preparation.closeButtonBounds) &&
preparation.priorities?.length === 3 &&
preparation.priorities.every(
({ cardBounds, actionButtonBounds }) =>
Boolean(cardBounds && actionButtonBounds)
)
);
},
undefined,
{ timeout: 30000 }
);
await page.waitForTimeout(150);
lastState = await readCamp(page);
if (lastState?.thirdCampPreparation?.browserOpen) {
return lastState;
}
}
throw new Error(
`Preparation browser did not remain open: ${JSON.stringify({
sortieVisible: lastState?.sortieVisible,
activeTab: lastState?.activeTab,
preparation: lastState?.thirdCampPreparation
})}`
);
return readCamp(page);
}
async function selectPreparationCard(page, priorityId) {
@@ -1384,14 +1834,21 @@ async function assertCampaignSelectionPersisted(
return raw ? JSON.parse(raw) : null;
});
assert(persisted, `${label}: campaign save is missing.`);
assert.deepEqual(
persisted.thirdCampPreparationSelection,
{
sourceBattleId,
targetBattleId,
priorityId
}
);
if (priorityId) {
assert.deepEqual(
persisted.thirdCampPreparationSelection,
{
sourceBattleId,
targetBattleId,
priorityId
}
);
} else {
assert.equal(
persisted.thirdCampPreparationSelection,
undefined
);
}
assert.equal(
persisted.completedCampVisits.includes(
selectionRecordId