feat: refine exploration presentation and audio flow
This commit is contained in:
@@ -40,6 +40,12 @@ try {
|
||||
await waitForDebugApi(page);
|
||||
await waitForTitle(page);
|
||||
await assertDesktopViewport(page);
|
||||
await verifyReducedMotionExploration(page);
|
||||
await page.evaluate(() => window.localStorage.clear());
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await waitForDebugApi(page);
|
||||
await waitForTitle(page);
|
||||
await assertDesktopViewport(page);
|
||||
|
||||
await clickLegacyTitleControl(page, 962, 240);
|
||||
await waitForStoryReady(page);
|
||||
@@ -174,8 +180,13 @@ try {
|
||||
walkingZhangFei.x > 360 && walkingZhangFei.x < 1000,
|
||||
`Zhang Fei should move progressively instead of teleporting: ${JSON.stringify(walkingZhangFei)}`
|
||||
);
|
||||
const queuedGuanTarget = villageNpc(village, 'guan-yu');
|
||||
await page.mouse.click(queuedGuanTarget.x, queuedGuanTarget.y);
|
||||
const queuedVillagerTarget = villageNpc(village, 'market-villager');
|
||||
const queuedVillagerAutoCount =
|
||||
village.movement.autoInteraction.triggeredCount;
|
||||
await page.mouse.click(
|
||||
queuedVillagerTarget.x,
|
||||
queuedVillagerTarget.y
|
||||
);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.village?.();
|
||||
return (
|
||||
@@ -188,15 +199,45 @@ try {
|
||||
const state = window.__HEROS_DEBUG__?.village?.();
|
||||
return (
|
||||
state?.movement?.appliedQueuedIntentCount >= 1 &&
|
||||
state?.movement?.targetNpcId === 'guan-yu'
|
||||
state?.movement?.targetNpcId === 'market-villager'
|
||||
);
|
||||
});
|
||||
await waitForVillageInteractionTarget(page, 'guan-yu');
|
||||
await waitForVillageInteractionTarget(page, 'market-villager');
|
||||
await page.waitForFunction(() => (
|
||||
window.__HEROS_DEBUG__?.village?.()?.dialogue?.sourceNpcId ===
|
||||
'market-villager'
|
||||
));
|
||||
village = await readVillage(page);
|
||||
assert.equal(
|
||||
village.movement.autoInteraction.triggeredCount,
|
||||
queuedVillagerAutoCount + 1,
|
||||
'Queued pointer navigation must open the villager dialogue exactly once on arrival.'
|
||||
);
|
||||
const protectedVillagerLineIndex = village.dialogue.lineIndex;
|
||||
await page.mouse.click(960, 850);
|
||||
await page.waitForTimeout(80);
|
||||
village = await readVillage(page);
|
||||
assert.equal(
|
||||
village.dialogue.lineIndex,
|
||||
protectedVillagerLineIndex,
|
||||
'A follow-up click inside the arrival guard must not skip the first villager line.'
|
||||
);
|
||||
assert.equal(
|
||||
village.dialogue.sourceNpcId,
|
||||
'market-villager',
|
||||
'The arrival guard must keep the newly opened villager dialogue active.'
|
||||
);
|
||||
const tavernZhangFei = villageNpc(village, 'zhang-fei');
|
||||
assert.equal(tavernZhangFei.moving, false);
|
||||
assert.equal(tavernZhangFei.animationKey, 'exploration-zhang-fei-idle-east');
|
||||
await captureStableScreenshot(page, 'dist/verification-prologue-village-dialogue.png');
|
||||
await page.waitForTimeout(160);
|
||||
assert.equal(
|
||||
(await readVillage(page)).movement.autoInteraction.triggeredCount,
|
||||
queuedVillagerAutoCount + 1,
|
||||
'Arrival must not retrigger the same villager dialogue on a later update.'
|
||||
);
|
||||
await advanceDialogueUntilClosed(page);
|
||||
|
||||
await teleportTo(page, 'make-oath');
|
||||
await page.keyboard.press('e');
|
||||
@@ -260,6 +301,8 @@ try {
|
||||
`Zhang Fei should move progressively toward registration: ${JSON.stringify(walkingZhangToRecruitment)}`
|
||||
);
|
||||
const queuedClerkTarget = villageNpc(village, 'recruiting-clerk');
|
||||
const queuedClerkAutoCount =
|
||||
village.movement.autoInteraction.triggeredCount;
|
||||
await page.mouse.click(queuedClerkTarget.x, queuedClerkTarget.y);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.village?.();
|
||||
@@ -273,14 +316,18 @@ try {
|
||||
window.__HEROS_DEBUG__?.village?.()?.movement?.targetNpcId === 'recruiting-clerk'
|
||||
));
|
||||
await waitForVillageInteractionTarget(page, 'recruiting-clerk');
|
||||
village = await readVillage(page);
|
||||
assertNpcPosition(village, 'guan-yu', { x: 850, y: 655 }, 'recruitment Guan Yu');
|
||||
assertNpcPosition(village, 'zhang-fei', { x: 810, y: 805 }, 'recruitment Zhang Fei');
|
||||
|
||||
await page.keyboard.press('e');
|
||||
await page.waitForFunction(() => (
|
||||
window.__HEROS_DEBUG__?.village?.()?.dialogue?.sourceNpcId === 'recruiting-clerk'
|
||||
));
|
||||
village = await readVillage(page);
|
||||
assert.equal(
|
||||
village.movement.autoInteraction.triggeredCount,
|
||||
queuedClerkAutoCount + 1,
|
||||
'Queued pointer navigation must open the recruiting dialogue exactly once.'
|
||||
);
|
||||
assertNpcPosition(village, 'guan-yu', { x: 850, y: 655 }, 'recruitment Guan Yu');
|
||||
assertNpcPosition(village, 'zhang-fei', { x: 810, y: 805 }, 'recruitment Zhang Fei');
|
||||
|
||||
await advanceDialogueUntilClosed(page);
|
||||
|
||||
village = await readVillage(page);
|
||||
@@ -407,7 +454,74 @@ try {
|
||||
'Militia camp interaction outside the radius must not open dialogue.'
|
||||
);
|
||||
|
||||
const campPlayerBeforeMove = (await readMilitiaCamp(page)).player;
|
||||
militiaCamp = await readMilitiaCamp(page);
|
||||
const pointerZouJing = militiaCamp.npcs.find(
|
||||
({ id }) => id === 'zou-jing'
|
||||
);
|
||||
assert(pointerZouJing, 'Zou Jing must be available as a pointer navigation target.');
|
||||
const campAutoInteractionBefore =
|
||||
militiaCamp.movement.autoInteraction.triggeredCount;
|
||||
const campDetourCountBefore = militiaCamp.movement.detourCount;
|
||||
await page.mouse.click(pointerZouJing.x, pointerZouJing.y);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.militiaCamp?.();
|
||||
return (
|
||||
state?.movement?.targetNpcId === 'zou-jing' &&
|
||||
state?.movement?.detourCount > 0
|
||||
);
|
||||
});
|
||||
await page.waitForFunction(() => (
|
||||
window.__HEROS_DEBUG__?.militiaCamp?.()?.dialogue?.sourceNpcId ===
|
||||
'zou-jing'
|
||||
));
|
||||
militiaCamp = await readMilitiaCamp(page);
|
||||
assert(
|
||||
militiaCamp.movement.detourCount > campDetourCountBefore,
|
||||
'Clicking Zou Jing from the entrance must route around the central fire.'
|
||||
);
|
||||
assert.equal(
|
||||
militiaCamp.movement.autoInteraction.triggeredCount,
|
||||
campAutoInteractionBefore + 1,
|
||||
'Militia camp pointer arrival must open dialogue exactly once.'
|
||||
);
|
||||
const protectedCampLineIndex = militiaCamp.dialogue.lineIndex;
|
||||
await page.mouse.click(960, 850);
|
||||
await page.waitForTimeout(80);
|
||||
militiaCamp = await readMilitiaCamp(page);
|
||||
assert.equal(
|
||||
militiaCamp.dialogue.lineIndex,
|
||||
protectedCampLineIndex,
|
||||
'A follow-up click inside the arrival guard must not skip Zou Jing’s first line.'
|
||||
);
|
||||
assert.equal(
|
||||
militiaCamp.dialogue.sourceNpcId,
|
||||
'zou-jing',
|
||||
'The arrival guard must keep Zou Jing’s newly opened dialogue active.'
|
||||
);
|
||||
await page.waitForTimeout(160);
|
||||
assert.equal(
|
||||
(await readMilitiaCamp(page)).movement.autoInteraction.triggeredCount,
|
||||
campAutoInteractionBefore + 1,
|
||||
'Militia camp arrival must not retrigger dialogue on a later update.'
|
||||
);
|
||||
await advanceMilitiaCampDialogueUntilClosed(page);
|
||||
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
await waitForDebugApi(page);
|
||||
await page.waitForFunction(() =>
|
||||
window.__HEROS_DEBUG__?.activeScenes?.().includes('TitleScene')
|
||||
);
|
||||
await clickLegacyTitleControl(page, 962, 310);
|
||||
await waitForMilitiaCampReady(page);
|
||||
await page.waitForTimeout(380);
|
||||
militiaCamp = await readMilitiaCamp(page);
|
||||
assert.equal(
|
||||
militiaCamp.dialogue.active,
|
||||
false,
|
||||
'The pointer-route smoke reload must not replay the one-time camp introduction.'
|
||||
);
|
||||
|
||||
const campPlayerBeforeMove = militiaCamp.player;
|
||||
await page.keyboard.down('ArrowUp');
|
||||
await page.waitForTimeout(360);
|
||||
await page.keyboard.up('ArrowUp');
|
||||
@@ -906,7 +1020,7 @@ try {
|
||||
|
||||
console.log(
|
||||
`Verified the playable prologue village and militia camp at ${desktopBrowserViewport.width}x${desktopBrowserViewport.height}, ` +
|
||||
`DPR ${desktopBrowserDeviceScaleFactor}: sequential meetings, direct movement, distance-gated dialogue, ` +
|
||||
`DPR ${desktopBrowserDeviceScaleFactor}: sequential meetings, direct movement, obstacle detours, single-fire arrival dialogue, ` +
|
||||
'autosave/reload, legacy-save routing, locked oath and command, camp preparation, optional promise memory, ' +
|
||||
'departure story, and first deployment.'
|
||||
);
|
||||
@@ -924,6 +1038,62 @@ function withDebugOptions(url) {
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
async function verifyReducedMotionExploration(page) {
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.setItem(
|
||||
'heros-web:visual-motion-mode',
|
||||
'reduced'
|
||||
);
|
||||
});
|
||||
await page.evaluate(async () => {
|
||||
await window.__HEROS_DEBUG__?.goToVillage();
|
||||
});
|
||||
await waitForVillageReady(page);
|
||||
await page.waitForTimeout(380);
|
||||
|
||||
let village = await readVillage(page);
|
||||
assert.equal(
|
||||
village.visualMotionReduced,
|
||||
true,
|
||||
'The playable village must honor the reduced-motion preference.'
|
||||
);
|
||||
assert.equal(
|
||||
village.player.animationPlaying,
|
||||
false,
|
||||
'Reduced motion must keep the idle player on a stable directional frame.'
|
||||
);
|
||||
assert(
|
||||
village.npcs.every((npc) => npc.animationPlaying === false),
|
||||
`Reduced motion must freeze NPC idle loops: ${JSON.stringify(village.npcs)}`
|
||||
);
|
||||
|
||||
await advanceDialogueUntilClosed(page);
|
||||
await page.keyboard.down('d');
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const player = window.__HEROS_DEBUG__?.village?.()?.player;
|
||||
return (
|
||||
player?.moving === true &&
|
||||
player?.animationPlaying === true &&
|
||||
player?.animationKey?.includes('-walk-')
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await page.keyboard.up('d');
|
||||
}
|
||||
await page.waitForFunction(() => {
|
||||
const player = window.__HEROS_DEBUG__?.village?.()?.player;
|
||||
return player?.moving === false && player?.animationPlaying === false;
|
||||
});
|
||||
|
||||
village = await readVillage(page);
|
||||
assert.equal(
|
||||
village.player.animationPlaying,
|
||||
false,
|
||||
'Reduced motion must return the player to a stable frame after walking.'
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForDebugApi(page) {
|
||||
await page.waitForFunction(() => (
|
||||
document.querySelector('canvas') !== null &&
|
||||
|
||||
Reference in New Issue
Block a user