fix: prevent dialogue input rebound

This commit is contained in:
2026-07-29 04:30:46 +09:00
parent 6cb65e4335
commit 1f805a718d
2 changed files with 142 additions and 7 deletions

View File

@@ -748,6 +748,10 @@ async function openResonanceChoice(page) {
}
async function openShop(page) {
await waitForCityInteractionIdle(
page,
fixture.marketActorId
);
let interacted = false;
for (let attempt = 0; attempt < 8; attempt += 1) {
interacted = await page.evaluate((actorId) =>
@@ -761,10 +765,18 @@ async function openShop(page) {
}
await page.waitForTimeout(120);
}
const diagnostic = interacted
? null
: await readCityInteractionDiagnostic(
page,
fixture.marketActorId
);
assert.equal(
interacted,
true,
`${renderer}: could not open the city shop.`
`${renderer}: could not open the city shop: ${JSON.stringify(
diagnostic
)}`
);
await waitForShopOpen(page);
const city = await readCity(page);
@@ -864,17 +876,115 @@ function assertDialogue(
async function finishCityDialogue(page) {
for (let attempt = 0; attempt < 12; attempt += 1) {
const city = await readCity(page);
if (!city?.dialogue?.active) {
if (
city?.ready === true &&
city.dialogue?.active === false
) {
await waitForCityInteractionIdle(page);
return;
}
await page.waitForTimeout(150);
await page.keyboard.press('e');
if (city?.dialogue?.active === true) {
await page.waitForTimeout(150);
await page.keyboard.press('e');
} else {
await page.waitForTimeout(150);
}
}
throw new Error(
`${renderer}: city dialogue did not finish.`
);
}
async function waitForCityInteractionIdle(
page,
targetActorId
) {
const expected = { targetActorId };
const waitForIdle = () =>
page.waitForFunction(
({ targetActorId: actorId }) => {
const debug = window.__HEROS_DEBUG__;
const city = debug?.cityStay?.();
return (
debug
?.activeScenes?.()
.includes('CityStayScene') === true &&
city?.scene === 'CityStayScene' &&
city.ready === true &&
city.navigationPending === false &&
city.dialogue?.active === false &&
city.shop?.open === false &&
city.choice?.open === false &&
city.campaignObjectiveJournal?.view?.visible ===
false &&
(!actorId ||
city.actors?.some(
({ id }) => id === actorId
))
);
},
expected,
{ timeout: 30000 }
);
await waitForIdle();
await afterTwoFrames(page);
const stable = await readCityInteractionDiagnostic(
page,
targetActorId
);
assert.equal(
stable.idle,
true,
`${renderer}: city interaction became blocked after reaching idle; the closing key may have reopened a modal: ${JSON.stringify(
stable
)}`
);
}
async function readCityInteractionDiagnostic(
page,
targetActorId
) {
return page.evaluate((actorId) => {
const debug = window.__HEROS_DEBUG__;
const scene = debug?.scene('CityStayScene');
const city = debug?.cityStay?.() ?? null;
const activeScenes = debug?.activeScenes?.() ?? [];
const actor =
city?.actors?.find(({ id }) => id === actorId) ??
null;
const objectiveJournalOpen =
city?.campaignObjectiveJournal?.view?.visible ??
null;
const idle =
activeScenes.includes('CityStayScene') &&
city?.scene === 'CityStayScene' &&
city.ready === true &&
city.navigationPending === false &&
city.dialogue?.active === false &&
city.shop?.open === false &&
city.choice?.open === false &&
objectiveJournalOpen === false &&
(!actorId || Boolean(actor));
return {
idle,
activeScenes,
sceneActive:
scene?.scene?.isActive?.() ?? false,
scenePaused:
scene?.scene?.isPaused?.() ?? false,
ready: city?.ready ?? null,
navigationPending:
city?.navigationPending ?? null,
dialogue: city?.dialogue ?? null,
shopOpen: city?.shop?.open ?? null,
choiceOpen: city?.choice?.open ?? null,
objectiveJournalOpen,
actor
};
}, targetActorId);
}
async function finishCityDialogueToCamp(page) {
for (let attempt = 0; attempt < 12; attempt += 1) {
const activeScenes = await page.evaluate(
@@ -1229,7 +1339,7 @@ async function ensureLocalServer(url) {
}
await delay(250);
}
child.kill();
await stopServerProcess(child);
throw new Error(
`Vite server did not start at ${url}\n${stderr.join('')}`
);
@@ -1247,11 +1357,25 @@ async function canReach(url) {
}
async function stopServerProcess(child) {
if (!child || child.killed) {
if (
!child ||
child.exitCode !== null ||
child.signalCode !== null
) {
return;
}
const exited = new Promise((resolve) =>
child.once('exit', resolve)
);
child.kill();
await delay(80);
await Promise.race([exited, delay(5000)]);
if (
child.exitCode === null &&
child.signalCode === null
) {
child.kill('SIGKILL');
await Promise.race([exited, delay(2000)]);
}
}
function delay(ms) {

View File

@@ -214,6 +214,7 @@ export class CityStayScene extends Phaser.Scene {
private navigationPending = false;
private inputReadyAt = Number.POSITIVE_INFINITY;
private autoDialogueInputReadyAt = 0;
private interactionInputReadyAt = 0;
private stepIndex = 0;
private lastStepAt = 0;
private lastNotice = '';
@@ -367,6 +368,9 @@ export class CityStayScene extends Phaser.Scene {
}
if (this.consumeInteractionRequest()) {
if (time < this.interactionInputReadyAt) {
return;
}
this.tryInteract();
return;
}
@@ -677,6 +681,7 @@ export class CityStayScene extends Phaser.Scene {
this.navigationPending = false;
this.inputReadyAt = Number.POSITIVE_INFINITY;
this.autoDialogueInputReadyAt = 0;
this.interactionInputReadyAt = 0;
this.explorationInput = undefined;
this.stepIndex = 0;
this.lastStepAt = 0;
@@ -3220,6 +3225,9 @@ export class CityStayScene extends Phaser.Scene {
this.dialogueState = undefined;
this.dialoguePanel?.setVisible(false);
this.stopPlayerMovement();
this.interactionInputReadyAt =
this.time.now + inputCarryoverGuardMs;
this.explorationInput?.discardInteractionRequest();
this.restoreActorDirection(sourceTargetId);
const checkpointCommitted = onComplete?.() === true;
if (!checkpointCommitted) {
@@ -3233,6 +3241,9 @@ export class CityStayScene extends Phaser.Scene {
this.dialogueState = undefined;
this.dialoguePanel?.setVisible(false);
this.stopPlayerMovement();
this.interactionInputReadyAt =
this.time.now + inputCarryoverGuardMs;
this.explorationInput?.discardInteractionRequest();
this.restoreActorDirection(sourceTargetId);
this.persistExplorationCheckpoint(true);
this.refreshInteractionPrompt();