feat: make sortie feedback actionable
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user