feat: deepen audiovisual story immersion
This commit is contained in:
@@ -29,6 +29,8 @@ try {
|
||||
|
||||
await verifyCampModalAndNavigation(page);
|
||||
await verifyBattlePointerFlow(page);
|
||||
await verifyWolongNarrativeVictoryGate(page);
|
||||
await verifyCampTimelineRowLayout(page);
|
||||
|
||||
if (pageErrors.length > 0) {
|
||||
throw new Error(`Unexpected browser errors: ${JSON.stringify(pageErrors.slice(-5))}`);
|
||||
@@ -42,7 +44,9 @@ try {
|
||||
console.log(
|
||||
`Verified pointer-based interaction UX at ${desktopBrowserViewport.width}x${desktopBrowserViewport.height}: ` +
|
||||
'camp and battle save modals block click-through, slow camp navigation is single-flight and commits after loading, ' +
|
||||
'battle event overlays block edge-scroll and hover feedback, ' +
|
||||
'battle event overlays block edge-scroll and hover feedback, prioritized same-action notices collapse into one disclosed modal and battle log, ' +
|
||||
'tactical reactions exclude undeployed or defeated officers, the Wolong narrative objectives gate victory, ' +
|
||||
'long camp timeline titles and victory conditions stay in separate fixed-width columns, ' +
|
||||
'movement commands stay anchored to the destination, the final ally prompt waits for the command, ' +
|
||||
'the persistent turn-end action reopens it, and the right-click menu follows the pointer.'
|
||||
);
|
||||
@@ -290,6 +294,7 @@ async function verifyBattlePointerFlow(page) {
|
||||
}, expectedBattleId, { timeout: 90000 });
|
||||
await startDeploymentIfNeeded(page, expectedBattleId);
|
||||
await verifyBattleEventOverlayInputBlock(page);
|
||||
await verifyBattleEventQueueBehavior(page);
|
||||
|
||||
const lastAllySetup = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
@@ -614,6 +619,230 @@ async function verifyBattleEventOverlayInputBlock(page) {
|
||||
await page.mouse.move(safeScreenPoint.x, safeScreenPoint.y);
|
||||
}
|
||||
|
||||
async function verifyBattleEventQueueBehavior(page) {
|
||||
const initial = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
if (!scene) {
|
||||
return null;
|
||||
}
|
||||
scene.hideBattleEventBanner();
|
||||
scene.clearBattleEvents();
|
||||
['qa-event-a', 'qa-event-b', 'qa-event-c', 'qa-event-d', 'qa-event-interrupted'].forEach((key) => scene.triggeredBattleEvents.delete(key));
|
||||
scene.deferBattleEventPresentation = true;
|
||||
scene.triggerBattleEvent('qa-event-a', '먼저 감지된 사건', ['같은 전투 흐름에서 먼저 표시됩니다.'], { playCue: false });
|
||||
scene.triggerBattleEvent('qa-event-b', '낮은 우선순위', ['마지막에 표시되어야 합니다.'], { playCue: false, priority: 'low' });
|
||||
scene.triggerBattleEvent('qa-event-c', '긴급 사건 하나', ['가장 높은 우선순위로 표시되어야 합니다.'], { playCue: false, priority: 'critical' });
|
||||
scene.triggerBattleEvent('qa-event-d', '긴급 사건 둘', ['같은 우선순위의 발생 순서를 지켜야 합니다.'], { playCue: false, priority: 'critical' });
|
||||
scene.deferBattleEventPresentation = false;
|
||||
scene.showNextBattleEvent();
|
||||
return {
|
||||
active: scene.activeBattleEvent?.key ?? null,
|
||||
queued: scene.battleEventQueue.map((event) => event.key),
|
||||
completed: ['qa-event-a', 'qa-event-b', 'qa-event-c', 'qa-event-d'].filter((key) => scene.triggeredBattleEvents.has(key)),
|
||||
objectCount: scene.battleEventObjects.length,
|
||||
bannerText: scene.battleEventObjects
|
||||
.map((object) => typeof object.text === 'string' ? object.text : '')
|
||||
.filter(Boolean)
|
||||
};
|
||||
});
|
||||
assert(
|
||||
initial?.active === 'qa-event-c' &&
|
||||
JSON.stringify(initial.queued) === JSON.stringify(['qa-event-d', 'qa-event-a', 'qa-event-b']) &&
|
||||
initial.completed.length === 0 &&
|
||||
initial.objectCount > 0 &&
|
||||
initial.bannerText.some((text) => text.includes('외 3건')),
|
||||
`Battle event grouping did not preserve priority/FIFO or disclose grouped notices: ${JSON.stringify(initial)}`
|
||||
);
|
||||
|
||||
await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
scene?.hideBattleEventBanner();
|
||||
});
|
||||
await page.waitForFunction(() => (
|
||||
window.__HEROS_GAME__?.scene.getScene('BattleScene')?.activeBattleEvent === undefined
|
||||
));
|
||||
const afterFirstDismissal = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
return scene
|
||||
? {
|
||||
active: scene.activeBattleEvent?.key ?? null,
|
||||
queued: scene.battleEventQueue.map((event) => event.key),
|
||||
completed: ['qa-event-a', 'qa-event-b', 'qa-event-c', 'qa-event-d'].filter((key) => scene.triggeredBattleEvents.has(key)),
|
||||
objectCount: scene.battleEventObjects.length
|
||||
}
|
||||
: null;
|
||||
});
|
||||
assert(
|
||||
afterFirstDismissal?.active === null &&
|
||||
afterFirstDismissal.queued.length === 0 &&
|
||||
afterFirstDismissal.completed.length === 4 &&
|
||||
afterFirstDismissal.objectCount === 0,
|
||||
`Battle event dismissal did not complete the grouped one-modal batch: ${JSON.stringify(afterFirstDismissal)}`
|
||||
);
|
||||
|
||||
const cleanup = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
scene?.triggerBattleEvent('qa-event-interrupted', '중단될 사건', ['정리 시 완료 처리되면 안 됩니다.'], { playCue: false });
|
||||
scene?.clearBattleEvents();
|
||||
return scene
|
||||
? {
|
||||
active: scene.activeBattleEvent?.key ?? null,
|
||||
queued: scene.battleEventQueue.map((event) => event.key),
|
||||
objectCount: scene.battleEventObjects.length,
|
||||
interruptedCompleted: scene.triggeredBattleEvents.has('qa-event-interrupted')
|
||||
}
|
||||
: null;
|
||||
});
|
||||
assert(
|
||||
cleanup?.active === null &&
|
||||
cleanup.queued.length === 0 &&
|
||||
cleanup.objectCount === 0 &&
|
||||
cleanup.interruptedCompleted === false,
|
||||
`Battle event cleanup retained or falsely completed interrupted events: ${JSON.stringify(cleanup)}`
|
||||
);
|
||||
|
||||
const reactionProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const units = scene?.debugBattleUnits?.() ?? [];
|
||||
const liuBei = units.find((unit) => unit.id === 'liu-bei');
|
||||
if (!scene || !liuBei) {
|
||||
return null;
|
||||
}
|
||||
const livingReaction = scene.tacticalEventReaction('ally-danger');
|
||||
const previousHp = liuBei.hp;
|
||||
liuBei.hp = 0;
|
||||
const defeatedReaction = scene.tacticalEventReaction('ally-danger');
|
||||
liuBei.hp = previousHp;
|
||||
return {
|
||||
deployedIds: units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id),
|
||||
livingSpeaker: livingReaction?.unitId ?? null,
|
||||
defeatedSpeaker: defeatedReaction?.unitId ?? null
|
||||
};
|
||||
});
|
||||
assert(
|
||||
reactionProbe?.livingSpeaker === 'liu-bei' &&
|
||||
reactionProbe.defeatedSpeaker === null &&
|
||||
!reactionProbe.deployedIds.includes('zhao-yun'),
|
||||
`Tactical event reaction used an undeployed or defeated officer: ${JSON.stringify(reactionProbe)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyWolongNarrativeVictoryGate(page) {
|
||||
const battleId = 'seventeenth-battle-wolong-visit-road';
|
||||
await page.evaluate((nextBattleId) => window.__HEROS_DEBUG__?.goToBattle(nextBattleId), battleId);
|
||||
await page.waitForFunction((expectedId) => {
|
||||
const battle = window.__HEROS_DEBUG__?.battle();
|
||||
return battle?.battleId === expectedId && ['deployment', 'idle'].includes(battle.phase);
|
||||
}, battleId, { timeout: 90000 });
|
||||
await startDeploymentIfNeeded(page, battleId);
|
||||
|
||||
const probe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const units = scene?.debugBattleUnits?.() ?? [];
|
||||
const allies = units.filter((unit) => unit.faction === 'ally' && unit.hp > 0);
|
||||
const enemies = units.filter((unit) => unit.faction === 'enemy');
|
||||
const visitor = allies.find((unit) => unit.id === 'liu-bei') ?? allies[0];
|
||||
if (!scene || !visitor || enemies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
scene.clearBattleEvents();
|
||||
enemies.forEach((enemy) => {
|
||||
enemy.hp = 0;
|
||||
});
|
||||
const blockedOutcome = scene.pendingBattleOutcome() ?? null;
|
||||
const before = scene.requiredVictoryObjectiveStates().map((objective) => ({
|
||||
id: objective.id,
|
||||
category: objective.category,
|
||||
achieved: objective.achieved
|
||||
}));
|
||||
const gateEventKnown = scene.isBattleEventKnown('victory-gate-pending');
|
||||
visitor.x = 29;
|
||||
visitor.y = 16;
|
||||
const after = scene.requiredVictoryObjectiveStates().map((objective) => ({
|
||||
id: objective.id,
|
||||
category: objective.category,
|
||||
achieved: objective.achieved
|
||||
}));
|
||||
const completedOutcome = scene.pendingBattleOutcome() ?? null;
|
||||
return { blockedOutcome, completedOutcome, before, after, gateEventKnown };
|
||||
});
|
||||
assert(
|
||||
probe?.blockedOutcome === null &&
|
||||
probe.completedOutcome === 'victory' &&
|
||||
probe.gateEventKnown === true &&
|
||||
JSON.stringify(probe.before.map((objective) => objective.id)) === JSON.stringify(['scholar-rendezvous', 'longzhong-cottage']) &&
|
||||
probe.before.every((objective) => objective.category === 'required' && objective.achieved === false) &&
|
||||
probe.after.every((objective) => objective.category === 'required' && objective.achieved === true),
|
||||
`Wolong battle ignored or misreported its narrative victory gate: ${JSON.stringify(probe)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyCampTimelineRowLayout(page) {
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.goToCamp());
|
||||
await page.waitForFunction(() => {
|
||||
const scenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
return scenes.includes('CampScene') && window.__HEROS_GAME__?.scene.getScene('CampScene')?.scene?.isActive();
|
||||
}, undefined, { timeout: 90000 });
|
||||
|
||||
const rows = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
if (!scene) {
|
||||
return null;
|
||||
}
|
||||
scene.hideSortiePrep(false);
|
||||
scene.clearContent();
|
||||
scene.renderProgressChapterDetail(
|
||||
{
|
||||
id: 'qa-long-timeline-fields',
|
||||
title: '긴 연표 필드 검증',
|
||||
period: '1920×1080',
|
||||
description: '긴 전투명과 승리 조건이 서로 침범하지 않는지 확인합니다.',
|
||||
battleIds: [
|
||||
'seventeenth-battle-wolong-visit-road',
|
||||
'fifty-fifth-battle-northern-qishan-road',
|
||||
'fifty-seventh-battle-jieting-crisis',
|
||||
'fifty-eighth-battle-qishan-retreat'
|
||||
],
|
||||
nextHints: []
|
||||
},
|
||||
0,
|
||||
804,
|
||||
262,
|
||||
390,
|
||||
310,
|
||||
new Set()
|
||||
);
|
||||
const fields = scene.children.list
|
||||
.filter((object) => object.type === 'Text' && object.active && object.getData('timelineField'))
|
||||
.map((object) => {
|
||||
const bounds = object.getBounds();
|
||||
return {
|
||||
field: object.getData('timelineField'),
|
||||
text: object.text,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
right: bounds.right,
|
||||
width: bounds.width
|
||||
};
|
||||
});
|
||||
const titles = fields.filter((field) => field.field === 'battle-title').sort((left, right) => left.y - right.y);
|
||||
const objectives = fields.filter((field) => field.field === 'victory-condition').sort((left, right) => left.y - right.y);
|
||||
return titles.map((title, index) => ({ title, objective: objectives[index] ?? null }));
|
||||
});
|
||||
assert(
|
||||
rows?.length === 4 &&
|
||||
rows.every(({ title, objective }) => (
|
||||
objective &&
|
||||
title.right < objective.x &&
|
||||
title.width > 0 &&
|
||||
objective.width > 0 &&
|
||||
objective.text.endsWith('…')
|
||||
)),
|
||||
`Camp timeline detail fields overlap or fail to truncate at 1920x1080: ${JSON.stringify(rows)}`
|
||||
);
|
||||
}
|
||||
|
||||
async function startDeploymentIfNeeded(page, battleId) {
|
||||
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||
if (state?.phase === 'idle') {
|
||||
|
||||
Reference in New Issue
Block a user