feat: make sortie feedback actionable

This commit is contained in:
2026-07-15 06:30:26 +09:00
parent 0cadcb22b9
commit b96029bce3
6 changed files with 1506 additions and 38 deletions

View File

@@ -35,6 +35,47 @@ try {
throw new Error(`Expected first battle auto-save in current and slot 1 saves: ${JSON.stringify(autoSave)}`);
}
await page.evaluate(() => {
const battleId = 'first-battle-zhuo-commandery';
const installRecommendation = (key) => {
const raw = window.localStorage.getItem(key);
if (!raw) {
return;
}
const state = JSON.parse(raw);
const orderId = state.sortieOrderSelection?.orderId ?? 'elite';
state.sortieOrderSelection = { battleId, orderId };
delete state.sortieResonanceSelection;
const selectedUnitIds = state.selectedSortieUnitIds?.length
? [...state.selectedSortieUnitIds]
: ['liu-bei', 'guan-yu', 'zhang-fei'];
state.selectedSortieUnitIds = [...selectedUnitIds];
const fallbackRoles = ['front', 'flank', 'support', 'reserve'];
const formationAssignments = Object.fromEntries(selectedUnitIds.map((unitId, index) => [
unitId,
state.sortieFormationAssignments?.[unitId] ?? fallbackRoles[index] ?? 'reserve'
]));
state.sortieFormationAssignments = {
...(state.sortieFormationAssignments ?? {}),
...formationAssignments
};
state.sortieRecommendationSelection = {
version: 1,
battleId,
planId: 'order',
label: '재도전 검증안',
summary: '패배 후 같은 전장 보완 동선을 검증합니다.',
sortieOrderId: orderId,
selectedUnitIds,
formationAssignments,
unitReasons: Object.fromEntries(selectedUnitIds.map((unitId) => [unitId, '추천 역할 수행을 검증합니다.']))
};
window.localStorage.setItem(key, JSON.stringify(state));
};
installRecommendation('heros-web:campaign-state');
installRecommendation('heros-web:campaign-state:slot-1');
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForTitle(page);
await page.screenshot({ path: 'dist/verification-save-title-continue.png', fullPage: true });
@@ -42,7 +83,12 @@ try {
await startTitleDefaultAction(page);
await waitForBattleReady(page, 'first-battle-zhuo-commandery');
const continuedBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
if (continuedBattle?.battleOutcome !== null || continuedBattle?.resultVisible !== false || continuedBattle?.turnNumber !== 1) {
if (
continuedBattle?.battleOutcome !== null ||
continuedBattle?.resultVisible !== false ||
continuedBattle?.turnNumber !== 1 ||
continuedBattle?.sortieRecommendation?.battleId !== 'first-battle-zhuo-commandery'
) {
throw new Error(`Expected continue to reopen a clean first battle: ${JSON.stringify(continuedBattle)}`);
}
await page.screenshot({ path: 'dist/verification-save-continued-battle.png', fullPage: true });
@@ -56,7 +102,86 @@ try {
throw new Error(`Expected defeat to save retry-ready first battle state: ${JSON.stringify(defeatSave.current)}`);
}
await clickLegacyUi(page, 884, 642);
const closedDefeatEvaluation = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation);
if (
closedDefeatEvaluation?.improvementAction?.available !== true ||
closedDefeatEvaluation.improvementAction.label !== '재도전 보완' ||
closedDefeatEvaluation.improvementAction.sourceBattleId !== 'first-battle-zhuo-commandery' ||
closedDefeatEvaluation.improvementAction.targetBattleId !== 'first-battle-zhuo-commandery'
) {
throw new Error(`Expected defeat to expose same-battle improvement semantics: ${JSON.stringify(closedDefeatEvaluation)}`);
}
await clickDebugBounds(page, 'BattleScene', closedDefeatEvaluation.toggleButtonBounds);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation?.open === true,
undefined,
{ timeout: 30000 }
);
const openedDefeatEvaluation = await page.evaluate(() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation);
if (!insideViewport(openedDefeatEvaluation?.improvementAction?.buttonBounds, baselineViewport)) {
throw new Error(`Expected an in-FHD defeat improvement button: ${JSON.stringify(openedDefeatEvaluation)}`);
}
await clickDebugBounds(page, 'BattleScene', openedDefeatEvaluation.improvementAction.buttonBounds);
try {
await page.waitForFunction(() => {
const camp = window.__HEROS_DEBUG__?.camp?.();
return camp?.sortieVisible === true &&
camp?.sortiePrepStep === 'formation' &&
camp?.nextSortieBattleId === 'first-battle-zhuo-commandery' &&
camp?.sortieComparisonAction?.kind === 'confirm-improvement';
}, undefined, { timeout: 30000 });
} catch {
const transitionState = await page.evaluate(() => ({
activeScenes: window.__HEROS_GAME__?.scene.getScenes(true).map((scene) => scene.scene.key),
battleOutcome: window.__HEROS_DEBUG__?.battle?.()?.battleOutcome,
camp: (() => {
const camp = window.__HEROS_DEBUG__?.camp?.();
return camp
? {
sortieVisible: camp.sortieVisible,
sortiePrepStep: camp.sortiePrepStep,
nextSortieBattleId: camp.nextSortieBattleId,
selectedSortieUnitIds: camp.campaign?.selectedSortieUnitIds,
guidedImprovement: camp.sortieGuidedImprovement,
comparisonAction: camp.sortieComparisonAction,
feedback: camp.sortiePlanFeedback
}
: null;
})()
}));
throw new Error(`Expected defeat improvement CTA to open same-battle Camp preview: ${JSON.stringify(transitionState)}`);
}
const defeatGuidedPreview = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
if (
defeatGuidedPreview?.sortieGuidedImprovement?.sourceBattleId !== 'first-battle-zhuo-commandery' ||
defeatGuidedPreview.sortieGuidedImprovement.targetBattleId !== 'first-battle-zhuo-commandery' ||
defeatGuidedPreview.sortieComparisonPreview?.source !== 'guided-improvement' ||
!insideViewport(defeatGuidedPreview.sortieComparisonAction?.buttonBounds, baselineViewport)
) {
throw new Error(`Expected same-battle guided preview after defeat: ${JSON.stringify(defeatGuidedPreview)}`);
}
await clickDebugBounds(page, 'CampScene', defeatGuidedPreview.sortieComparisonAction.buttonBounds);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp?.()?.sortieComparisonAction?.kind === 'undo-improvement',
undefined,
{ timeout: 30000 }
);
const appliedDefeatImprovement = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const appliedDefeatProposal = appliedDefeatImprovement?.sortieGuidedImprovement?.proposal;
if (
!appliedDefeatProposal ||
appliedDefeatImprovement?.sortieRecommendationSelection?.battleId !== 'first-battle-zhuo-commandery' ||
appliedDefeatImprovement.sortieRecommendationSelection.planId !== appliedDefeatProposal.planId
) {
throw new Error(`Expected defeat improvement to preserve same-battle recommendation provenance: ${JSON.stringify(appliedDefeatImprovement)}`);
}
await clickLegacyUi(page, 1116, 656);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp?.()?.sortiePrepStep === 'loadout',
undefined,
{ timeout: 30000 }
);
await clickLegacyUi(page, 1116, 656);
await waitForBattleReady(page, 'first-battle-zhuo-commandery');
const retriedBattle = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
const retriedLiuBei = retriedBattle?.units?.find((unit) => unit.id === 'liu-bei');
@@ -66,6 +191,9 @@ try {
retriedBattle?.battleOutcome !== null ||
retriedBattle?.resultVisible !== false ||
retriedBattle?.turnNumber !== 1 ||
retriedBattle?.sortieRecommendation?.battleId !== 'first-battle-zhuo-commandery' ||
retriedBattle?.sortieRecommendation?.planId !== appliedDefeatProposal.planId ||
JSON.stringify(retriedBattle?.selectedSortieUnitIds) !== JSON.stringify(appliedDefeatProposal.projectedSelectedUnitIds) ||
!retriedLiuBei ||
retriedLiuBei.hp !== retriedLiuBei.maxHp ||
damagedAlliesAfterRetry.length > 0 ||
@@ -275,3 +403,43 @@ function delay(ms) {
async function clickLegacyUi(page, x, y, options) {
await page.mouse.click(x * legacyUiScale, y * legacyUiScale, options);
}
async function clickDebugBounds(page, sceneKey, bounds) {
if (!bounds) {
throw new Error(`Expected ${sceneKey} debug bounds before click.`);
}
const point = await page.evaluate(({ sceneKey, bounds }) => {
const scene = window.__HEROS_GAME__?.scene.getScene(sceneKey);
const canvas = document.querySelector('canvas');
if (!scene || !canvas) {
return null;
}
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, { sceneKey, bounds });
if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
throw new Error(`Expected a canvas-scaled ${sceneKey} click point: ${JSON.stringify({ bounds, point })}`);
}
await page.mouse.click(point.x, point.y);
}
function insideViewport(bounds, viewport) {
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 &&
bounds.x >= 0 &&
bounds.y >= 0 &&
bounds.x + bounds.width <= viewport.width &&
bounds.y + bounds.height <= viewport.height
);
}