fix: prevent camp save click-through

This commit is contained in:
2026-07-29 11:42:11 +09:00
parent 289599149b
commit 4321ba591e
3 changed files with 152 additions and 41 deletions

View File

@@ -42,9 +42,12 @@ const expectedBattleId = 'second-battle-yellow-turban-pursuit';
let serverProcess;
let browser;
const pageErrors = [];
let delayedBattleModuleRequests = 0;
try {
serverProcess = await ensureLocalServer(targetUrl);
await prewarmBattleSceneModule(targetUrl);
browser = await chromium.launch({
headless:
process.env.VERIFY_INTERACTION_UX_HEADLESS !== '0',
@@ -60,10 +63,8 @@ try {
const page = await context.newPage();
page.setDefaultTimeout(30000);
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.stack ?? error.message));
let delayedBattleModuleRequests = 0;
await page.route('**/src/game/scenes/BattleScene.ts*', async (route) => {
delayedBattleModuleRequests += 1;
await delay(1200);
@@ -275,11 +276,49 @@ async function verifyCampModalAndNavigation(page) {
})}`
);
await page.waitForFunction(() => {
const scenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const battle = window.__HEROS_DEBUG__?.battle();
return scenes.includes('BattleScene') && battle?.scene === 'BattleScene';
}, undefined, { timeout: 90000 });
try {
await page.waitForFunction(() => {
const scenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
const battle = window.__HEROS_DEBUG__?.battle();
return scenes.includes('BattleScene') && battle?.scene === 'BattleScene';
}, undefined, { timeout: 90000 });
} catch (error) {
const diagnostic = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const camp = window.__HEROS_DEBUG__?.camp();
return {
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? [],
camp: camp
? {
campaignStep: camp.campaign?.step ?? null,
sortiePrepStep: camp.sortiePrepStep ?? null,
sortiePrimaryAction: camp.sortiePrimaryAction?.kind ?? null,
selectedSortieUnitIds: camp.selectedSortieUnitIds ?? [],
nextSortieBattleId: camp.nextSortieBattleId ?? null,
skipIntroStoryForSortie:
camp.skipIntroStoryForSortie ?? null
}
: null,
navigationPending: scene?.navigationPending ?? null,
navigationBlockerCount: scene?.navigationBlockerObjects?.length ?? null,
saveSlotObjectCount: scene?.saveSlotObjects?.length ?? null,
saveConfirmObjectCount: scene?.saveSlotConfirmObjects?.length ?? null,
sceneKeys: Object.keys(window.__HEROS_GAME__?.scene.keys ?? {}),
resources: performance
.getEntriesByType('resource')
.map((entry) => entry.name)
.filter((name) => name.includes('BattleScene'))
};
});
throw new Error(
`BattleScene did not become active after camp navigation: ${JSON.stringify({
diagnostic,
delayedBattleModuleRequests,
pageErrors
})}`,
{ cause: error }
);
}
const navigationCompleteState = await page.evaluate(() => ({
battleSceneStarts: window.__HEROS_INTERACTION_UX__?.battleSceneStarts ?? 0,
activeScenes: window.__HEROS_DEBUG__?.activeScenes() ?? []
@@ -2260,6 +2299,23 @@ async function canReach(url) {
}
}
async function prewarmBattleSceneModule(url) {
const parsed = new URL(url);
const moduleUrl = new URL(
'/heros_web/src/game/scenes/BattleScene.ts',
parsed.origin
);
const response = await fetch(moduleUrl, {
signal: AbortSignal.timeout(30000)
});
if (!response.ok) {
throw new Error(
`Could not prewarm the BattleScene module (${response.status}) at ${moduleUrl}.`
);
}
await response.arrayBuffer();
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -62,18 +62,25 @@ try {
await waitForUrl(serverUrl, 30_000);
}
browser = await chromium.launch({
headless:
process.env.VERIFY_MANUAL_SAVE_UX_HEADLESS !==
'0',
args: [
'--use-angle=swiftshader',
'--enable-unsafe-swiftshader'
]
});
for (const renderer of renderers) {
await verifyRenderer(browser, renderer);
browser = await chromium.launch({
headless:
process.env.VERIFY_MANUAL_SAVE_UX_HEADLESS !==
'0',
args:
renderer === 'webgl'
? [
'--use-angle=swiftshader',
'--enable-unsafe-swiftshader'
]
: []
});
try {
await verifyRenderer(browser, renderer);
} finally {
await browser.close();
browser = undefined;
}
}
console.info(
@@ -504,7 +511,8 @@ async function verifyCampManualSaveIsolation(
await page.screenshot({
path:
`dist/qa-manual-save-camp-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('Escape');
@@ -748,7 +756,8 @@ async function verifyTitleCampRestore(
await page.screenshot({
path:
`dist/qa-manual-save-title-camp-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('Escape');
await waitForTitleFocus(
@@ -915,7 +924,8 @@ async function verifyTitleCampRestore(
await page.screenshot({
path:
`dist/qa-manual-save-title-confirm-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('Escape');
await waitForTitleFocus(
@@ -1200,7 +1210,8 @@ async function verifyBattleManualSaveIsolation(
await page.screenshot({
path:
`dist/qa-manual-save-battle-save-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('Escape');
await waitForBattleSavePanel(page, null);
@@ -1364,7 +1375,8 @@ async function verifyBattleManualSaveIsolation(
await page.screenshot({
path:
`dist/qa-manual-save-battle-load-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('Escape');
await waitForBattleSavePanel(page, null);
@@ -1437,7 +1449,8 @@ async function verifyTitleBattleRestore(
await page.screenshot({
path:
`dist/qa-manual-save-title-battle-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
await page.keyboard.press('1');
@@ -1571,7 +1584,8 @@ async function verifyTitleBattleRestore(
await page.screenshot({
path:
`dist/qa-manual-save-battle-restored-${renderer}-1920x1080.png`,
fullPage: true
animations: 'disabled',
timeout: 90_000
});
}

View File

@@ -11842,6 +11842,7 @@ export class CampScene extends Phaser.Scene {
musicVolume: this.campSoundscape.music.volume,
ambienceVolume: this.campSoundscape.ambience.volume
});
this.input.setTopOnly(true);
this.input.keyboard?.on('keydown-ESC', (event: KeyboardEvent) => this.handleEscapeKey(event));
this.input.keyboard?.on('keydown-ENTER', (event: KeyboardEvent) => this.handleSortieEnterKey(event));
this.input.keyboard?.on('keydown', (event: KeyboardEvent) => {
@@ -13388,9 +13389,13 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
soundDirector.playSelect();
this.hideCampSaveSlotPanel();
this.deferCampSavePointerAction(
event,
() => {
soundDirector.playSelect();
this.hideCampSaveSlotPanel();
}
);
}
);
shade.on(
@@ -13462,9 +13467,13 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
soundDirector.playSelect();
this.hideCampSaveSlotPanel();
this.deferCampSavePointerAction(
event,
() => {
soundDirector.playSelect();
this.hideCampSaveSlotPanel();
}
);
});
this.trackCampSaveSlot(close);
}
@@ -13537,8 +13546,10 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
this.requestSaveCampToSlot(summary.slot);
this.deferCampSavePointerAction(
event,
() => this.requestSaveCampToSlot(summary.slot)
);
};
[row, title, keycap, keyText, detail, meta, saveLabel].forEach((object) => {
object.setInteractive({ useHandCursor: true });
@@ -13614,9 +13625,13 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
soundDirector.playSelect();
this.hideCampSaveConfirm();
this.deferCampSavePointerAction(
event,
() => {
soundDirector.playSelect();
this.hideCampSaveConfirm();
}
);
}
);
shade.on(
@@ -13690,8 +13705,10 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
this.confirmCampSaveOverwrite();
this.deferCampSavePointerAction(
event,
() => this.confirmCampSaveOverwrite()
);
});
this.trackCampSaveConfirm(confirm);
@@ -13706,9 +13723,13 @@ export class CampScene extends Phaser.Scene {
_localY: number,
event?: { stopPropagation: () => void }
) => {
event?.stopPropagation();
soundDirector.playSelect();
this.hideCampSaveConfirm();
this.deferCampSavePointerAction(
event,
() => {
soundDirector.playSelect();
this.hideCampSaveConfirm();
}
);
});
this.trackCampSaveConfirm(cancel);
}
@@ -13805,6 +13826,20 @@ export class CampScene extends Phaser.Scene {
this.pendingSaveSlot = undefined;
}
private deferCampSavePointerAction(
event:
| { stopPropagation: () => void }
| undefined,
action: () => void
) {
event?.stopPropagation();
queueMicrotask(() => {
if (this.sys.isActive()) {
action();
}
});
}
private settleVictoryRewardArrival(report: CampaignVictoryRewardReport) {
if (report.outcome !== 'victory') {
this.pendingVictoryRewardReport = undefined;
@@ -14911,6 +14946,12 @@ export class CampScene extends Phaser.Scene {
}
private setSortiePrepStep(step: SortiePrepStep) {
if (
this.saveSlotObjects.length > 0 ||
this.saveSlotConfirmObjects.length > 0
) {
return;
}
if (step !== 'briefing') {
this.thirdCampPreparationBrowserOpen = false;
}