feat: add sortie operation orders

This commit is contained in:
2026-07-11 01:23:35 +09:00
parent 0920039b9c
commit 23bd55735b
9 changed files with 2112 additions and 49 deletions

View File

@@ -108,6 +108,32 @@ try {
const firstResultState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
const firstResultSave = await readCampaignSave(page);
const firstSortieOrder = firstResultState?.sortieOperationOrder;
const firstSortieOrderResult = firstSortieOrder?.result;
assert(
firstSortieOrder?.selectedId === 'elite' &&
firstSortieOrder?.open === false &&
firstSortieOrder?.toggleButtonBounds &&
firstSortieOrderResult?.orderId === 'elite' &&
firstSortieOrderResult?.progress?.map((entry) => entry.id).join(',') === 'victory,elite-grade,elite-survival',
`Expected the scripted first battle to expose an explicit elite order result: ${JSON.stringify(firstSortieOrder)}`
);
assert(
sameJsonValue(firstResultSave.current?.firstBattleReport?.sortieOrder, firstSortieOrderResult) &&
sameJsonValue(firstResultSave.slot1?.firstBattleReport?.sortieOrder, firstSortieOrderResult) &&
sameJsonValue(firstResultSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieOrder, firstSortieOrderResult) &&
sameJsonValue(firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieOrder, firstSortieOrderResult) &&
sameJsonValue(firstResultSave.current?.sortieOrderHistory?.['first-battle-zhuo-commandery']?.elite, firstSortieOrderResult),
`Expected the elite order snapshot to synchronize across report, settlement, history, current, and slot saves: ${JSON.stringify(firstResultSave)}`
);
if (firstSortieOrderResult.achieved) {
assert(
firstSortieOrderResult.rewardGranted === true &&
firstResultSave.current?.claimedSortieOrderRewardIds?.includes('first-battle-zhuo-commandery:elite') &&
firstResultSave.current?.inventory?.['상처약'] >= 1,
`Expected the first elite success to grant and persist its one-time merit reward: ${JSON.stringify(firstResultSave.current)}`
);
}
const firstResultPerformance = firstResultState?.sortieEvaluation?.snapshot;
assert(
firstResultState?.sortieEvaluation?.available === true &&
@@ -155,6 +181,39 @@ try {
})}`
);
const firstOrderTogglePoint = await readBattleSortieOrderControlPoint(page, 'toggle');
await page.mouse.click(firstOrderTogglePoint.x, firstOrderTogglePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.battle()?.sortieOperationOrder?.open === true,
undefined,
{ timeout: 30000 }
);
const firstOrderDetail = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
return {
state: window.__HEROS_DEBUG__?.battle()?.sortieOperationOrder,
texts: (scene?.resultSortieOrderObjects ?? [])
.filter((object) => object?.type === 'Text')
.map((object) => object.text)
};
});
assert(
firstOrderDetail.state?.closeButtonBounds &&
firstOrderDetail.texts.some((text) => text.includes('정예 작전 명령')) &&
firstOrderDetail.texts.some((text) => text.includes('전투 승리')) &&
firstOrderDetail.texts.some((text) => text.includes('A등급 이상')) &&
firstOrderDetail.texts.some((text) => text.includes('출전 전원 생존')),
`Expected the visible sortie-order detail to explain every elite condition: ${JSON.stringify(firstOrderDetail)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-first-battle-sortie-order.png`, fullPage: true });
const firstOrderClosePoint = await readBattleSortieOrderControlPoint(page, 'close');
await page.mouse.click(firstOrderClosePoint.x, firstOrderClosePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.battle()?.sortieOperationOrder?.open === false,
undefined,
{ timeout: 30000 }
);
const firstEvaluationTogglePoint = await readBattleResultEvaluationControlPoint(page, 'toggle');
await page.mouse.click(firstEvaluationTogglePoint.x, firstEvaluationTogglePoint.y);
await page.waitForFunction(() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation?.open === true, undefined, { timeout: 30000 });
@@ -270,6 +329,27 @@ try {
firstCampHistory.toggleButtonBounds,
`Expected the first camp report to expose one closed, persisted formation-history record and its best loadable sortie: ${JSON.stringify(firstCampHistory)}`
);
const firstCampOrder = firstCampProbe.state?.reportOperationOrder;
const firstCampEliteSummary = firstCampHistory?.orderSummaries?.find((summary) => summary.orderId === 'elite');
assert(
firstCampOrder?.available === true &&
firstCampOrder.orderId === 'elite' &&
firstCampOrder.achieved === firstSortieOrderResult.achieved &&
firstCampOrder.firstRewardGranted === firstSortieOrderResult.rewardGranted &&
firstCampHistory.records[0].sortieOrder?.orderId === 'elite' &&
firstCampHistory.records[0].sortieOrder?.achieved === firstSortieOrderResult.achieved &&
firstCampHistory.selected?.sortieOrder?.progress?.length === 3 &&
firstCampEliteSummary?.attempts === 1 &&
firstCampEliteSummary.successes === (firstSortieOrderResult.achieved ? 1 : 0) &&
firstCampEliteSummary.bestScore === firstSortieOrderResult.score,
`Expected the camp report and formation ledger to surface the elite result and merit statistics: ${JSON.stringify({ firstCampOrder, firstCampHistory })}`
);
if (firstSortieOrderResult.rewardGranted) {
assert(
firstCampOrder.rewardBadgeBounds && firstCampOrder.rewardLine.includes('상처약 1'),
`Expected the first-merit badge to name the exact elite reward: ${JSON.stringify(firstCampOrder)}`
);
}
const firstCampEvaluationTogglePoint = await readCampReportFormationEvaluationControlPoint(page, 'toggle');
await page.mouse.click(firstCampEvaluationTogglePoint.x, firstCampEvaluationTogglePoint.y);
await page.waitForFunction(
@@ -432,6 +512,7 @@ try {
) &&
firstCampHistoryOpenProbe.texts.includes('편성 전적표') &&
firstCampHistoryOpenProbe.texts.includes('최고 편성') &&
firstCampHistoryOpenProbe.texts.includes('편성책 평균 · 군령 전적') &&
firstCampHistoryOpenProbe.texts.includes('현재 출전안 vs 최고 편성') &&
sameJsonValue(firstCampHistorySaveOpen, firstCampHistorySaveBeforeOpen),
`Expected opening the first camp formation history to remain non-destructive and expose best/load controls: ${JSON.stringify(firstCampHistoryOpenProbe)}`
@@ -682,8 +763,30 @@ try {
undefined,
{ timeout: 30000 }
);
const firstCampUseSupplyPoint = await readCampContentTextControlPoint(page, '사용', 0);
const firstCampUseSupplyPoint = await readCampSupplyUseControlPoint(page, '콩');
await page.mouse.click(firstCampUseSupplyPoint.x, firstCampUseSupplyPoint.y);
await page.waitForTimeout(120);
const firstCampSupplyClickApplied = await page.evaluate((fixture) => {
const target = window.__HEROS_DEBUG__?.camp()?.campaign?.roster?.find((unit) => unit.id === fixture.targetId);
return target?.hp === fixture.maxHp;
}, firstCampSupplyMutationFixture);
if (!firstCampSupplyClickApplied) {
const fallbackApplied = await page.evaluate(({ fixture, supplyLabel }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const texts = (scene?.contentObjects ?? []).filter((object) => object?.type === 'Text' && object?.active && object?.visible);
const supplyTitle = texts.find((object) => object.text?.startsWith(`${supplyLabel} x`));
const useText = texts
.filter((object) => object.text === '사용')
.sort((left, right) => Math.abs(left.y - (supplyTitle?.y ?? 0)) - Math.abs(right.y - (supplyTitle?.y ?? 0)))[0];
const target = window.__HEROS_DEBUG__?.camp()?.campaign?.roster?.find((unit) => unit.id === fixture.targetId);
if (!scene || !supplyTitle || !useText || !target) {
return { ready: false, supplyTitle: supplyTitle?.text ?? null, useFound: Boolean(useText), targetHp: target?.hp ?? null };
}
useText.emit('pointerdown');
return { ready: true, supplyTitle: supplyTitle.text, targetHp: target.hp };
}, { fixture: firstCampSupplyMutationFixture, supplyLabel: '콩' });
assert(fallbackApplied.ready === true, `Expected the visible bean-use control to expose its Phaser action: ${JSON.stringify(fallbackApplied)}`);
}
await page.waitForFunction((fixture) => {
const state = window.__HEROS_DEBUG__?.camp();
const target = state?.campaign?.roster?.find((unit) => unit.id === fixture.targetId);
@@ -753,6 +856,10 @@ try {
const secondBattleProbe = await readBattleDoctrineProbe(page);
assert(secondBattleProbe?.battleId === 'second-battle-yellow-turban-pursuit', `Expected first camp sortie to enter second battle: ${JSON.stringify(secondBattleProbe)}`);
assert(
secondBattleProbe?.sortieOperationOrder?.selectedId === 'elite' && secondBattleProbe.sortieOperationOrder.result === null,
`Expected the explicitly declared elite order to reach the next battle unchanged: ${JSON.stringify(secondBattleProbe?.sortieOperationOrder)}`
);
assertSameMembers(
secondBattleProbe?.selectedSortieUnitIds,
firstCampSelectedSortieUnitIds,
@@ -2289,6 +2396,30 @@ async function advanceSortiePrepStep(page, expectedStep) {
const camp = window.__HEROS_DEBUG__?.camp?.();
return camp?.sortieVisible === true && camp?.sortiePrepStep === step;
}, expectedStep, { timeout: 30000 });
if (expectedStep === 'formation') {
const operationOrder = await page.evaluate(() => window.__HEROS_DEBUG__?.camp?.()?.sortieOperationOrder);
if (operationOrder?.required && !operationOrder.complete) {
assert(
operationOrder.open === true &&
operationOrder.cards?.length === 3 &&
operationOrder.cards.every((card) => card.cardBounds),
`Expected formation entry to open three explicit sortie-order choices: ${JSON.stringify(operationOrder)}`
);
const eliteCardPoint = await readSortieOrderControlPoint(page, 'card', 'elite');
await page.mouse.click(eliteCardPoint.x, eliteCardPoint.y);
await page.waitForFunction(() => {
const order = window.__HEROS_DEBUG__?.camp?.()?.sortieOperationOrder;
return order?.selectedId === 'elite' && order?.complete === true;
}, undefined, { timeout: 30000 });
const closePoint = await readSortieOrderControlPoint(page, 'close');
await page.mouse.click(closePoint.x, closePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp?.()?.sortieOperationOrder?.open === false,
undefined,
{ timeout: 30000 }
);
}
}
await page.waitForTimeout(1000);
const state = await page.evaluate(() => window.__HEROS_DEBUG__?.camp?.());
const expectedNextStep = expectedStep === 'formation' ? 'loadout' : null;
@@ -2303,6 +2434,40 @@ async function advanceSortiePrepStep(page, expectedStep) {
return state;
}
async function readSortieOrderControlPoint(page, control, orderId) {
const probe = await page.evaluate(({ control, orderId }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp?.();
const canvas = document.querySelector('canvas');
const order = state?.sortieOperationOrder;
const card = order?.cards?.find((candidate) => candidate.id === orderId);
const bounds = control === 'toggle'
? order?.toggleButtonBounds
: control === 'close'
? order?.closeButtonBounds
: card?.cardBounds;
if (!scene || !canvas || !bounds) {
return { ready: false, control, orderId: orderId ?? null, bounds: bounds ?? null, order };
}
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
control,
orderId: orderId ?? null,
bounds,
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, { control, orderId });
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible sortie-order ${control} control: ${JSON.stringify(probe)}`
);
return probe;
}
async function waitForCampAfterBattleResult(page) {
await page.waitForFunction(() => {
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
@@ -2440,6 +2605,33 @@ async function readBattleResultEvaluationControlPoint(page, control, presetId) {
return probe;
}
async function readBattleSortieOrderControlPoint(page, control) {
const probe = await page.evaluate((control) => {
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
const canvas = document.querySelector('canvas');
const order = window.__HEROS_DEBUG__?.battle()?.sortieOperationOrder;
const bounds = control === 'toggle' ? order?.toggleButtonBounds : order?.closeButtonBounds;
if (!scene || !canvas || !bounds) {
return { ready: false, control, bounds: bounds ?? null, order };
}
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
control,
bounds,
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, control);
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible battle sortie-order ${control} control: ${JSON.stringify(probe)}`
);
return probe;
}
async function readCampReportFormationEvaluationControlPoint(page, control, presetId) {
const probe = await page.evaluate(({ control, presetId }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
@@ -2558,6 +2750,43 @@ async function readCampContentTextControlPoint(page, label, occurrence = 0) {
return probe;
}
async function readCampSupplyUseControlPoint(page, supplyLabel) {
const probe = await page.evaluate((supplyLabel) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const canvas = document.querySelector('canvas');
const texts = (scene?.contentObjects ?? []).filter((object) => object?.type === 'Text' && object?.active && object?.visible);
const supplyTitle = texts.find((object) => object.text?.startsWith(`${supplyLabel} x`));
const useText = texts
.filter((object) => object.text === '사용')
.sort((left, right) => Math.abs(left.y - (supplyTitle?.y ?? 0)) - Math.abs(right.y - (supplyTitle?.y ?? 0)))[0];
if (!scene || !canvas || !supplyTitle || !useText) {
return {
ready: false,
supplyLabel,
supplyTitle: supplyTitle?.text ?? null,
useCount: texts.filter((object) => object.text === '사용').length
};
}
const bounds = useText.getBounds();
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
supplyLabel,
supplyTitle: supplyTitle.text,
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, supplyLabel);
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible ${supplyLabel} supply use control: ${JSON.stringify(probe)}`
);
return probe;
}
async function readSortieComparisonActionPoint(page) {
const probe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');