fix: preserve pursuit target through finisher

This commit is contained in:
2026-07-29 05:43:07 +09:00
parent c3a80cd343
commit 4c9f299de7
2 changed files with 247 additions and 32 deletions

View File

@@ -73,6 +73,79 @@ async function clickSceneBounds(page, sceneKey, bounds) {
await page.mouse.click(point.x, point.y);
}
async function clickTitleContinue(page, slot = 1) {
const continueItem = await page.evaluate(() => {
const state =
window.__HEROS_DEBUG__
?.scene('TitleScene')
?.getDebugState?.();
return (
state?.focus?.scope === 'main'
? state.focus.items.find(
(item) =>
item.id === 'continue' &&
item.enabled === true
)
: undefined
) ?? null;
});
assert(
continueItem?.bounds,
`Expected an enabled title continue control: ${JSON.stringify(
continueItem
)}`
);
await clickSceneBounds(
page,
'TitleScene',
continueItem.bounds
);
await page.waitForFunction(
() => {
const activeScenes =
window.__HEROS_DEBUG__?.activeScenes() ?? [];
const state =
window.__HEROS_DEBUG__
?.scene('TitleScene')
?.getDebugState?.();
return (
!activeScenes.includes('TitleScene') ||
state?.navigating === true ||
state?.panels?.saveSlots === true
);
},
undefined,
{ timeout: 10000 }
);
const saveSlotItem = await page.evaluate(
(requestedSlot) => {
const state =
window.__HEROS_DEBUG__
?.scene('TitleScene')
?.getDebugState?.();
if (state?.panels?.saveSlots !== true) {
return null;
}
return (
state.focus?.items?.find(
(item) =>
item.id === `slot-${requestedSlot}` &&
item.enabled === true
) ?? null
);
},
slot
);
if (saveSlotItem) {
await clickSceneBounds(
page,
'TitleScene',
saveSlotItem.bounds
);
}
}
async function clickBattleDeploymentStart(page) {
const point = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
@@ -223,7 +296,7 @@ try {
);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForStoryReady(page);
const resumedPrologue = await page.evaluate(() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.());
const resumedPrologueSave = await readCampaignSave(page);
@@ -1398,11 +1471,13 @@ try {
cutInOrder.push(isPrimary ? 'primary' : result === counterResult || result.isCounter ? 'counter' : 'other');
if (isPrimary) {
primaryCutInStarted = true;
this.updateCameraView();
visibleAtCutInStart = viewState();
}
await originalPlayCombatCutIn.call(this, result);
if (isPrimary) {
primaryCutInCompleted = true;
this.updateCameraView();
visibleAtCutInEnd = viewState();
}
};
@@ -1448,6 +1523,10 @@ try {
visibleAtCutInEnd,
defeatApplications,
visibleAfterWrapper: viewState(),
pendingDefeatPresentationAfterWrapper:
scene.pendingDefeatPresentationUnitIds.has(
target.id
),
counterCalls,
counterProduced,
cutInOrder,
@@ -1594,8 +1673,10 @@ try {
productionFinisher.visibleAfterWrapper.spriteVisible === false &&
productionFinisher.visibleAfterWrapper.spriteAlpha === 0 &&
productionFinisher.visibleAfterWrapper.labelVisible === false &&
productionFinisher.visibleAfterWrapper.hitZoneVisible === false &&
productionFinisher.visibleAfterWrapper.miniMapDotVisible === false &&
productionFinisher.counterCalls === 1 &&
productionFinisher.pendingDefeatPresentationAfterWrapper === false &&
productionFinisher.counterCalls === 0 &&
productionFinisher.counterProduced === false &&
productionFinisher.result.counter === false &&
JSON.stringify(productionFinisher.cutInOrder) === JSON.stringify(['primary']),
@@ -2692,7 +2773,7 @@ try {
);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForStoryReady(page);
const resumedFirstAftermath = await page.evaluate(() => window.__HEROS_DEBUG__?.scene('StoryScene')?.getDebugState?.());
const resumedFirstAftermathSave = await readCampaignSave(page);
@@ -5826,7 +5907,7 @@ try {
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
const requiredCasualtyRecruitment = await page.evaluate(() => {
@@ -5893,7 +5974,7 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
const requiredCasualtyCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
@@ -6204,7 +6285,7 @@ try {
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
const midCampaignSelectionFixture = await page.evaluate((requestedUnitIds) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
@@ -7351,7 +7432,7 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
const restoredUnderCapacityState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
@@ -8334,7 +8415,7 @@ try {
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
await waitForCampSkinTransition(page, 'northern');
await waitForCampSoundscapeTransition(page, 'camp-frontier', 'mountain-wind-ambience');
@@ -8476,7 +8557,7 @@ try {
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForCamp(page);
await waitForCampSkinTransition(page, 'northern');
const resumedFinalCampAfterRewardClose = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
@@ -8515,7 +8596,7 @@ try {
await page.keyboard.press('Enter');
await waitForTitle(page);
await clickLegacyUi(page, 962, 310);
await clickTitleContinue(page);
await waitForEnding(page);
await page.screenshot({ path: `${screenshotDir}/rc-ending-continue.png`, fullPage: true });
await assertCanvasPainted(page, 'ending continue');
@@ -8573,8 +8654,28 @@ async function waitForTitle(page) {
await page.waitForFunction(() => window.__HEROS_DEBUG__ !== undefined, undefined, { timeout: 90000 });
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
return activeScenes.includes('TitleScene');
const state =
window.__HEROS_DEBUG__
?.scene('TitleScene')
?.getDebugState?.();
return (
activeScenes.includes('TitleScene') &&
state?.scene === 'TitleScene' &&
state.navigating === false &&
Array.isArray(state.focus?.items) &&
state.focus.items.some(
(item) =>
item.enabled === true &&
Number.isFinite(item.bounds?.x) &&
Number.isFinite(item.bounds?.y) &&
Number.isFinite(item.bounds?.width) &&
Number.isFinite(item.bounds?.height)
)
);
}, undefined, { timeout: 90000 });
await page.evaluate(() => new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve));
}));
}
async function assertReleaseShellReady(page) {
@@ -10641,16 +10742,41 @@ async function waitForBattleOutcome(page, outcome) {
}
async function waitForCamp(page) {
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const scene = window.__HEROS_DEBUG__?.scene('CampScene');
return (
activeScenes.includes('CampScene') &&
!activeScenes.includes('BattleScene') &&
window.__HEROS_DEBUG__?.camp()?.scene === 'CampScene' &&
(scene?.contentObjects?.length ?? 0) > 0
try {
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const scene = window.__HEROS_DEBUG__?.scene('CampScene');
return (
activeScenes.includes('CampScene') &&
!activeScenes.includes('BattleScene') &&
window.__HEROS_DEBUG__?.camp()?.scene === 'CampScene' &&
(scene?.contentObjects?.length ?? 0) > 0
);
}, undefined, { timeout: 90000 });
} catch (error) {
const diagnostic = await page.evaluate(() => ({
activeScenes:
window.__HEROS_DEBUG__?.activeScenes() ?? [],
title:
window.__HEROS_DEBUG__
?.scene('TitleScene')
?.getDebugState?.() ?? null,
battle:
window.__HEROS_DEBUG__?.battle?.() ?? null,
camp:
window.__HEROS_DEBUG__?.camp?.() ?? null,
story:
window.__HEROS_DEBUG__
?.scene('StoryScene')
?.getDebugState?.() ?? null
}));
throw new Error(
`Expected a ready camp scene: ${JSON.stringify(
diagnostic
)}`,
{ cause: error }
);
}, undefined, { timeout: 90000 });
}
await page.evaluate(() => new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve));
}));
@@ -12411,12 +12537,33 @@ async function readBattleDoctrineProbe(page) {
}
async function seedCampaignSave(page, options) {
await page.evaluate(() => {
const game = window.__HEROS_GAME__;
game?.scene
.getScenes(true)
.forEach((scene) =>
game.scene.stop(scene.scene.key)
);
});
await page.evaluate(
() =>
new Promise((resolve) =>
requestAnimationFrame(() =>
requestAnimationFrame(resolve)
)
)
);
await page.evaluate((seed) => {
const storageKey = 'heros-web:campaign-state';
const current = JSON.parse(window.localStorage.getItem(storageKey) ?? 'null');
if (!current?.firstBattleReport) {
throw new Error('Expected an existing campaign report before seeding RC save.');
}
// These fixtures model a completed victory already resting at camp.
// Do not inherit a live battle auto-save or an older navigation checkpoint.
Object.keys(window.localStorage)
.filter((key) => key.startsWith('heros-web:battle:'))
.forEach((key) => window.localStorage.removeItem(key));
const now = new Date().toISOString();
const objectives = [
@@ -12492,6 +12639,12 @@ async function seedCampaignSave(page, options) {
step: seed.step,
activeSaveSlot: 1,
gold: seed.gold,
pendingAftermathBattleId: undefined,
activeCityStayId: undefined,
activeCampVisitId: undefined,
storyCheckpoint: undefined,
explorationCheckpoint: undefined,
sortiePreparationCheckpoint: undefined,
inventory: { ...(current.inventory ?? {}), : 12, 상처약: 6, 탁주: 4 },
selectedSortieUnitIds: seed.selectedSortieUnitIds,
latestBattleId: seed.battleId,
@@ -12516,6 +12669,26 @@ async function seedCampaignSave(page, options) {
window.localStorage.setItem(storageKey, JSON.stringify(next));
window.localStorage.setItem(`${storageKey}:slot-1`, JSON.stringify(next));
const persisted = JSON.parse(
window.localStorage.getItem(
`${storageKey}:slot-1`
) ?? 'null'
);
if (
persisted?.step !== seed.step ||
persisted?.storyCheckpoint ||
persisted?.explorationCheckpoint ||
persisted?.sortiePreparationCheckpoint ||
persisted?.pendingAftermathBattleId ||
persisted?.activeCityStayId ||
persisted?.activeCampVisitId
) {
throw new Error(
`Camp seed retained a navigation checkpoint: ${JSON.stringify(
persisted
)}`
);
}
}, options);
}