feat: add walkable Guangzong sortie camp
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user