feat: stage sortie preparation across campaign

This commit is contained in:
2026-07-10 18:58:09 +09:00
parent 4a5a73a220
commit fc34eca8cf
4 changed files with 863 additions and 330 deletions

View File

@@ -170,7 +170,15 @@ try {
await waitForSortiePrep(page);
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-sortie-prep.png`, fullPage: true });
await assertCanvasPainted(page, 'first camp sortie prep');
await advanceFirstSortiePrepStep(page, 'formation');
const firstSortieBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
firstSortieBriefingState?.stagedSortiePrep === true &&
firstSortieBriefingState?.sortiePrepStep === 'briefing' &&
firstSortieBriefingState?.sortiePrimaryAction?.kind === 'next' &&
firstSortieBriefingState?.sortiePrimaryAction?.nextStep === 'formation',
`Expected the first sortie to open at the staged battlefield briefing: ${JSON.stringify(firstSortieBriefingState)}`
);
await advanceSortiePrepStep(page, 'formation');
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-sortie-formation.png`, fullPage: true });
await assertCanvasPainted(page, 'first camp sortie formation');
const firstSortieSynergy = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortieSynergyPreview);
@@ -182,7 +190,7 @@ try {
firstSortieSynergy?.firstPursuitTrinityConfigured === true,
`Expected first sortie to preview three bonds, the strongest effect, and trinity: ${JSON.stringify(firstSortieSynergy)}`
);
await advanceFirstSortiePrepStep(page, 'loadout');
await advanceSortiePrepStep(page, 'loadout');
await page.mouse.click(1116, 656);
await advanceUntilBattle(page, 'second-battle-yellow-turban-pursuit');
await page.screenshot({ path: `${screenshotDir}/rc-second-battle-from-first-camp.png`, fullPage: true });
@@ -360,6 +368,42 @@ try {
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-prep.png`, fullPage: true });
await assertCanvasPainted(page, 'mid camp sortie prep');
const midBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
midBriefingState?.stagedSortiePrep === true &&
midBriefingState?.firstSortiePrep === false &&
midBriefingState?.sortiePrepStep === 'briefing' &&
midBriefingState?.sortiePrimaryAction?.kind === 'next' &&
midBriefingState?.sortiePrimaryAction?.nextStep === 'formation',
`Expected a mid-campaign battle sortie to open at the staged briefing: ${JSON.stringify(midBriefingState)}`
);
assert(
midBriefingState?.sortiePortraitRosterView?.enabled === true &&
midBriefingState?.sortiePortraitRosterView?.total > 8 &&
midBriefingState?.sortiePortraitRosterView?.visibleCount === 8 &&
midBriefingState?.sortiePortraitRosterView?.start === 1 &&
midBriefingState?.sortiePortraitRosterView?.end === 8,
`Expected the mid-campaign staged formation to expose an eight-card portrait roster window: ${JSON.stringify(midBriefingState?.sortiePortraitRosterView)}`
);
await advanceSortiePrepStep(page, 'formation');
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-formation.png`, fullPage: true });
await assertCanvasPainted(page, 'mid camp sortie formation');
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
await page.mouse.click(658, 196);
await page.waitForFunction((previousScroll) => {
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
return view?.scroll > previousScroll;
}, midFormationFirstPage?.scroll ?? 0, { timeout: 30000 });
const midFormationSecondPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
assert(
midFormationSecondPage?.start > 1 && midFormationSecondPage?.end > 8,
`Expected the portrait roster next-page control to reveal later officers: ${JSON.stringify(midFormationSecondPage)}`
);
await page.mouse.click(610, 196);
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView?.scroll === 0, undefined, { timeout: 30000 });
const fullRosterCandidateProbe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp();
@@ -374,6 +418,7 @@ try {
await page.waitForFunction((probe) => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortieFocusedUnitId === probe.candidateId &&
state?.sortiePrepStep === 'formation' &&
state?.sortieFocusedSynergyPreview?.mode === 'waiting' &&
state?.selectedSortieUnitIds?.length === probe.selectedBefore.length &&
!state.selectedSortieUnitIds.includes(probe.candidateId);
@@ -393,6 +438,7 @@ try {
await page.waitForFunction((probe) => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortieVisible === true &&
state?.sortiePrepStep === 'formation' &&
state?.selectedSortieUnitIds?.length === probe.selectedBefore.length - 1 &&
!state.selectedSortieUnitIds.includes(probe.removedUnitId) &&
state?.sortieFocusedSynergyPreview?.mode === 'add';
@@ -429,8 +475,35 @@ try {
const midCampSelectedSortieUnitIds = reserveDoctrineSetup.state?.selectedSortieUnitIds ?? [];
const midCampDeploymentPreview = reserveDoctrineSetup.state?.sortieDeploymentPreview ?? [];
const midCampFormationAssignments = reserveDoctrineSetup.state?.sortieFormationAssignments ?? {};
const midCampItemAssignments = reserveDoctrineSetup.state?.sortieItemAssignments ?? {};
await page.mouse.click(1160, 38);
await waitForSortiePrep(page);
const restoredMidBriefingState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
restoredMidBriefingState?.stagedSortiePrep === true && restoredMidBriefingState?.sortiePrepStep === 'briefing',
`Expected reopening the mid-campaign sortie to restart at briefing: ${JSON.stringify(restoredMidBriefingState)}`
);
const restoredMidFormationState = await advanceSortiePrepStep(page, 'formation');
assert(
restoredMidFormationState?.sortiePlan?.formationCoverageComplete === false &&
JSON.stringify(restoredMidFormationState?.sortieFormationAssignments) === JSON.stringify(midCampFormationAssignments) &&
JSON.stringify(restoredMidFormationState?.sortieItemAssignments) === JSON.stringify(midCampItemAssignments),
`Expected the staged formation screen to preserve role and supply assignments: ${JSON.stringify(restoredMidFormationState)}`
);
const restoredMidLoadoutState = await advanceSortiePrepStep(page, 'loadout');
assertSameMembers(
restoredMidLoadoutState?.selectedSortieUnitIds,
midCampSelectedSortieUnitIds,
`Expected briefing, formation, and loadout steps to preserve the selected mid-campaign roster: ${JSON.stringify(restoredMidLoadoutState?.selectedSortieUnitIds)}`
);
assert(
JSON.stringify(restoredMidLoadoutState?.sortieFormationAssignments) === JSON.stringify(midCampFormationAssignments) &&
JSON.stringify(restoredMidLoadoutState?.sortieItemAssignments) === JSON.stringify(midCampItemAssignments),
`Expected loadout navigation to preserve role and supply assignments: ${JSON.stringify(restoredMidLoadoutState)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-loadout.png`, fullPage: true });
await assertCanvasPainted(page, 'mid camp sortie loadout');
await page.mouse.click(1116, 656);
await advanceUntilBattle(page, midCampNextBattleId);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-next-battle.png`, fullPage: true });
@@ -489,6 +562,7 @@ try {
const finalCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(finalCampState?.campaign?.step === 'sixty-sixth-camp', `Expected final camp save to continue: ${JSON.stringify(finalCampState)}`);
assert(finalCampState.sortieHasBattle === false, `Expected final camp to expose no playable sortie target: ${JSON.stringify(finalCampState)}`);
assert(finalCampState.stagedSortiePrep === false, `Expected the non-battle final camp to avoid the three-stage sortie flow: ${JSON.stringify(finalCampState)}`);
assert(finalCampState.nextSortieBattleId === null, `Expected final camp to route to ending instead of a battle: ${JSON.stringify(finalCampState)}`);
assert(
Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0,
@@ -675,12 +749,24 @@ async function waitForSortiePrep(page) {
}, undefined, { timeout: 30000 });
}
async function advanceFirstSortiePrepStep(page, expectedStep) {
async function advanceSortiePrepStep(page, expectedStep) {
await page.mouse.click(1116, 656);
await page.waitForFunction((step) => {
const camp = window.__HEROS_DEBUG__?.camp?.();
return camp?.sortieVisible === true && camp?.sortiePrepStep === step;
}, expectedStep, { timeout: 30000 });
await page.waitForTimeout(1000);
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp?.());
const expectedNextStep = expectedStep === 'formation' ? 'loadout' : null;
assert(
state?.stagedSortiePrep === true &&
state?.sortiePrepSteps?.filter((step) => step.active).length === 1 &&
state?.sortiePrepSteps?.some((step) => step.id === expectedStep && step.active) &&
state?.sortiePrimaryAction?.kind === (expectedNextStep ? 'next' : 'launch') &&
state?.sortiePrimaryAction?.nextStep === expectedNextStep,
`Expected staged sortie navigation to reach ${expectedStep}: ${JSON.stringify(state)}`
);
return state;
}
async function waitForCampAfterBattleResult(page) {