feat: add live sortie order HUD
This commit is contained in:
@@ -73,12 +73,22 @@ try {
|
||||
assert(firstBattleProbe.battleId === 'first-battle-zhuo-commandery', `Expected first battle: ${JSON.stringify(firstBattleProbe)}`);
|
||||
assert(firstBattleProbe.objectiveText?.startsWith('승리 목표:'), `Expected clear victory objective label: ${JSON.stringify(firstBattleProbe)}`);
|
||||
assert(firstBattleProbe.objectiveSubText?.includes('패배 조건:'), `Expected clear defeat condition label: ${JSON.stringify(firstBattleProbe)}`);
|
||||
assert(firstBattleProbe.objectiveSubText?.includes('진군:'), `Expected tactical route in objective tracker: ${JSON.stringify(firstBattleProbe)}`);
|
||||
assert(
|
||||
firstBattleProbe.objectiveSubText?.startsWith('패배 조건:') &&
|
||||
!firstBattleProbe.objectiveSubText.includes('진군:') &&
|
||||
!firstBattleProbe.objectiveSubText.includes('보조:'),
|
||||
`Expected the compact objective subtext to retain only the defeat condition without duplicate route or bonus guidance: ${JSON.stringify(firstBattleProbe)}`
|
||||
);
|
||||
assert(
|
||||
firstBattleProbe.sideTexts.some((text) => text.includes('출진 군세')) &&
|
||||
!firstBattleProbe.sideTexts.some((text) => text.includes('...')),
|
||||
`Expected sortie doctrine opening message without ASCII truncation: ${JSON.stringify(firstBattleProbe.sideTexts)}`
|
||||
);
|
||||
await assertLiveSortieOrderHud(page, {
|
||||
battleId: 'first-battle-zhuo-commandery',
|
||||
orderId: 'elite',
|
||||
context: 'first battle'
|
||||
});
|
||||
|
||||
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||
await waitForBattleOutcome(page, 'victory');
|
||||
@@ -860,6 +870,11 @@ try {
|
||||
secondBattleProbe?.sortieOperationOrder?.selectedId === 'elite' && secondBattleProbe.sortieOperationOrder.result === null,
|
||||
`Expected the explicitly declared elite order to reach the next battle unchanged: ${JSON.stringify(secondBattleProbe?.sortieOperationOrder)}`
|
||||
);
|
||||
await assertLiveSortieOrderHud(page, {
|
||||
battleId: 'second-battle-yellow-turban-pursuit',
|
||||
orderId: 'elite',
|
||||
context: 'second battle'
|
||||
});
|
||||
assertSameMembers(
|
||||
secondBattleProbe?.selectedSortieUnitIds,
|
||||
firstCampSelectedSortieUnitIds,
|
||||
@@ -2363,6 +2378,108 @@ async function startDeploymentIfNeeded(page, battleId) {
|
||||
}, battleId, { timeout: 30000 });
|
||||
}
|
||||
|
||||
async function assertLiveSortieOrderHud(page, { battleId, orderId, context }) {
|
||||
const probe = await page.evaluate(({ expectedBattleId, expectedOrderId }) => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||
const operationOrder = state?.sortieOperationOrder;
|
||||
const rawHud = operationOrder?.live ?? operationOrder?.hud;
|
||||
const criteria = rawHud?.criteria ?? rawHud?.progress ?? [];
|
||||
return {
|
||||
battleId: state?.battleId ?? null,
|
||||
phase: state?.phase ?? null,
|
||||
battleOutcome: state?.battleOutcome ?? null,
|
||||
resultVisible: state?.resultVisible ?? false,
|
||||
selectedId: operationOrder?.selectedId ?? null,
|
||||
result: operationOrder?.result ?? null,
|
||||
sceneBounds: scene
|
||||
? { x: 0, y: 0, width: scene.scale.width, height: scene.scale.height }
|
||||
: null,
|
||||
panelBounds: scene?.layout
|
||||
? {
|
||||
x: scene.layout.panelX,
|
||||
y: scene.layout.panelY,
|
||||
width: scene.layout.panelWidth,
|
||||
height: scene.layout.panelHeight
|
||||
}
|
||||
: null,
|
||||
hud: rawHud
|
||||
? {
|
||||
visible: rawHud.visible,
|
||||
bounds: rawHud.bounds ?? null,
|
||||
orderId: rawHud.orderId ?? null,
|
||||
label: rawHud.label ?? '',
|
||||
victoryLabel: rawHud.victoryLabel ?? '',
|
||||
victoryTone: rawHud.victoryTone ?? rawHud.victoryStatus ?? '',
|
||||
status: rawHud.status ?? '',
|
||||
tone: rawHud.tone ?? rawHud.status ?? '',
|
||||
projectedScore: rawHud.projectedScore,
|
||||
criteria: criteria.map((entry) => ({
|
||||
id: entry?.id ?? null,
|
||||
label: entry?.label ?? '',
|
||||
value: entry?.value,
|
||||
tone: entry?.tone ?? entry?.status ?? '',
|
||||
achieved: entry?.achieved,
|
||||
bounds: entry?.bounds ?? null
|
||||
})),
|
||||
stampedIds: Array.isArray(rawHud.stampedIds) ? [...rawHud.stampedIds] : rawHud.stampedIds,
|
||||
animationCount: rawHud.animationCount
|
||||
}
|
||||
: null,
|
||||
expectedBattleId,
|
||||
expectedOrderId
|
||||
};
|
||||
}, { expectedBattleId: battleId, expectedOrderId: orderId });
|
||||
|
||||
const hud = probe.hud;
|
||||
const requiredCriteriaIds = ['elite-grade', 'elite-survival'];
|
||||
const criterionIds = hud?.criteria?.map((entry) => entry.id) ?? [];
|
||||
assert(
|
||||
probe.battleId === battleId &&
|
||||
probe.phase !== 'resolved' &&
|
||||
probe.battleOutcome === null &&
|
||||
probe.resultVisible === false &&
|
||||
probe.selectedId === orderId &&
|
||||
probe.result === null,
|
||||
`Expected ${context} to expose the selected ${orderId} order before its result: ${JSON.stringify(probe)}`
|
||||
);
|
||||
assert(
|
||||
hud?.visible === true &&
|
||||
hud.orderId === orderId &&
|
||||
typeof hud.label === 'string' && hud.label.includes('정예') &&
|
||||
typeof hud.victoryLabel === 'string' && hud.victoryLabel.length > 0 &&
|
||||
typeof hud.victoryTone === 'string' && hud.victoryTone.length > 0 &&
|
||||
typeof hud.status === 'string' && hud.status.length > 0 &&
|
||||
typeof hud.tone === 'string' && hud.tone.length > 0 &&
|
||||
Number.isFinite(hud.projectedScore),
|
||||
`Expected ${context} to show a fully labelled live ${orderId} HUD with a finite projected score: ${JSON.stringify(probe)}`
|
||||
);
|
||||
assert(
|
||||
isFiniteBounds(probe.sceneBounds) &&
|
||||
isFiniteBounds(probe.panelBounds) &&
|
||||
isFiniteBounds(hud.bounds) &&
|
||||
boundsInside(hud.bounds, probe.sceneBounds) &&
|
||||
boundsInside(hud.bounds, probe.panelBounds) &&
|
||||
requiredCriteriaIds.every((id) => criterionIds.includes(id)) &&
|
||||
hud.criteria.length >= requiredCriteriaIds.length &&
|
||||
hud.criteria.every((entry) => (
|
||||
typeof entry.id === 'string' && entry.id.length > 0 &&
|
||||
typeof entry.label === 'string' && entry.label.length > 0 &&
|
||||
Object.prototype.hasOwnProperty.call(entry, 'value') &&
|
||||
typeof entry.tone === 'string' && entry.tone.length > 0 &&
|
||||
typeof entry.achieved === 'boolean' &&
|
||||
isFiniteBounds(entry.bounds) &&
|
||||
boundsInside(entry.bounds, hud.bounds)
|
||||
)),
|
||||
`Expected ${context} live order criteria and bounds to remain inside the visible HUD panel: ${JSON.stringify(probe)}`
|
||||
);
|
||||
assert(
|
||||
Array.isArray(hud.stampedIds) && hud.stampedIds.length === 0 && hud.animationCount === 0,
|
||||
`Expected ${context} live order HUD to start without stamped criteria or animations: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function waitForBattleOutcome(page, outcome) {
|
||||
await page.waitForFunction((expectedOutcome) => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
@@ -3018,6 +3135,26 @@ function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function isFiniteBounds(bounds) {
|
||||
return Boolean(
|
||||
bounds &&
|
||||
Number.isFinite(bounds.x) &&
|
||||
Number.isFinite(bounds.y) &&
|
||||
Number.isFinite(bounds.width) &&
|
||||
Number.isFinite(bounds.height) &&
|
||||
bounds.width > 0 &&
|
||||
bounds.height > 0
|
||||
);
|
||||
}
|
||||
|
||||
function boundsInside(inner, outer, tolerance = 1) {
|
||||
return isFiniteBounds(inner) && isFiniteBounds(outer) &&
|
||||
inner.x >= outer.x - tolerance &&
|
||||
inner.y >= outer.y - tolerance &&
|
||||
inner.x + inner.width <= outer.x + outer.width + tolerance &&
|
||||
inner.y + inner.height <= outer.y + outer.height + tolerance;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
|
||||
Reference in New Issue
Block a user