From 4321ba591ece9fc3c7f063cc1d9b0b827bd525a5 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 29 Jul 2026 11:42:11 +0900 Subject: [PATCH] fix: prevent camp save click-through --- scripts/verify-interaction-ux.mjs | 70 +++++++++++++++++++--- scripts/verify-manual-save-ux-browser.mjs | 50 ++++++++++------ src/game/scenes/CampScene.ts | 73 ++++++++++++++++++----- 3 files changed, 152 insertions(+), 41 deletions(-) diff --git a/scripts/verify-interaction-ux.mjs b/scripts/verify-interaction-ux.mjs index f756d62..d9a041f 100644 --- a/scripts/verify-interaction-ux.mjs +++ b/scripts/verify-interaction-ux.mjs @@ -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)); } diff --git a/scripts/verify-manual-save-ux-browser.mjs b/scripts/verify-manual-save-ux-browser.mjs index 6c03db2..26189f0 100644 --- a/scripts/verify-manual-save-ux-browser.mjs +++ b/scripts/verify-manual-save-ux-browser.mjs @@ -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 }); } diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index d202ca1..34b82e5 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -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; }