feat: add live sortie order HUD

This commit is contained in:
2026-07-11 01:51:36 +09:00
parent 23bd55735b
commit 9f49069db2
4 changed files with 633 additions and 10 deletions

View File

@@ -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);