feat: add sortie formation history

This commit is contained in:
2026-07-11 00:19:02 +09:00
parent f3d0127ab7
commit 0920039b9c
6 changed files with 1768 additions and 30 deletions

View File

@@ -132,6 +132,28 @@ try {
sameJsonValue(firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortiePerformance, firstResultPerformance),
`Expected the first formation result snapshot to synchronize across report, history, current, and slot-1 saves: ${JSON.stringify(firstResultSave)}`
);
const expectedFirstSortieReview = {
version: 1,
score: firstResultState.sortieEvaluation.score,
grade: firstResultState.sortieEvaluation.grade,
activeBondCount: firstResultState.sortieEvaluation.activeBondCount,
enemyCount: firstResultState.units.filter((unit) => unit.faction === 'enemy').length,
sourcePresetIds: []
};
assert(
sameJsonValue(firstResultSave.current?.firstBattleReport?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(firstResultSave.slot1?.firstBattleReport?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(firstResultSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview, expectedFirstSortieReview) &&
firstResultSave.current.firstBattleReport.sortieReview.sourcePresetIds.length === 0,
`Expected the first persisted sortie review to match the live grade, score, enemies, bonds, and empty preset attribution everywhere: ${JSON.stringify({
expected: expectedFirstSortieReview,
currentReport: firstResultSave.current?.firstBattleReport?.sortieReview,
slotReport: firstResultSave.slot1?.firstBattleReport?.sortieReview,
currentHistory: firstResultSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview,
slotHistory: firstResultSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview
})}`
);
const firstEvaluationTogglePoint = await readBattleResultEvaluationControlPoint(page, 'toggle');
await page.mouse.click(firstEvaluationTogglePoint.x, firstEvaluationTogglePoint.y);
@@ -218,6 +240,7 @@ try {
);
const firstCampEvaluation = firstCampProbe.state?.reportFormationEvaluation;
const firstCampHistory = firstCampProbe.state?.reportFormationHistory;
const firstCampEvaluationSaveBefore = await readCampaignSave(page);
assert(
firstCampEvaluation?.available === true &&
@@ -229,6 +252,24 @@ try {
firstCampEvaluation.toggleButtonBounds,
`Expected the first camp report to expose the persisted battle formation evaluation: ${JSON.stringify(firstCampEvaluation)}`
);
assert(
firstCampHistory?.available === true &&
firstCampHistory.open === false &&
firstCampHistory.recordCount === 1 &&
firstCampHistory.page === 0 &&
firstCampHistory.pageCount === 1 &&
firstCampHistory.records?.length === 1 &&
firstCampHistory.records[0].battleId === 'first-battle-zhuo-commandery' &&
firstCampHistory.records[0].grade === expectedFirstSortieReview.grade &&
firstCampHistory.records[0].score === expectedFirstSortieReview.score &&
sameJsonValue(firstCampHistory.records[0].sourcePresetIds, expectedFirstSortieReview.sourcePresetIds) &&
firstCampHistory.selectedBattleId === 'first-battle-zhuo-commandery' &&
firstCampHistory.best?.battleId === 'first-battle-zhuo-commandery' &&
firstCampHistory.loadableBest?.battleId === 'first-battle-zhuo-commandery' &&
firstCampHistory.loadUndoAvailable === false &&
firstCampHistory.toggleButtonBounds,
`Expected the first camp report to expose one closed, persisted formation-history record and its best loadable sortie: ${JSON.stringify(firstCampHistory)}`
);
const firstCampEvaluationTogglePoint = await readCampReportFormationEvaluationControlPoint(page, 'toggle');
await page.mouse.click(firstCampEvaluationTogglePoint.x, firstCampEvaluationTogglePoint.y);
await page.waitForFunction(
@@ -267,6 +308,338 @@ try {
{ timeout: 30000 }
);
const firstCampHistoryMutationFixture = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const campaign = scene?.campaign;
const snapshot = campaign?.battleHistory?.['first-battle-zhuo-commandery']?.sortiePerformance;
const targetId = snapshot?.selectedUnitIds?.find((unitId) => scene?.selectedSortieUnitIds?.includes(unitId));
const historicalRole = targetId ? snapshot?.formationAssignments?.[targetId] : undefined;
const divergentRole = ['front', 'flank', 'support', 'reserve'].find((role) => role !== historicalRole);
if (
!scene ||
!campaign ||
!snapshot?.selectedUnitIds?.length ||
!targetId ||
!historicalRole ||
!divergentRole ||
typeof scene.persistSortieSelection !== 'function' ||
typeof scene.render !== 'function'
) {
return {
ready: false,
targetId: targetId ?? null,
historicalRole: historicalRole ?? null,
divergentRole: divergentRole ?? null
};
}
const before = {
selectedUnitIds: [...scene.selectedSortieUnitIds],
formationAssignments: { ...scene.sortieFormationAssignments },
itemAssignments: structuredClone(scene.sortieItemAssignments ?? {}),
focusedUnitId: scene.sortieFocusedUnitId,
planFeedback: scene.sortiePlanFeedback,
rosterScroll: scene.sortieRosterScroll
};
scene.sortieFormationAssignments = {
...scene.sortieFormationAssignments,
[targetId]: divergentRole
};
scene.persistSortieSelection();
scene.render();
return {
ready: true,
targetId,
historicalRole,
divergentRole,
before,
divergent: {
selectedUnitIds: [...scene.selectedSortieUnitIds],
formationAssignments: { ...scene.sortieFormationAssignments },
itemAssignments: structuredClone(scene.sortieItemAssignments ?? {})
}
};
});
assert(
firstCampHistoryMutationFixture.ready === true &&
firstCampHistoryMutationFixture.historicalRole !== firstCampHistoryMutationFixture.divergentRole &&
firstCampHistoryMutationFixture.divergent?.formationAssignments?.[firstCampHistoryMutationFixture.targetId] ===
firstCampHistoryMutationFixture.divergentRole,
`Expected a deterministic current-formation mismatch before exercising best-history loading: ${JSON.stringify(firstCampHistoryMutationFixture)}`
);
const firstCampHistorySaveBeforeOpen = await readCampaignSave(page);
assert(
sameJsonValue(
firstCampHistorySaveBeforeOpen.current?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.divergent.selectedUnitIds
) &&
sameJsonValue(
firstCampHistorySaveBeforeOpen.current?.sortieFormationAssignments,
firstCampHistoryMutationFixture.divergent.formationAssignments
) &&
sameJsonValue(
firstCampHistorySaveBeforeOpen.current?.sortieItemAssignments,
firstCampHistoryMutationFixture.divergent.itemAssignments
) &&
sameJsonValue(
firstCampHistorySaveBeforeOpen.slot1?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.divergent.selectedUnitIds
) &&
sameJsonValue(
firstCampHistorySaveBeforeOpen.slot1?.sortieFormationAssignments,
firstCampHistoryMutationFixture.divergent.formationAssignments
) &&
sameJsonValue(
firstCampHistorySaveBeforeOpen.slot1?.sortieItemAssignments,
firstCampHistoryMutationFixture.divergent.itemAssignments
),
`Expected the deliberate history-load mismatch to synchronize across current and slot-1 saves: ${JSON.stringify(firstCampHistorySaveBeforeOpen)}`
);
const firstCampHistoryTogglePoint = await readCampReportFormationHistoryControlPoint(page, 'toggle');
await page.mouse.click(firstCampHistoryTogglePoint.x, firstCampHistoryTogglePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationHistory?.open === true,
undefined,
{ timeout: 30000 }
);
const firstCampHistoryOpenProbe = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
return {
history: window.__HEROS_DEBUG__?.camp()?.reportFormationHistory,
texts: (scene?.reportFormationHistoryObjects ?? [])
.filter((object) => object?.type === 'Text')
.map((object) => object.text)
};
});
const firstCampHistorySaveOpen = await readCampaignSave(page);
const firstCampHistoryLoadProjection = firstCampHistoryOpenProbe.history?.loadableBest;
assert(
firstCampHistoryOpenProbe.history?.open === true &&
firstCampHistoryOpenProbe.history.recordCount === 1 &&
firstCampHistoryOpenProbe.history.comparison?.matchesCurrent === false &&
firstCampHistoryOpenProbe.history.closeButtonBounds &&
firstCampHistoryOpenProbe.history.bestButtonBounds &&
firstCampHistoryOpenProbe.history.loadBestButtonBounds &&
firstCampHistoryOpenProbe.history.undoButtonBounds === null &&
sameJsonValue(firstCampHistoryLoadProjection?.projectedSelectedUnitIds, firstResultPerformance.selectedUnitIds) &&
firstResultPerformance.selectedUnitIds.every(
(unitId) => firstCampHistoryLoadProjection?.projectedFormationAssignments?.[unitId] === firstResultPerformance.formationAssignments[unitId]
) &&
sameJsonValue(
firstCampHistoryLoadProjection?.projectedItemAssignments,
firstCampHistoryMutationFixture.divergent.itemAssignments
) &&
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)}`
);
await page.screenshot({ path: `${screenshotDir}/rc-first-camp-formation-history.png`, fullPage: true });
await assertCanvasPainted(page, 'first camp formation history');
const firstCampHistoryBestPoint = await readCampReportFormationHistoryControlPoint(page, 'best');
await page.mouse.click(firstCampHistoryBestPoint.x, firstCampHistoryBestPoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationHistory?.selectedBattleId === 'first-battle-zhuo-commandery',
undefined,
{ timeout: 30000 }
);
const firstCampHistorySaveAfterBest = await readCampaignSave(page);
assert(
sameJsonValue(firstCampHistorySaveAfterBest, firstCampHistorySaveBeforeOpen),
`Expected selecting the best history card to leave campaign saves untouched: ${JSON.stringify(firstCampHistorySaveAfterBest)}`
);
const firstCampHistoryLoadBestPoint = await readCampReportFormationHistoryControlPoint(page, 'loadBest');
await page.mouse.click(firstCampHistoryLoadBestPoint.x, firstCampHistoryLoadBestPoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationHistory?.loadUndoAvailable === true,
undefined,
{ timeout: 30000 }
);
const firstCampHistoryAppliedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const firstCampHistoryAppliedSave = await readCampaignSave(page);
assert(
sameJsonValue(firstCampHistoryAppliedState?.selectedSortieUnitIds, firstCampHistoryLoadProjection.projectedSelectedUnitIds) &&
sameJsonValue(
firstCampHistoryAppliedState?.sortieFormationAssignments,
firstCampHistoryLoadProjection.projectedFormationAssignments
) &&
sameJsonValue(
firstCampHistoryAppliedState?.sortieItemAssignments,
firstCampHistoryLoadProjection.projectedItemAssignments
) &&
firstCampHistoryAppliedState?.reportFormationHistory?.comparison?.matchesCurrent === true &&
firstCampHistoryAppliedState?.reportFormationHistory?.loadUndoAvailable === true &&
firstCampHistoryAppliedState?.reportFormationHistory?.undoButtonBounds &&
firstCampHistoryAppliedState?.reportFormationHistory?.feedback?.includes('최고 편성 적용') &&
sameJsonValue(
firstCampHistoryAppliedSave.current?.selectedSortieUnitIds,
firstCampHistoryLoadProjection.projectedSelectedUnitIds
) &&
sameJsonValue(
firstCampHistoryAppliedSave.slot1?.selectedSortieUnitIds,
firstCampHistoryLoadProjection.projectedSelectedUnitIds
) &&
sameJsonValue(
firstCampHistoryAppliedSave.current?.sortieFormationAssignments,
firstCampHistoryAppliedState.sortieFormationAssignments
) &&
sameJsonValue(
firstCampHistoryAppliedSave.slot1?.sortieFormationAssignments,
firstCampHistoryAppliedState.sortieFormationAssignments
) &&
sameJsonValue(
firstCampHistoryAppliedSave.current?.sortieItemAssignments,
firstCampHistoryLoadProjection.projectedItemAssignments
) &&
sameJsonValue(
firstCampHistoryAppliedSave.slot1?.sortieItemAssignments,
firstCampHistoryLoadProjection.projectedItemAssignments
) &&
sameJsonValue(firstCampHistoryAppliedSave.current?.firstBattleReport?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(
firstCampHistoryAppliedSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview,
expectedFirstSortieReview
) &&
sameJsonValue(firstCampHistoryAppliedSave.slot1?.firstBattleReport?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(
firstCampHistoryAppliedSave.slot1?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview,
expectedFirstSortieReview
),
`Expected one best-history click to apply persisted names and roles, preserve common supplies, and keep the stored review immutable: ${JSON.stringify({
state: firstCampHistoryAppliedState,
save: firstCampHistoryAppliedSave
})}`
);
const firstCampHistoryUndoPoint = await readCampReportFormationHistoryControlPoint(page, 'undo');
await page.mouse.click(firstCampHistoryUndoPoint.x, firstCampHistoryUndoPoint.y);
await page.waitForFunction(
(fixture) => {
const state = window.__HEROS_DEBUG__?.camp();
return state?.reportFormationHistory?.loadUndoAvailable === false &&
JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(fixture.divergent.selectedUnitIds) &&
JSON.stringify(state?.sortieFormationAssignments) === JSON.stringify(fixture.divergent.formationAssignments) &&
JSON.stringify(state?.sortieItemAssignments) === JSON.stringify(fixture.divergent.itemAssignments);
},
firstCampHistoryMutationFixture,
{ timeout: 30000 }
);
const firstCampHistoryUndoneState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const firstCampHistoryUndoneSave = await readCampaignSave(page);
assert(
firstCampHistoryUndoneState?.reportFormationHistory?.open === true &&
firstCampHistoryUndoneState.reportFormationHistory.loadUndoAvailable === false &&
firstCampHistoryUndoneState.reportFormationHistory.undoButtonBounds === null &&
firstCampHistoryUndoneState.reportFormationHistory.comparison?.matchesCurrent === false &&
firstCampHistoryUndoneState.reportFormationHistory.feedback?.includes('되돌리고') &&
sameJsonValue(
firstCampHistoryUndoneSave.current?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.divergent.selectedUnitIds
) &&
sameJsonValue(
firstCampHistoryUndoneSave.current?.sortieFormationAssignments,
firstCampHistoryMutationFixture.divergent.formationAssignments
) &&
sameJsonValue(
firstCampHistoryUndoneSave.current?.sortieItemAssignments,
firstCampHistoryMutationFixture.divergent.itemAssignments
) &&
sameJsonValue(
firstCampHistoryUndoneSave.slot1?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.divergent.selectedUnitIds
) &&
sameJsonValue(
firstCampHistoryUndoneSave.slot1?.sortieFormationAssignments,
firstCampHistoryMutationFixture.divergent.formationAssignments
) &&
sameJsonValue(
firstCampHistoryUndoneSave.slot1?.sortieItemAssignments,
firstCampHistoryMutationFixture.divergent.itemAssignments
) &&
sameJsonValue(firstCampHistoryUndoneSave.current?.firstBattleReport?.sortieReview, expectedFirstSortieReview) &&
sameJsonValue(
firstCampHistoryUndoneSave.current?.battleHistory?.['first-battle-zhuo-commandery']?.sortieReview,
expectedFirstSortieReview
),
`Expected the real history undo control to restore the exact prior names, roles, and supplies without touching the battle review: ${JSON.stringify({
state: firstCampHistoryUndoneState,
save: firstCampHistoryUndoneSave
})}`
);
const firstCampHistoryClosePoint = await readCampReportFormationHistoryControlPoint(page, 'close');
await page.mouse.click(firstCampHistoryClosePoint.x, firstCampHistoryClosePoint.y);
await page.waitForFunction(
() => window.__HEROS_DEBUG__?.camp()?.reportFormationHistory?.open === false,
undefined,
{ timeout: 30000 }
);
const firstCampHistoryClosedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
assert(
firstCampHistoryClosedState?.reportFormationHistory?.recordCount === 1 &&
firstCampHistoryClosedState.reportFormationHistory.open === false &&
firstCampHistoryClosedState.reportFormationHistory.closeButtonBounds === null &&
firstCampHistoryClosedState.reportFormationHistory.toggleButtonBounds,
`Expected the real history close control to return to the first camp report: ${JSON.stringify(firstCampHistoryClosedState?.reportFormationHistory)}`
);
await page.evaluate((before) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
if (!scene || typeof scene.persistSortieSelection !== 'function' || typeof scene.render !== 'function') {
throw new Error('Expected CampScene runtime methods while restoring the first-camp history fixture.');
}
scene.selectedSortieUnitIds = [...before.selectedUnitIds];
scene.sortieFormationAssignments = { ...before.formationAssignments };
scene.sortieItemAssignments = structuredClone(before.itemAssignments);
scene.sortieFocusedUnitId = before.focusedUnitId;
scene.sortiePlanFeedback = before.planFeedback;
scene.sortieRosterScroll = before.rosterScroll;
scene.persistSortieSelection();
scene.render();
}, firstCampHistoryMutationFixture.before);
await page.waitForFunction(
(before) => {
const state = window.__HEROS_DEBUG__?.camp();
return JSON.stringify(state?.selectedSortieUnitIds) === JSON.stringify(before.selectedUnitIds) &&
JSON.stringify(state?.sortieFormationAssignments) === JSON.stringify(before.formationAssignments) &&
JSON.stringify(state?.sortieItemAssignments) === JSON.stringify(before.itemAssignments);
},
firstCampHistoryMutationFixture.before,
{ timeout: 30000 }
);
const firstCampHistoryRestoredSave = await readCampaignSave(page);
assert(
sameJsonValue(
firstCampHistoryRestoredSave.current?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.before.selectedUnitIds
) &&
sameJsonValue(
firstCampHistoryRestoredSave.current?.sortieFormationAssignments,
firstCampHistoryMutationFixture.before.formationAssignments
) &&
sameJsonValue(
firstCampHistoryRestoredSave.current?.sortieItemAssignments,
firstCampHistoryMutationFixture.before.itemAssignments
) &&
sameJsonValue(
firstCampHistoryRestoredSave.slot1?.selectedSortieUnitIds,
firstCampHistoryMutationFixture.before.selectedUnitIds
) &&
sameJsonValue(
firstCampHistoryRestoredSave.slot1?.sortieFormationAssignments,
firstCampHistoryMutationFixture.before.formationAssignments
) &&
sameJsonValue(
firstCampHistoryRestoredSave.slot1?.sortieItemAssignments,
firstCampHistoryMutationFixture.before.itemAssignments
),
`Expected the RC-only mismatch fixture to restore the original first-camp sortie before continuing: ${JSON.stringify(firstCampHistoryRestoredSave)}`
);
const firstCampSupplyMutationFixture = await page.evaluate(() => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const state = window.__HEROS_DEBUG__?.camp();
@@ -1396,6 +1769,9 @@ try {
const midResultState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
const midResultSaveBeforeUpdate = await readCampaignSave(page);
const midResultPerformance = midResultState?.sortieEvaluation?.snapshot;
const midResultSortieReviewBeforePresetUpdate = structuredClone(
midResultSaveBeforeUpdate.current?.battleHistory?.[midCampNextBattleId]?.sortieReview
);
const midContributorPerformance = midResultPerformance?.unitStats?.find((stats) => stats.unitId === midResultFixture.contributorId);
const midBattleRoleById = Object.fromEntries(
(midNextBattleProbe?.deployedAllyPositions ?? []).map((unit) => [unit.id, unit.formationRole])
@@ -1438,6 +1814,20 @@ try {
mobile: midResultSaveBeforeUpdate.current?.sortieFormationPresets?.mobile
})}`
);
assert(
midResultSortieReviewBeforePresetUpdate?.version === 1 &&
midResultSortieReviewBeforePresetUpdate.score === midResultState?.sortieEvaluation?.score &&
midResultSortieReviewBeforePresetUpdate.grade === midResultState?.sortieEvaluation?.grade &&
midResultSortieReviewBeforePresetUpdate.activeBondCount === midResultState?.sortieEvaluation?.activeBondCount &&
midResultSortieReviewBeforePresetUpdate.enemyCount === midNextBattleProbe.units.filter((unit) => unit.faction === 'enemy').length &&
sameJsonValue(midResultSaveBeforeUpdate.current?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(midResultSaveBeforeUpdate.slot1?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(midResultSaveBeforeUpdate.slot1?.battleHistory?.[midCampNextBattleId]?.sortieReview, midResultSortieReviewBeforePresetUpdate),
`Expected the mid-campaign result to persist one canonical sortie review before any result-screen preset update: ${JSON.stringify({
review: midResultSortieReviewBeforePresetUpdate,
evaluation: midResultState?.sortieEvaluation
})}`
);
const midEvaluationTogglePoint = await readBattleResultEvaluationControlPoint(page, 'toggle');
await page.mouse.click(midEvaluationTogglePoint.x, midEvaluationTogglePoint.y);
@@ -1515,6 +1905,10 @@ try {
sameJsonValue(updatedMobileResultSave.slot1?.sortieItemAssignments, midResultSaveBeforeUpdate.slot1?.sortieItemAssignments) &&
sameJsonValue(updatedMobileResultSave.current?.firstBattleReport?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedMobileResultSave.slot1?.battleHistory?.[midCampNextBattleId]?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedMobileResultSave.current?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedMobileResultSave.slot1?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedMobileResultSave.current?.battleHistory?.[midCampNextBattleId]?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedMobileResultSave.slot1?.battleHistory?.[midCampNextBattleId]?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedMobileResultSave.current?.sortieFormationPresets?.elite, midResultSaveBeforeUpdate.current?.sortieFormationPresets?.elite) &&
sameJsonValue(updatedMobileResultSave.current?.sortieFormationPresets?.siege, midResultSaveBeforeUpdate.current?.sortieFormationPresets?.siege),
`Expected result-to-preset update to leave the active sortie, supplies, report snapshot, and other presets unchanged: ${JSON.stringify({
@@ -1543,6 +1937,7 @@ try {
const midCampReviewState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const midCampReviewSaveBefore = await readCampaignSave(page);
const midCampFormationEvaluation = midCampReviewState?.reportFormationEvaluation;
const midCampMobileCard = midCampFormationEvaluation?.presetCards?.find((card) => card.id === 'mobile');
const midCampSiegeCard = midCampFormationEvaluation?.presetCards?.find((card) => card.id === 'siege');
assert(
midCampFormationEvaluation?.available === true &&
@@ -1551,8 +1946,9 @@ try {
midCampFormationEvaluation.grade === updatedMobileResultState?.sortieEvaluation?.grade &&
midCampFormationEvaluation.score === updatedMobileResultState?.sortieEvaluation?.score &&
sameJsonValue(midCampFormationEvaluation.snapshot, midResultPerformance) &&
midCampFormationEvaluation.sourcePresetIds?.includes('mobile') === true &&
midCampFormationEvaluation.sourcePresetIds?.includes('siege') === false &&
sameJsonValue(midCampFormationEvaluation.sourcePresetIds, midResultSortieReviewBeforePresetUpdate.sourcePresetIds) &&
midCampMobileCard?.saved === true &&
midCampMobileCard.matching === true &&
midCampSiegeCard?.saved === true &&
midCampSiegeCard.matching === false &&
midCampFormationEvaluation.toggleButtonBounds &&
@@ -1605,7 +2001,10 @@ try {
const pendingCampSiegeUpdateState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
const pendingCampSiegeUpdateSave = await readCampaignSave(page);
assert(
pendingCampSiegeUpdateState?.reportFormationEvaluation?.sourcePresetIds?.includes('siege') === false &&
sameJsonValue(
pendingCampSiegeUpdateState?.reportFormationEvaluation?.sourcePresetIds,
midResultSortieReviewBeforePresetUpdate.sourcePresetIds
) &&
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({
@@ -1620,7 +2019,7 @@ try {
const evaluation = window.__HEROS_DEBUG__?.camp()?.reportFormationEvaluation;
return evaluation?.open === true &&
evaluation?.overwriteConfirmId === null &&
evaluation?.sourcePresetIds?.includes('siege') &&
evaluation?.presetCards?.find((card) => card.id === 'siege')?.matching === true &&
evaluation?.feedback?.includes('갱신 완료');
}, undefined, { timeout: 30000 });
const updatedCampSiegeState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
@@ -1645,6 +2044,10 @@ try {
sameJsonValue(updatedCampSiegeSave.slot1?.sortieItemAssignments, midCampReviewSaveBefore.slot1?.sortieItemAssignments) &&
sameJsonValue(updatedCampSiegeSave.current?.firstBattleReport?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedCampSiegeSave.slot1?.battleHistory?.[midCampNextBattleId]?.sortiePerformance, midResultPerformance) &&
sameJsonValue(updatedCampSiegeSave.current?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedCampSiegeSave.slot1?.firstBattleReport?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedCampSiegeSave.current?.battleHistory?.[midCampNextBattleId]?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
sameJsonValue(updatedCampSiegeSave.slot1?.battleHistory?.[midCampNextBattleId]?.sortieReview, midResultSortieReviewBeforePresetUpdate) &&
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) &&
@@ -2078,6 +2481,50 @@ async function readCampReportFormationEvaluationControlPoint(page, control, pres
return probe;
}
async function readCampReportFormationHistoryControlPoint(page, control) {
const probe = await page.evaluate((control) => {
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
const history = window.__HEROS_DEBUG__?.camp()?.reportFormationHistory;
const canvas = document.querySelector('canvas');
const bounds = control === 'toggle'
? history?.toggleButtonBounds
: control === 'close'
? history?.closeButtonBounds
: control === 'best'
? history?.bestButtonBounds
: control === 'loadBest'
? history?.loadBestButtonBounds
: history?.undoButtonBounds;
if (!scene || !canvas || !bounds) {
return {
ready: false,
control,
historyOpen: history?.open ?? false,
loadUndoAvailable: history?.loadUndoAvailable ?? 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,
historyOpen: history?.open ?? false,
loadUndoAvailable: history?.loadUndoAvailable ?? false,
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 Phaser camp formation-history ${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');
@@ -2249,6 +2696,7 @@ async function seedCampaignSave(page, options) {
createdAt: now
};
delete report.sortiePerformance;
delete report.sortieReview;
const settlement = {
battleId: report.battleId,
battleTitle: report.battleTitle,