feat: reopen formation reviews in camp

This commit is contained in:
2026-07-10 23:38:14 +09:00
parent 78669d3c6e
commit f3d0127ab7
4 changed files with 1081 additions and 151 deletions

View File

@@ -217,6 +217,135 @@ try {
`Expected campaign report debug state to retain first battle unlock reward: ${JSON.stringify(firstCampProbe.state?.report?.campaignRewards)}`
);
const firstCampEvaluation = firstCampProbe.state?.reportFormationEvaluation;
const firstCampEvaluationSaveBefore = await readCampaignSave(page);
assert(
firstCampEvaluation?.available === true &&
firstCampEvaluation.open === false &&
firstCampEvaluation.battleId === 'first-battle-zhuo-commandery' &&
firstCampEvaluation.grade === firstResultState?.sortieEvaluation?.grade &&
firstCampEvaluation.score === firstResultState?.sortieEvaluation?.score &&
sameJsonValue(firstCampEvaluation.snapshot, firstResultPerformance) &&
firstCampEvaluation.toggleButtonBounds,
`Expected the first camp report to expose the persisted battle formation evaluation: ${JSON.stringify(firstCampEvaluation)}`
);
const firstCampEvaluationTogglePoint = await readCampReportFormationEvaluationControlPoint(page, 'toggle');
await page.mouse.click(firstCampEvaluationTogglePoint.x, firstCampEvaluationTogglePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation?.open === true,
undefined,
{ timeout: 30000 }
);
const firstCampEvaluationProbe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
return {
evaluation: window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation,
texts: (scene?.reportFormationReviewObjects ?? [])
.filter((object) => object?.type === 'Text')
.map((object) => object.text)
};
});
const firstCampEvaluationSaveOpen = await readCampaignSave(page);
assert(
firstCampEvaluationProbe.evaluation?.open === true &&
firstCampEvaluationProbe.texts.includes('최근 전투 편성 평가') &&
firstCampEvaluationProbe.texts.some((text) => text.includes('명단·역할만 저장') && text.includes('장비·보급 제외')) &&
firstCampEvaluationProbe.evaluation?.presetCards?.length === 3 &&
firstCampEvaluationProbe.evaluation.presetCards.every(
(card) => card.saved === false && card.matching === false && card.actionButtonBounds
) &&
sameJsonValue(firstCampEvaluationSaveOpen, firstCampEvaluationSaveBefore),
`Expected reopening the first camp formation evaluation to remain non-destructive and show three empty preset actions: ${JSON.stringify(firstCampEvaluationProbe)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-formation-evaluation.png`, fullPage: true });
await assertCanvasPainted(page, 'first camp formation evaluation');
const firstCampEvaluationClosePoint = await readCampReportFormationEvaluationControlPoint(page, 'close');
await page.mouse.click(firstCampEvaluationClosePoint.x, firstCampEvaluationClosePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation?.open === false,
undefined,
{ timeout: 30000 }
);
const firstCampSupplyMutationFixture = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp();
const campaign = scene?.campaign;
const report = scene?.report;
const targetId = state?.reportFormationEvaluation?.snapshot?.selectedUnitIds?.find((unitId) =>
campaign?.roster?.some((unit) => unit.id === unitId)
);
const target = campaign?.roster?.find((unit) => unit.id === targetId);
const reportUnit = report?.units?.find((unit) => unit.id === targetId && unit.faction === 'ally');
const settlementUnit = campaign?.battleHistory?.[report?.battleId]?.units?.find((unit) => unit.unitId === targetId);
if (!scene || !campaign || !report || !target || !reportUnit || !settlementUnit || typeof scene.render !== 'function') {
return { ready: false, targetId: targetId ?? null };
}
const beanBefore = campaign.inventory?.['콩'] ?? 0;
target.hp = Math.max(1, target.maxHp - 12);
reportUnit.hp = target.hp;
campaign.inventory['콩'] = beanBefore + 1;
scene.selectedUnitId = target.id;
scene.render();
return {
ready: true,
targetId: target.id,
woundedHp: target.hp,
maxHp: target.maxHp,
settlementHp: settlementUnit.hp,
beanBefore
};
});
assert(
firstCampSupplyMutationFixture.ready === true &&
firstCampSupplyMutationFixture.targetId &&
firstCampSupplyMutationFixture.woundedHp < firstCampSupplyMutationFixture.maxHp,
`Expected a deterministic wounded report-unit fixture for camp supply recovery: ${JSON.stringify(firstCampSupplyMutationFixture)}`
);
await page.mouse.click(798, 38);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.activeTab === 'supplies',
undefined,
{ timeout: 30000 }
);
const firstCampUseSupplyPoint = await readCampContentTextControlPoint(page, '사용', 0);
await page.mouse.click(firstCampUseSupplyPoint.x, firstCampUseSupplyPoint.y);
await page.waitForFunction((fixture) => {
const state = window.__HEROS_DEBUG__?.camp();
const target = state?.campaign?.roster?.find((unit) => unit.id === fixture.targetId);
return target?.hp === fixture.maxHp;
}, firstCampSupplyMutationFixture, { timeout: 30000 });
const firstCampAfterSupplyProbe = await page.evaluate((fixture) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp();
const report = scene?.report;
const settlement = scene?.campaign?.battleHistory?.[report?.battleId];
return {
evaluation: state?.reportFormationEvaluation,
rosterHp: state?.campaign?.roster?.find((unit) => unit.id === fixture.targetId)?.hp ?? null,
reportHp: report?.units?.find((unit) => unit.id === fixture.targetId && unit.faction === 'ally')?.hp ?? null,
settlementHp: settlement?.units?.find((unit) => unit.unitId === fixture.targetId)?.hp ?? null,
beanAmount: state?.campaign?.inventory?.['콩'] ?? 0
};
}, firstCampSupplyMutationFixture);
assert(
firstCampAfterSupplyProbe.rosterHp === firstCampSupplyMutationFixture.maxHp &&
firstCampAfterSupplyProbe.reportHp === firstCampSupplyMutationFixture.maxHp &&
firstCampAfterSupplyProbe.settlementHp === firstCampSupplyMutationFixture.settlementHp &&
firstCampAfterSupplyProbe.beanAmount === firstCampSupplyMutationFixture.beanBefore &&
firstCampAfterSupplyProbe.evaluation?.grade === firstCampEvaluation.grade &&
firstCampAfterSupplyProbe.evaluation?.score === firstCampEvaluation.score &&
firstCampAfterSupplyProbe.evaluation?.survivorCount === firstCampEvaluation.survivorCount &&
firstCampAfterSupplyProbe.evaluation?.recoveryNeededCount === firstCampEvaluation.recoveryNeededCount &&
sameJsonValue(firstCampAfterSupplyProbe.evaluation?.snapshot, firstCampEvaluation.snapshot),
`Expected camp supply use to update report HP without rewriting the settlement-backed formation evaluation: ${JSON.stringify({
before: firstCampEvaluation,
fixture: firstCampSupplyMutationFixture,
after: firstCampAfterSupplyProbe
})}`
);
const firstCampSelectedSortieUnitIds = firstCampProbe.state?.selectedSortieUnitIds ?? [];
const firstCampDeploymentPreview = firstCampProbe.state?.sortieDeploymentPreview ?? [];
await page.mouse.click(1160, 38);
@@ -1008,6 +1137,44 @@ try {
})}`
);
const openPresetBookForSiegeFixturePoint = await readSortiePresetControlPoint(page, 'toggle');
await page.mouse.click(openPresetBookForSiegeFixturePoint.x, openPresetBookForSiegeFixturePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.sortiePresetBrowser?.open === true,
undefined,
{ timeout: 30000 }
);
const saveSiegePresetPoint = await readSortiePresetControlPoint(page, 'save', 'siege');
await page.mouse.click(saveSiegePresetPoint.x, saveSiegePresetPoint.y);
await page.waitForFunction(() => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.sortiePresetBrowser?.open === true &&
state?.sortieFormationPresets?.siege?.selectedUnitIds?.length > 0;
}, undefined, { timeout: 30000 });
const savedSiegeFixtureState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const savedSiegeFixtureSave = await readCampaignSave(page);
const savedSiegePreset = savedSiegeFixtureState?.sortieFormationPresets?.siege;
assert(
savedSiegePreset?.selectedUnitIds?.length === savedSiegeFixtureState?.selectedSortieUnitIds?.length &&
savedSiegePreset.selectedUnitIds.every((unitId) => Boolean(savedSiegePreset.formationAssignments?.[unitId])) &&
JSON.stringify(Object.keys(savedSiegePreset).sort()) === JSON.stringify(['formationAssignments', 'selectedUnitIds']) &&
savedSiegePreset.sortieItemAssignments === undefined &&
savedSiegePreset.itemAssignments === undefined &&
sameJsonValue(savedSiegeFixtureSave.current?.sortieFormationPresets?.siege, savedSiegePreset) &&
sameJsonValue(savedSiegeFixtureSave.slot1?.sortieFormationPresets?.siege, savedSiegePreset),
`Expected a deterministic pre-battle siege preset fixture containing only the current roster and roles: ${JSON.stringify({
state: savedSiegeFixtureState,
save: savedSiegeFixtureSave
})}`
);
const closePresetBookAfterSiegeFixturePoint = await readSortiePresetControlPoint(page, 'close');
await page.mouse.click(closePresetBookAfterSiegeFixturePoint.x, closePresetBookAfterSiegeFixturePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.sortiePresetBrowser?.open === false,
undefined,
{ timeout: 30000 }
);
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
await page.mouse.click(658, 196);
await page.waitForFunction((previousScroll) => {
@@ -1363,6 +1530,146 @@ try {
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-formation-evaluation-updated.png`, fullPage: true });
await assertCanvasPainted(page, 'mid camp formation evaluation update');
const closeMidBattleEvaluationPoint = await readBattleResultEvaluationControlPoint(page, 'close');
await page.mouse.click(closeMidBattleEvaluationPoint.x, closeMidBattleEvaluationPoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.battle()?.sortieEvaluation?.open === false,
undefined,
{ timeout: 30000 }
);
await page.mouse.click(738, 642);
await waitForCampAfterBattleResult(page);
const midCampReviewState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const midCampReviewSaveBefore = await readCampaignSave(page);
const midCampFormationEvaluation = midCampReviewState?.reportFormationEvaluation;
const midCampSiegeCard = midCampFormationEvaluation?.presetCards?.find((card) => card.id === 'siege');
assert(
midCampFormationEvaluation?.available === true &&
midCampFormationEvaluation.open === false &&
midCampFormationEvaluation.battleId === midCampNextBattleId &&
midCampFormationEvaluation.grade === updatedMobileResultState?.sortieEvaluation?.grade &&
midCampFormationEvaluation.score === updatedMobileResultState?.sortieEvaluation?.score &&
sameJsonValue(midCampFormationEvaluation.snapshot, midResultPerformance) &&
midCampFormationEvaluation.sourcePresetIds?.includes('mobile') === true &&
midCampFormationEvaluation.sourcePresetIds?.includes('siege') === false &&
midCampSiegeCard?.saved === true &&
midCampSiegeCard.matching === false &&
midCampFormationEvaluation.toggleButtonBounds &&
!sameJsonValue(savedSiegePreset, expectedMidResultPreset),
`Expected the next camp report to reopen the persisted result while keeping the older siege preset distinct: ${JSON.stringify({
evaluation: midCampFormationEvaluation,
savedSiegePreset
})}`
);
const midCampReviewTogglePoint = await readCampReportFormationEvaluationControlPoint(page, 'toggle');
await page.mouse.click(midCampReviewTogglePoint.x, midCampReviewTogglePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation?.open === true,
undefined,
{ timeout: 30000 }
);
const openedMidCampReviewProbe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
return {
state: window.__HEROS_DEBUG__?.camp(),
texts: (scene?.reportFormationReviewObjects ?? [])
.filter((object) => object?.type === 'Text')
.map((object) => object.text)
};
});
const openedMidCampReviewSave = await readCampaignSave(page);
const openedMidCampSiegeCard = openedMidCampReviewProbe.state?.reportFormationEvaluation?.presetCards?.find(
(card) => card.id === 'siege'
);
assert(
openedMidCampReviewProbe.state?.reportFormationEvaluation?.open === true &&
openedMidCampReviewProbe.texts.includes('최근 전투 편성 평가') &&
openedMidCampReviewProbe.texts.some((text) => text.includes('공성') && text.includes('갱신')) &&
openedMidCampSiegeCard?.saved === true &&
openedMidCampSiegeCard.matching === false &&
openedMidCampSiegeCard.actionButtonBounds &&
sameJsonValue(openedMidCampReviewSave, midCampReviewSaveBefore),
`Expected opening the camp report evaluation to remain non-destructive and expose the siege update action: ${JSON.stringify(openedMidCampReviewProbe)}`
);
const firstCampSiegeUpdatePoint = await readCampReportFormationEvaluationControlPoint(page, 'preset', 'siege');
await page.mouse.click(firstCampSiegeUpdatePoint.x, firstCampSiegeUpdatePoint.y);
await page.waitForFunction(() => {
const evaluation = window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation;
return evaluation?.open === true &&
evaluation?.overwriteConfirmId === 'siege' &&
evaluation?.feedback?.includes('한 번 더');
}, undefined, { timeout: 30000 });
const pendingCampSiegeUpdateState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const pendingCampSiegeUpdateSave = await readCampaignSave(page);
assert(
pendingCampSiegeUpdateState?.reportFormationEvaluation?.sourcePresetIds?.includes('siege') === false &&
pendingCampSiegeUpdateState?.reportFormationEvaluation?.presetCards?.find((card) => card.id === 'siege')?.matching === false &&
sameJsonValue(pendingCampSiegeUpdateSave, midCampReviewSaveBefore),
`Expected the first camp siege update click to request confirmation without mutating either save: ${JSON.stringify({
evaluation: pendingCampSiegeUpdateState?.reportFormationEvaluation,
save: pendingCampSiegeUpdateSave
})}`
);
const confirmCampSiegeUpdatePoint = await readCampReportFormationEvaluationControlPoint(page, 'preset', 'siege');
await page.mouse.click(confirmCampSiegeUpdatePoint.x, confirmCampSiegeUpdatePoint.y);
await page.waitForFunction(() => {
const evaluation = window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation;
return evaluation?.open === true &&
evaluation?.overwriteConfirmId === null &&
evaluation?.sourcePresetIds?.includes('siege') &&
evaluation?.feedback?.includes('갱신 완료');
}, undefined, { timeout: 30000 });
const updatedCampSiegeState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const updatedCampSiegeSave = await readCampaignSave(page);
const updatedCurrentSiegePreset = updatedCampSiegeSave.current?.sortieFormationPresets?.siege;
const updatedSlotSiegePreset = updatedCampSiegeSave.slot1?.sortieFormationPresets?.siege;
assert(
sameJsonValue(updatedCurrentSiegePreset, expectedMidResultPreset) &&
sameJsonValue(updatedSlotSiegePreset, expectedMidResultPreset) &&
JSON.stringify(Object.keys(updatedCurrentSiegePreset ?? {}).sort()) === JSON.stringify(['formationAssignments', 'selectedUnitIds']) &&
updatedCurrentSiegePreset?.sortieItemAssignments === undefined &&
updatedCurrentSiegePreset?.itemAssignments === undefined &&
updatedCurrentSiegePreset?.unitStats === undefined,
`Expected the confirmed camp action to store only the reviewed roster and roles in both siege preset saves: ${JSON.stringify(updatedCampSiegeSave)}`
);
assert(
JSON.stringify(updatedCampSiegeSave.current?.selectedSortieUnitIds) === JSON.stringify(midCampReviewSaveBefore.current?.selectedSortieUnitIds) &&
sameJsonValue(updatedCampSiegeSave.current?.sortieFormationAssignments, midCampReviewSaveBefore.current?.sortieFormationAssignments) &&
sameJsonValue(updatedCampSiegeSave.current?.sortieItemAssignments, midCampReviewSaveBefore.current?.sortieItemAssignments) &&
JSON.stringify(updatedCampSiegeSave.slot1?.selectedSortieUnitIds) === JSON.stringify(midCampReviewSaveBefore.slot1?.selectedSortieUnitIds) &&
sameJsonValue(updatedCampSiegeSave.slot1?.sortieFormationAssignments, midCampReviewSaveBefore.slot1?.sortieFormationAssignments) &&
sameJsonValue(updatedCampSiegeSave.slot1?.sortieItemAssignments, midCampReviewSaveBefore.slot1?.sortieItemAssignments) &&
sameJsonValue(updatedCampSiegeSave.current?.firstBattleReport?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedCampSiegeSave.slot1?.battleHistory?.[midCampNextBattleId]?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedCampSiegeSave.current?.sortieFormationPresets?.mobile, midCampReviewSaveBefore.current?.sortieFormationPresets?.mobile) &&
sameJsonValue(updatedCampSiegeSave.current?.sortieFormationPresets?.elite, midCampReviewSaveBefore.current?.sortieFormationPresets?.elite) &&
JSON.stringify(updatedCampSiegeState?.selectedSortieUnitIds) === JSON.stringify(midCampReviewState?.selectedSortieUnitIds) &&
sameJsonValue(updatedCampSiegeState?.sortieFormationAssignments, midCampReviewState?.sortieFormationAssignments) &&
sameJsonValue(updatedCampSiegeState?.sortieItemAssignments, midCampReviewState?.sortieItemAssignments),
`Expected the camp review update to preserve the active sortie, supplies, report snapshot, and other presets: ${JSON.stringify({
before: midCampReviewSaveBefore,
after: updatedCampSiegeSave
})}`
);
assert(
updatedCampSiegeState?.reportFormationEvaluation?.presetCards?.find((card) => card.id === 'siege')?.matching === true &&
updatedCampSiegeState?.reportFormationEvaluation?.feedback?.includes('장비·보급은 저장하지 않았습니다.'),
`Expected visible completion feedback and a matching siege card after the camp update: ${JSON.stringify(updatedCampSiegeState?.reportFormationEvaluation)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-report-formation-evaluation-updated.png`, fullPage: true });
await assertCanvasPainted(page, 'mid camp report formation evaluation update');
const closeUpdatedCampReviewPoint = await readCampReportFormationEvaluationControlPoint(page, 'close');
await page.mouse.click(closeUpdatedCampReviewPoint.x, closeUpdatedCampReviewPoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation?.open === false,
undefined,
{ timeout: 30000 }
);
await seedCampaignSave(page, {
battleId: 'sixty-sixth-battle-wuzhang-final',
battleTitle: '오장원 최종전',
@@ -1381,6 +1688,13 @@ try {
assert(finalCampState.sortieHasBattle === false, `Expected final camp to expose no playable sortie target: ${JSON.stringify(finalCampState)}`);
assert(finalCampState.stagedSortiePrep === false, `Expected the non-battle final camp to avoid the three-stage sortie flow: ${JSON.stringify(finalCampState)}`);
assert(finalCampState.nextSortieBattleId === null, `Expected final camp to route to ending instead of a battle: ${JSON.stringify(finalCampState)}`);
assert(
finalCampState.reportFormationEvaluation?.available === false &&
finalCampState.reportFormationEvaluation.open === false &&
finalCampState.reportFormationEvaluation.toggleButtonBounds === null &&
finalCampState.reportFormationEvaluation.presetCards?.length === 0,
`Expected a latest report without sortie performance to hide the camp evaluation instead of falling back to older history: ${JSON.stringify(finalCampState.reportFormationEvaluation)}`
);
assert(
Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0,
`Expected final camp deployment preview to stay empty: ${JSON.stringify(finalCampState?.sortieDeploymentPreview)}`
@@ -1723,6 +2037,80 @@ async function readBattleResultEvaluationControlPoint(page, control, presetId) {
return probe;
}
async function readCampReportFormationEvaluationControlPoint(page, control, presetId) {
const probe = await page.evaluate(({ control, presetId }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const evaluation = window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation;
const canvas = document.querySelector('canvas');
const presetCard = evaluation?.presetCards?.find((card) => card.id === presetId);
const bounds = control === 'toggle'
? evaluation?.toggleButtonBounds
: control === 'close'
? evaluation?.closeButtonBounds
: presetCard?.actionButtonBounds;
if (!scene || !canvas || !bounds) {
return {
ready: false,
control,
presetId: presetId ?? null,
evaluationOpen: evaluation?.open ?? false,
bounds: bounds ?? null
};
}
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
control,
presetId: presetId ?? null,
evaluationOpen: evaluation?.open ?? false,
bounds,
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
};
}, { control, presetId });
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible Phaser camp-report evaluation ${control} control with a canvas-scaled click point: ${JSON.stringify(probe)}`
);
return probe;
}
async function readCampContentTextControlPoint(page, label, occurrence = 0) {
const probe = await page.evaluate(({ label, occurrence }) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const canvas = document.querySelector('canvas');
const candidates = (scene?.contentObjects ?? [])
.filter((object) => object?.type === 'Text' && object?.text === label && object?.active && object?.visible)
.sort((left, right) => left.y - right.y || left.x - right.x);
const target = candidates[occurrence];
if (!scene || !canvas || !target) {
return { ready: false, label, occurrence, candidateCount: candidates.length };
}
const bounds = target.getBounds();
const canvasBounds = canvas.getBoundingClientRect();
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
return {
ready: true,
label,
occurrence,
candidateCount: candidates.length,
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
};
}, { label, occurrence });
assert(
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
`Expected a visible Phaser camp text control "${label}" with a canvas-scaled click point: ${JSON.stringify(probe)}`
);
return probe;
}
async function readSortieComparisonActionPoint(page) {
const probe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');