feat: show sortie recommendation reasons

This commit is contained in:
2026-07-13 08:10:04 +09:00
parent cd1abbe815
commit 7b58317f67
2 changed files with 398 additions and 5 deletions

View File

@@ -404,6 +404,9 @@ try {
if (!damageStrategy) {
throw new Error(`Expected ${partner.id} to have a damaging strategy for the pursuit-ready UI fixture.`);
}
partner.x = target.x;
partner.y = target.y - 1;
scene.positionUnitView(partner);
stageTargeting({
user: partner,
action: damageStrategy.command,
@@ -3911,6 +3914,140 @@ try {
})}`
);
const midRecommendationToggleBounds = midFormationPortraitState?.sortieRecommendationBrowser?.toggleButtonBounds;
const midRecommendationSelectionBefore = {
selectedUnitIds: [...(midFormationPortraitState?.selectedSortieUnitIds ?? [])],
formationAssignments: { ...(midFormationPortraitState?.sortieFormationAssignments ?? {}) },
itemAssignments: { ...(midFormationPortraitState?.sortieItemAssignments ?? {}) }
};
assert(
isFiniteBounds(midRecommendationToggleBounds),
`Expected the mid-campaign formation to expose the recommendation browser toggle: ${JSON.stringify(midFormationPortraitState?.sortieRecommendationBrowser)}`
);
await page.mouse.click(
midRecommendationToggleBounds.x + midRecommendationToggleBounds.width / 2,
midRecommendationToggleBounds.y + midRecommendationToggleBounds.height / 2
);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.open === true,
undefined,
{ timeout: 30000 }
);
const midRecommendationOpenState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const midRecommendationTarget = midRecommendationOpenState?.sortieRecommendationBrowser?.cards?.find(
(card) => card.applicable && !card.current &&
card.selectedUnitIds.some((unitId) => !midRecommendationSelectionBefore.selectedUnitIds.includes(unitId)) &&
midRecommendationSelectionBefore.selectedUnitIds.some((unitId) => !card.selectedUnitIds.includes(unitId)) &&
isFiniteBounds(card.backgroundBounds) && isFiniteBounds(card.compareButtonBounds)
);
assert(
midRecommendationTarget && isFiniteBounds(midRecommendationOpenState?.sortieRecommendationBrowser?.closeButtonBounds),
`Expected an applicable recommendation card that differs from the current formation: ${JSON.stringify(midRecommendationOpenState?.sortieRecommendationBrowser)}`
);
await page.mouse.move(
midRecommendationTarget.backgroundBounds.x + midRecommendationTarget.backgroundBounds.width / 2,
midRecommendationTarget.backgroundBounds.y + midRecommendationTarget.backgroundBounds.height / 2
);
await page.waitForFunction(
(planId) => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.reasonPanel?.planId === planId,
midRecommendationTarget.id,
{ timeout: 30000 }
);
const midRecommendationHoverState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const midRecommendationHoverBrowser = midRecommendationHoverState?.sortieRecommendationBrowser;
const midRecommendationReasonPanel = midRecommendationHoverBrowser?.reasonPanel;
const midRecommendationReasonRows = midRecommendationReasonPanel?.rows ?? [];
const midRecommendationRequiredRow = midRecommendationReasonRows.find((row) => row.unitId === 'zhuge-liang');
const midRecommendationIncomingIds = midRecommendationTarget.selectedUnitIds.filter(
(unitId) => !midRecommendationSelectionBefore.selectedUnitIds.includes(unitId)
);
const midRecommendationOutgoingIds = midRecommendationSelectionBefore.selectedUnitIds.filter(
(unitId) => !midRecommendationTarget.selectedUnitIds.includes(unitId)
);
const midRecommendationOfficerNames = Object.fromEntries(
(midRecommendationHoverState?.sortiePortraitRoster ?? []).map((unit) => [unit.id, unit.name])
);
const midRecommendationFirstOutgoingName = midRecommendationOfficerNames[midRecommendationOutgoingIds[0]];
assert(
midRecommendationHoverBrowser?.hoveredPlanId === midRecommendationTarget.id &&
midRecommendationReasonPanel?.emptyVisible === false &&
sameJsonValue(midRecommendationReasonRows.map((row) => row.unitId), midRecommendationTarget.selectedUnitIds) &&
midRecommendationReasonRows.every(
(row) => row.fullReason === midRecommendationTarget.unitReasons[row.unitId] &&
isFiniteBounds(row.bounds) && boundsInside(row.bounds, midRecommendationReasonPanel.bounds) &&
isFiniteBounds(row.memberBounds) && boundsInside(row.memberBounds, row.bounds) &&
isFiniteBounds(row.reasonBounds) && boundsInside(row.reasonBounds, row.bounds)
) &&
midRecommendationIncomingIds.every(
(unitId) => midRecommendationReasonRows.find((row) => row.unitId === unitId)?.state === 'incoming'
) &&
midRecommendationIncomingIds.length > 0 &&
midRecommendationOutgoingIds.length > 0 &&
midRecommendationFirstOutgoingName &&
midRecommendationReasonPanel.changeSummary.includes(`OUT ${midRecommendationFirstOutgoingName}`) &&
(midRecommendationRequiredRow?.state === 'required' || midRecommendationRequiredRow?.state === 'required-role') &&
midRecommendationRequiredRow.fullReason === '이번 전투의 필수 출전 무장입니다. · 고유 동남풍 화계' &&
midRecommendationRequiredRow.reason.startsWith('고유 동남풍 화계') &&
sameJsonValue(
{
selectedUnitIds: midRecommendationHoverState?.selectedSortieUnitIds,
formationAssignments: midRecommendationHoverState?.sortieFormationAssignments,
itemAssignments: midRecommendationHoverState?.sortieItemAssignments
},
midRecommendationSelectionBefore
),
`Expected recommendation hover to render every source-backed officer reason inside the FHD panel without mutating formation: ${JSON.stringify({
target: midRecommendationTarget,
browser: midRecommendationHoverBrowser,
before: midRecommendationSelectionBefore,
after: {
selectedUnitIds: midRecommendationHoverState?.selectedSortieUnitIds,
formationAssignments: midRecommendationHoverState?.sortieFormationAssignments,
itemAssignments: midRecommendationHoverState?.sortieItemAssignments
}
})}`
);
await page.mouse.click(
midRecommendationTarget.compareButtonBounds.x + midRecommendationTarget.compareButtonBounds.width / 2,
midRecommendationTarget.compareButtonBounds.y + midRecommendationTarget.compareButtonBounds.height / 2
);
await page.mouse.move(40, 40);
await page.waitForFunction(
(planId) => {
const state = window.__HEROS_DEBUG__?.camp();
const browser = state?.sortieRecommendationBrowser;
return browser?.pinnedPlanId === planId && browser?.hoveredPlanId === null && browser?.reasonPanel?.planId === planId &&
state?.sortieComparisonPreview?.source === 'pinned-recommendation' &&
state?.sortieComparisonAction?.kind === 'confirm-recommendation';
},
midRecommendationTarget.id,
{ timeout: 30000 }
);
const midRecommendationPinnedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
midRecommendationPinnedState?.sortieRecommendationBrowser?.reasonPanel?.rows?.length === midRecommendationTarget.selectedUnitIds.length &&
sameJsonValue(
{
selectedUnitIds: midRecommendationPinnedState?.selectedSortieUnitIds,
formationAssignments: midRecommendationPinnedState?.sortieFormationAssignments,
itemAssignments: midRecommendationPinnedState?.sortieItemAssignments
},
midRecommendationSelectionBefore
),
`Expected a pinned recommendation to preserve its officer reasons without applying the formation: ${JSON.stringify(midRecommendationPinnedState?.sortieRecommendationBrowser)}`
);
const midRecommendationCloseBounds = midRecommendationPinnedState.sortieRecommendationBrowser.closeButtonBounds;
await page.mouse.click(
midRecommendationCloseBounds.x + midRecommendationCloseBounds.width / 2,
midRecommendationCloseBounds.y + midRecommendationCloseBounds.height / 2
);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.open === false,
undefined,
{ timeout: 30000 }
);
const midCoreSelectorToggle = midFormationPortraitState?.sortiePursuitPreview;
assert(
midCoreSelectorToggle?.selectorOpen === false &&