fix: paginate sortie portrait roster
This commit is contained in:
@@ -3411,11 +3411,11 @@ try {
|
||||
);
|
||||
assert(
|
||||
midBriefingState?.sortiePortraitRosterView?.enabled === true &&
|
||||
midBriefingState?.sortiePortraitRosterView?.total > 8 &&
|
||||
midBriefingState?.sortiePortraitRosterView?.visibleCount === 8 &&
|
||||
midBriefingState?.sortiePortraitRosterView?.total === 23 &&
|
||||
midBriefingState?.sortiePortraitRosterView?.visibleCount === 6 &&
|
||||
midBriefingState?.sortiePortraitRosterView?.start === 1 &&
|
||||
midBriefingState?.sortiePortraitRosterView?.end === 8,
|
||||
`Expected the mid-campaign staged formation to expose an eight-card portrait roster window: ${JSON.stringify(midBriefingState?.sortiePortraitRosterView)}`
|
||||
midBriefingState?.sortiePortraitRosterView?.end === 6,
|
||||
`Expected the mid-campaign staged formation to expose the first six-card portrait roster page: ${JSON.stringify(midBriefingState?.sortiePortraitRosterView)}`
|
||||
);
|
||||
|
||||
await advanceSortiePrepStep(page, 'formation');
|
||||
@@ -3574,7 +3574,8 @@ try {
|
||||
preservedUnitId: midFormationSwapFixture.preservedUnitId
|
||||
})}`
|
||||
);
|
||||
await page.mouse.click(210, 447);
|
||||
const maDaiCardPoint = await readSortiePortraitRosterCardPoint(page, 'ma-dai');
|
||||
await page.mouse.click(maDaiCardPoint.x, maDaiCardPoint.y);
|
||||
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortieFocusedUnitId === 'ma-dai', undefined, { timeout: 30000 });
|
||||
const swapFocusState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
assert(
|
||||
@@ -3583,7 +3584,8 @@ try {
|
||||
`Expected focusing Ma Dai to preserve the actual sortie roster: ${JSON.stringify(swapFocusState?.selectedSortieUnitIds)}`
|
||||
);
|
||||
|
||||
await page.mouse.move(530, 544);
|
||||
const guanYuHoverPoint = await readSortiePortraitRosterCardPoint(page, 'guan-yu');
|
||||
await page.mouse.move(guanYuHoverPoint.x, guanYuHoverPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.sortieHoveredUnitId === 'guan-yu' && state?.sortieComparisonPreview?.source === 'hover-swap';
|
||||
@@ -3688,12 +3690,13 @@ try {
|
||||
`Expected pointerout to restore the visible Ma Dai focus preview: ${JSON.stringify(afterSwapPreviewProbe)}`
|
||||
);
|
||||
|
||||
await page.mouse.move(530, 544);
|
||||
const guanYuSwapPoint = await readSortiePortraitRosterCardPoint(page, 'guan-yu');
|
||||
await page.mouse.move(guanYuSwapPoint.x, guanYuSwapPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.sortieHoveredUnitId === 'guan-yu' && state?.sortieComparisonPreview?.source === 'hover-swap';
|
||||
}, undefined, { timeout: 30000 });
|
||||
await page.mouse.click(530, 544);
|
||||
await page.mouse.click(guanYuSwapPoint.x, guanYuSwapPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.sortiePinnedSwapCandidateUnitId === 'guan-yu' &&
|
||||
@@ -4127,19 +4130,121 @@ try {
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
|
||||
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
await page.mouse.click(658, 196);
|
||||
await page.waitForFunction((previousScroll) => {
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
return view?.scroll > previousScroll;
|
||||
}, midFormationFirstPage?.scroll ?? 0, { timeout: 30000 });
|
||||
const midFormationSecondPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
const midPortraitUndoRestoredPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
assert(
|
||||
midFormationSecondPage?.start > 1 && midFormationSecondPage?.end > 8,
|
||||
`Expected the portrait roster next-page control to reveal later officers: ${JSON.stringify(midFormationSecondPage)}`
|
||||
midPortraitUndoRestoredPage?.page === 1 &&
|
||||
midPortraitUndoRestoredPage.scroll === midPortraitUndoRestoredPage.pageSize &&
|
||||
midPortraitUndoRestoredPage.previousEnabled === true &&
|
||||
isFiniteBounds(midPortraitUndoRestoredPage.previousButtonBounds),
|
||||
`Expected swap undo to restore the second portrait page before the independent paging roundtrip: ${JSON.stringify(midPortraitUndoRestoredPage)}`
|
||||
);
|
||||
const midPortraitPagingResetPoint = await readSortiePortraitRosterNavigationPoint(page, 'previous');
|
||||
await page.mouse.click(midPortraitPagingResetPoint.x, midPortraitPagingResetPoint.y);
|
||||
await page.waitForFunction(() => {
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
return view?.page === 0 &&
|
||||
view.scroll === 0 &&
|
||||
view.previousEnabled === false &&
|
||||
view.visibleUnitIds?.length === 6 &&
|
||||
view.cardBounds?.length === 6;
|
||||
}, undefined, { timeout: 30000 });
|
||||
|
||||
const midPortraitPagingBaseline = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
view: state?.sortiePortraitRosterView ?? null,
|
||||
allUnitIds: state?.sortiePortraitRoster?.map((unit) => unit.id) ?? [],
|
||||
focusedUnitId: state?.sortieFocusedUnitId ?? null,
|
||||
selectedUnitIds: state?.selectedSortieUnitIds ?? [],
|
||||
campaignSelectedUnitIds: state?.campaign?.selectedSortieUnitIds ?? [],
|
||||
formationAssignments: state?.sortieFormationAssignments ?? {},
|
||||
itemAssignments: state?.sortieItemAssignments ?? {}
|
||||
};
|
||||
});
|
||||
const midPortraitPagingSaveBefore = await readCampaignSave(page);
|
||||
assert(
|
||||
midPortraitPagingBaseline.allUnitIds.length === 23 &&
|
||||
new Set(midPortraitPagingBaseline.allUnitIds).size === 23,
|
||||
`Expected the portrait paging fixture to contain exactly 23 unique campaign officers: ${JSON.stringify(midPortraitPagingBaseline)}`
|
||||
);
|
||||
|
||||
const midPortraitForwardUnitIds = [];
|
||||
let midPortraitCurrentView = midPortraitPagingBaseline.view;
|
||||
for (let expectedPage = 0; expectedPage < 4; expectedPage += 1) {
|
||||
const expectedVisibleUnitIds = midPortraitPagingBaseline.allUnitIds.slice(expectedPage * 6, expectedPage * 6 + 6);
|
||||
assertSortiePortraitRosterPage(midPortraitCurrentView, expectedPage, expectedVisibleUnitIds);
|
||||
midPortraitForwardUnitIds.push(...midPortraitCurrentView.visibleUnitIds);
|
||||
if (expectedPage < 3) {
|
||||
const nextPoint = await readSortiePortraitRosterNavigationPoint(page, 'next');
|
||||
await page.mouse.click(nextPoint.x, nextPoint.y);
|
||||
await page.waitForFunction(
|
||||
({ pageIndex, visibleUnitIds }) => {
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
return view?.page === pageIndex && JSON.stringify(view.visibleUnitIds) === JSON.stringify(visibleUnitIds);
|
||||
},
|
||||
{ pageIndex: expectedPage + 1, visibleUnitIds: midPortraitPagingBaseline.allUnitIds.slice((expectedPage + 1) * 6, (expectedPage + 2) * 6) },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
midPortraitCurrentView = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
}
|
||||
}
|
||||
assert(
|
||||
midPortraitForwardUnitIds.length === 23 &&
|
||||
new Set(midPortraitForwardUnitIds).size === 23 &&
|
||||
sameJsonValue(midPortraitForwardUnitIds, midPortraitPagingBaseline.allUnitIds) &&
|
||||
midPortraitCurrentView.visibleUnitIds.length === 5,
|
||||
`Expected four portrait pages to expose all 23 officers exactly once and finish with five officers: ${JSON.stringify({
|
||||
expected: midPortraitPagingBaseline.allUnitIds,
|
||||
actual: midPortraitForwardUnitIds,
|
||||
lastPage: midPortraitCurrentView
|
||||
})}`
|
||||
);
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-sortie-roster-last-page.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'mid camp sortie portrait roster last page');
|
||||
|
||||
for (let expectedPage = 2; expectedPage >= 0; expectedPage -= 1) {
|
||||
const previousPoint = await readSortiePortraitRosterNavigationPoint(page, 'previous');
|
||||
await page.mouse.click(previousPoint.x, previousPoint.y);
|
||||
const expectedVisibleUnitIds = midPortraitPagingBaseline.allUnitIds.slice(expectedPage * 6, expectedPage * 6 + 6);
|
||||
await page.waitForFunction(
|
||||
({ pageIndex, visibleUnitIds }) => {
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
return view?.page === pageIndex && JSON.stringify(view.visibleUnitIds) === JSON.stringify(visibleUnitIds);
|
||||
},
|
||||
{ pageIndex: expectedPage, visibleUnitIds: expectedVisibleUnitIds },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
midPortraitCurrentView = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||
assertSortiePortraitRosterPage(midPortraitCurrentView, expectedPage, expectedVisibleUnitIds);
|
||||
}
|
||||
|
||||
const midPortraitPagingAfter = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
focusedUnitId: state?.sortieFocusedUnitId ?? null,
|
||||
selectedUnitIds: state?.selectedSortieUnitIds ?? [],
|
||||
campaignSelectedUnitIds: state?.campaign?.selectedSortieUnitIds ?? [],
|
||||
formationAssignments: state?.sortieFormationAssignments ?? {},
|
||||
itemAssignments: state?.sortieItemAssignments ?? {}
|
||||
};
|
||||
});
|
||||
const midPortraitPagingSaveAfter = await readCampaignSave(page);
|
||||
assert(
|
||||
midPortraitCurrentView?.page === 0 &&
|
||||
midPortraitPagingAfter.focusedUnitId === midPortraitPagingBaseline.focusedUnitId &&
|
||||
sameJsonValue(midPortraitPagingAfter.selectedUnitIds, midPortraitPagingBaseline.selectedUnitIds) &&
|
||||
sameJsonValue(midPortraitPagingAfter.campaignSelectedUnitIds, midPortraitPagingBaseline.campaignSelectedUnitIds) &&
|
||||
sameJsonValue(midPortraitPagingAfter.formationAssignments, midPortraitPagingBaseline.formationAssignments) &&
|
||||
sameJsonValue(midPortraitPagingAfter.itemAssignments, midPortraitPagingBaseline.itemAssignments) &&
|
||||
sameJsonValue(midPortraitPagingSaveAfter, midPortraitPagingSaveBefore),
|
||||
`Expected portrait paging to return to page one without changing focus, selection, formation, supplies, or saves: ${JSON.stringify({
|
||||
before: midPortraitPagingBaseline,
|
||||
after: midPortraitPagingAfter,
|
||||
saveBefore: midPortraitPagingSaveBefore,
|
||||
saveAfter: midPortraitPagingSaveAfter
|
||||
})}`
|
||||
);
|
||||
await page.mouse.click(610, 196);
|
||||
await page.waitForFunction(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView?.scroll === 0, undefined, { timeout: 30000 });
|
||||
|
||||
const fullRosterCandidateProbe = await page.evaluate(() => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
@@ -5532,16 +5637,7 @@ async function assertFinalCampRosterPagination(page) {
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
|
||||
const previousPoint = await readCampRosterNavigationPoint(page, 'previous');
|
||||
await page.mouse.click(previousPoint.x, previousPoint.y);
|
||||
await page.waitForFunction(
|
||||
({ pageNumber, unitId }) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.campRoster?.page === pageNumber && state?.selectedUnitId === unitId;
|
||||
},
|
||||
{ pageNumber: 0, unitId: selectionTargetUnitId },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await navigateCampRosterPage(page, 'previous', 0, selectionTargetUnitId);
|
||||
const restorePoint = await readCampRosterRowPoint(page, initialSelectedUnitId);
|
||||
await page.mouse.click(restorePoint.x, restorePoint.y);
|
||||
await page.waitForFunction(
|
||||
@@ -5549,16 +5645,7 @@ async function assertFinalCampRosterPagination(page) {
|
||||
initialSelectedUnitId,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
const nextPoint = await readCampRosterNavigationPoint(page, 'next');
|
||||
await page.mouse.click(nextPoint.x, nextPoint.y);
|
||||
await page.waitForFunction(
|
||||
({ pageNumber, unitId }) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.campRoster?.page === pageNumber && state?.selectedUnitId === unitId;
|
||||
},
|
||||
{ pageNumber: expectedPage, unitId: initialSelectedUnitId },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await navigateCampRosterPage(page, 'next', expectedPage, initialSelectedUnitId);
|
||||
}
|
||||
|
||||
if (expectedPage === initialRoster.pageCount - 1) {
|
||||
@@ -5575,13 +5662,7 @@ async function assertFinalCampRosterPagination(page) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextPoint = await readCampRosterNavigationPoint(page, 'next');
|
||||
await page.mouse.click(nextPoint.x, nextPoint.y);
|
||||
await page.waitForFunction(
|
||||
(pageNumber) => window.__HEROS_DEBUG__?.camp()?.campRoster?.page === pageNumber,
|
||||
expectedPage + 1,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await navigateCampRosterPage(page, 'next', expectedPage + 1, initialSelectedUnitId);
|
||||
}
|
||||
|
||||
assert(
|
||||
@@ -5595,29 +5676,16 @@ async function assertFinalCampRosterPagination(page) {
|
||||
);
|
||||
|
||||
for (let expectedPage = initialRoster.pageCount - 2; expectedPage >= 0; expectedPage -= 1) {
|
||||
const previousPoint = await readCampRosterNavigationPoint(page, 'previous');
|
||||
await page.mouse.click(previousPoint.x, previousPoint.y);
|
||||
await page.waitForFunction(
|
||||
({ pageNumber, selectedUnitId }) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.campRoster?.page === pageNumber && state?.selectedUnitId === selectedUnitId;
|
||||
},
|
||||
{ pageNumber: expectedPage, selectedUnitId: initialSelectedUnitId },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await navigateCampRosterPage(page, 'previous', expectedPage, initialSelectedUnitId);
|
||||
}
|
||||
} catch (error) {
|
||||
verificationError = error;
|
||||
} finally {
|
||||
const currentPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.campRoster?.page ?? 0);
|
||||
const restoreState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
const currentPage = restoreState?.campRoster?.page ?? 0;
|
||||
const restoreSelectedUnitId = restoreState?.selectedUnitId ?? null;
|
||||
for (let pageNumber = currentPage; pageNumber > 0; pageNumber -= 1) {
|
||||
const previousPoint = await readCampRosterNavigationPoint(page, 'previous');
|
||||
await page.mouse.click(previousPoint.x, previousPoint.y);
|
||||
await page.waitForFunction(
|
||||
(expectedPage) => window.__HEROS_DEBUG__?.camp()?.campRoster?.page === expectedPage,
|
||||
pageNumber - 1,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
await navigateCampRosterPage(page, 'previous', pageNumber - 1, restoreSelectedUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5739,6 +5807,81 @@ async function readCampRosterNavigationPoint(page, direction) {
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function navigateCampRosterPage(page, direction, expectedPage, expectedSelectedUnitId) {
|
||||
const maxAttempts = 3;
|
||||
const attemptTimeout = 4000;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const before = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
page: state?.campRoster?.page ?? null,
|
||||
selectedUnitId: state?.selectedUnitId ?? null,
|
||||
campRoster: state?.campRoster ?? null
|
||||
};
|
||||
});
|
||||
if (before.page === expectedPage && before.selectedUnitId === expectedSelectedUnitId) {
|
||||
return before;
|
||||
}
|
||||
if (before.selectedUnitId !== expectedSelectedUnitId) {
|
||||
throw new Error(
|
||||
`Final-camp roster ${direction} navigation changed the selected detail before attempt ${attempt}: ${JSON.stringify({
|
||||
expectedPage,
|
||||
expectedSelectedUnitId,
|
||||
before
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const point = await readCampRosterNavigationPoint(page, direction);
|
||||
await page.mouse.click(point.x, point.y);
|
||||
await page.waitForFunction(
|
||||
({ pageNumber, selectedUnitId }) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return state?.campRoster?.page === pageNumber && state?.selectedUnitId === selectedUnitId;
|
||||
},
|
||||
{ pageNumber: expectedPage, selectedUnitId: expectedSelectedUnitId },
|
||||
{ timeout: attemptTimeout }
|
||||
);
|
||||
return await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
page: state?.campRoster?.page ?? null,
|
||||
selectedUnitId: state?.selectedUnitId ?? null,
|
||||
campRoster: state?.campRoster ?? null
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < maxAttempts) {
|
||||
await page.waitForTimeout(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalState = await page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
return {
|
||||
page: state?.campRoster?.page ?? null,
|
||||
selectedUnitId: state?.selectedUnitId ?? null,
|
||||
campRoster: state?.campRoster ?? null
|
||||
};
|
||||
});
|
||||
const lastErrorSummary = lastError instanceof Error
|
||||
? `${lastError.name}: ${lastError.message}`
|
||||
: String(lastError ?? 'unknown input failure');
|
||||
throw new Error(
|
||||
`Failed final-camp roster ${direction} navigation to page ${expectedPage + 1} after ${maxAttempts} attempts: ${JSON.stringify({
|
||||
expectedPage,
|
||||
expectedSelectedUnitId,
|
||||
finalState,
|
||||
lastError: lastErrorSummary
|
||||
})}`
|
||||
);
|
||||
}
|
||||
|
||||
async function readCampRosterRowPoint(page, unitId) {
|
||||
const probe = await page.evaluate((targetUnitId) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
@@ -6284,6 +6427,149 @@ async function readSortiePresetControlPoint(page, control, presetId) {
|
||||
return probe;
|
||||
}
|
||||
|
||||
function assertSortiePortraitRosterPage(view, expectedPage, expectedVisibleUnitIds) {
|
||||
const expectedStart = expectedVisibleUnitIds.length > 0 ? expectedPage * 6 + 1 : 0;
|
||||
const expectedEnd = expectedPage * 6 + expectedVisibleUnitIds.length;
|
||||
const hdBounds = { x: 0, y: 0, width: 1280, height: 720 };
|
||||
assert(
|
||||
view?.enabled === true &&
|
||||
view.total === 23 &&
|
||||
view.page === expectedPage &&
|
||||
view.pageCount === 4 &&
|
||||
view.pageSize === 6 &&
|
||||
view.visibleCount === expectedVisibleUnitIds.length &&
|
||||
view.start === expectedStart &&
|
||||
view.end === expectedEnd &&
|
||||
sameJsonValue(view.visibleUnitIds, expectedVisibleUnitIds) &&
|
||||
view.previousEnabled === (expectedPage > 0) &&
|
||||
view.nextEnabled === (expectedPage < 3) &&
|
||||
isFiniteBounds(view.previousButtonBounds) &&
|
||||
isFiniteBounds(view.nextButtonBounds) &&
|
||||
boundsInside(view.previousButtonBounds, hdBounds) &&
|
||||
boundsInside(view.nextButtonBounds, hdBounds) &&
|
||||
view.cardBounds?.length === expectedVisibleUnitIds.length,
|
||||
`Expected exact six-officer portrait page ${expectedPage + 1}/4 metadata and navigation state: ${JSON.stringify({
|
||||
expectedVisibleUnitIds,
|
||||
view
|
||||
})}`
|
||||
);
|
||||
view.cardBounds.forEach((bounds, index) => {
|
||||
assert(
|
||||
bounds.unitId === expectedVisibleUnitIds[index] &&
|
||||
isFiniteBounds(bounds) &&
|
||||
boundsInside(bounds, hdBounds),
|
||||
`Expected portrait card ${index + 1} on page ${expectedPage + 1} to match its visible officer inside the HD canvas: ${JSON.stringify({
|
||||
bounds,
|
||||
expectedUnitId: expectedVisibleUnitIds[index]
|
||||
})}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function readSortiePortraitRosterNavigationPoint(page, direction) {
|
||||
const probe = await page.evaluate((navigationDirection) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const canvas = document.querySelector('canvas');
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
const bounds = navigationDirection === 'previous' ? view?.previousButtonBounds : view?.nextButtonBounds;
|
||||
const enabled = navigationDirection === 'previous' ? view?.previousEnabled : view?.nextEnabled;
|
||||
if (!scene || !canvas || !bounds || !enabled) {
|
||||
return { ready: false, direction: navigationDirection, enabled: enabled ?? false, bounds: bounds ?? null, view };
|
||||
}
|
||||
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
ready: true,
|
||||
direction: navigationDirection,
|
||||
enabled,
|
||||
bounds,
|
||||
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, direction);
|
||||
assert(
|
||||
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
|
||||
`Expected an enabled portrait roster ${direction} control with a canvas-scaled click point: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readSortiePortraitRosterCardPoint(page, unitId) {
|
||||
const target = await page.evaluate((targetUnitId) => {
|
||||
const state = window.__HEROS_DEBUG__?.camp();
|
||||
const view = state?.sortiePortraitRosterView;
|
||||
const unitIndex = state?.sortiePortraitRoster?.findIndex((unit) => unit.id === targetUnitId) ?? -1;
|
||||
return {
|
||||
ready: Boolean(view?.enabled && unitIndex >= 0 && view.pageSize > 0),
|
||||
unitId: targetUnitId,
|
||||
unitIndex,
|
||||
currentPage: view?.page ?? null,
|
||||
targetPage: unitIndex >= 0 && view?.pageSize > 0 ? Math.floor(unitIndex / view.pageSize) : null,
|
||||
pageCount: view?.pageCount ?? null
|
||||
};
|
||||
}, unitId);
|
||||
assert(
|
||||
target.ready === true &&
|
||||
Number.isInteger(target.currentPage) &&
|
||||
Number.isInteger(target.targetPage) &&
|
||||
target.targetPage >= 0 &&
|
||||
target.targetPage < target.pageCount,
|
||||
`Expected ${unitId} to belong to a valid portrait roster page: ${JSON.stringify(target)}`
|
||||
);
|
||||
|
||||
let currentPage = target.currentPage;
|
||||
while (currentPage !== target.targetPage) {
|
||||
const direction = currentPage < target.targetPage ? 'next' : 'previous';
|
||||
const navigationPoint = await readSortiePortraitRosterNavigationPoint(page, direction);
|
||||
await page.mouse.click(navigationPoint.x, navigationPoint.y);
|
||||
const expectedPage = currentPage + (direction === 'next' ? 1 : -1);
|
||||
await page.waitForFunction(
|
||||
(pageIndex) => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView?.page === pageIndex,
|
||||
expectedPage,
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
currentPage = expectedPage;
|
||||
}
|
||||
await page.waitForFunction(
|
||||
({ pageIndex, targetUnitId }) => {
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
return view?.page === pageIndex &&
|
||||
view.visibleUnitIds?.includes(targetUnitId) &&
|
||||
view.cardBounds?.some((card) => card.unitId === targetUnitId);
|
||||
},
|
||||
{ pageIndex: target.targetPage, targetUnitId: unitId },
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
|
||||
const probe = await page.evaluate((targetUnitId) => {
|
||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||
const canvas = document.querySelector('canvas');
|
||||
const view = window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView;
|
||||
const bounds = view?.cardBounds?.find((card) => card.unitId === targetUnitId);
|
||||
if (!scene || !canvas || !bounds) {
|
||||
return { ready: false, unitId: targetUnitId, bounds: bounds ?? null, view };
|
||||
}
|
||||
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
return {
|
||||
ready: true,
|
||||
unitId: targetUnitId,
|
||||
bounds,
|
||||
x: canvasBounds.left + centerX * canvasBounds.width / scene.scale.width,
|
||||
y: canvasBounds.top + centerY * canvasBounds.height / scene.scale.height
|
||||
};
|
||||
}, unitId);
|
||||
assert(
|
||||
probe.ready === true && Number.isFinite(probe.x) && Number.isFinite(probe.y),
|
||||
`Expected a visible portrait roster card for ${unitId} with a canvas-scaled pointer point: ${JSON.stringify(probe)}`
|
||||
);
|
||||
return probe;
|
||||
}
|
||||
|
||||
async function readBattleDoctrineProbe(page) {
|
||||
return page.evaluate(() => {
|
||||
const state = window.__HEROS_DEBUG__?.battle();
|
||||
|
||||
@@ -240,6 +240,19 @@ type CampRosterLayout = {
|
||||
nextEnabled: boolean;
|
||||
};
|
||||
|
||||
type SortiePortraitRosterLayout = {
|
||||
page: number;
|
||||
pageCount: number;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
visibleUnitIds: string[];
|
||||
previousButtonBounds: CampRosterBounds;
|
||||
nextButtonBounds: CampRosterBounds;
|
||||
cardBounds: Array<CampRosterBounds & { unitId: string }>;
|
||||
previousEnabled: boolean;
|
||||
nextEnabled: boolean;
|
||||
};
|
||||
|
||||
type CampSupplyDefinition = {
|
||||
label: string;
|
||||
usableId: string;
|
||||
@@ -475,6 +488,7 @@ type SortieConfigurationSnapshot = {
|
||||
focusedUnitId: string;
|
||||
planFeedback: string;
|
||||
rosterScroll: number;
|
||||
portraitRosterPage: number;
|
||||
};
|
||||
|
||||
type SortieSwapProjection = {
|
||||
@@ -907,6 +921,7 @@ const sortiePresetDefinitions: {
|
||||
];
|
||||
|
||||
const reportFormationHistoryPageSize = 7;
|
||||
const sortiePortraitRosterPageSize = 6;
|
||||
|
||||
const defaultSortieRule: SortieRuleDefinition = {
|
||||
maxUnits: maxSortieUnits,
|
||||
@@ -11091,6 +11106,8 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortieCoreResonanceToggleButton?: Phaser.GameObjects.Rectangle;
|
||||
private sortieCoreResonanceToggleLabel?: Phaser.GameObjects.Text;
|
||||
private sortieRosterScroll = 0;
|
||||
private sortiePortraitRosterPage = 0;
|
||||
private sortiePortraitRosterLayout?: SortiePortraitRosterLayout;
|
||||
private sortiePlanFeedback = '';
|
||||
private sortiePrepStep: SortiePrepStep = 'briefing';
|
||||
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
|
||||
@@ -11118,6 +11135,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieCoreResonanceSelectorOpen = false;
|
||||
this.sortieCoreResonanceToggleButton = undefined;
|
||||
this.sortieCoreResonanceToggleLabel = undefined;
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
this.sortiePortraitRosterLayout = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.sortieFormationPanelMode = 'roster';
|
||||
@@ -12717,6 +12737,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const usesStagedPrep = this.usesStagedSortiePrep(flow);
|
||||
if (usesStagedPrep && !wasVisible) {
|
||||
this.sortiePrepStep = 'briefing';
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
}
|
||||
const checklist = this.sortieChecklist();
|
||||
const prepTitle = isBattleSortie ? '출진 준비' : '군영 의정';
|
||||
@@ -12856,6 +12877,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieFormationPanelMode = 'order';
|
||||
}
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
@@ -13052,31 +13074,35 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
const rosterUnits = this.sortieRosterUnits();
|
||||
const displayUnits = this.sortieRosterDisplayUnits(rosterUnits);
|
||||
const visibleCount = 8;
|
||||
const maxScroll = Math.max(0, displayUnits.length - visibleCount);
|
||||
this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll);
|
||||
const visibleUnits = displayUnits.slice(this.sortieRosterScroll, this.sortieRosterScroll + visibleCount);
|
||||
const pageCount = Math.max(1, Math.ceil(displayUnits.length / sortiePortraitRosterPageSize));
|
||||
this.sortiePortraitRosterPage = Phaser.Math.Clamp(this.sortiePortraitRosterPage, 0, pageCount - 1);
|
||||
const pageStart = this.sortiePortraitRosterPage * sortiePortraitRosterPageSize;
|
||||
const visibleUnits = displayUnits.slice(pageStart, pageStart + sortiePortraitRosterPageSize);
|
||||
const selectedCount = this.selectedSortieUnitIds.length;
|
||||
const maxUnits = this.sortieMaxUnits();
|
||||
const rangeEnd = this.sortieRosterScroll + visibleUnits.length;
|
||||
const rangeEnd = pageStart + visibleUnits.length;
|
||||
const previousEnabled = this.sortiePortraitRosterPage > 0;
|
||||
const nextEnabled = this.sortiePortraitRosterPage < pageCount - 1;
|
||||
const previousButtonBounds = { x: x + width - 100, y: y + 9, width: 40, height: 30 };
|
||||
const nextButtonBounds = { x: x + width - 52, y: y + 9, width: 40, height: 30 };
|
||||
this.trackSortie(this.add.text(x + 18, y + 13, `무장 선택 · 출전 ${selectedCount}/${maxUnits}`, this.textStyle(19, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
const rangeText = this.trackSortie(
|
||||
this.add.text(
|
||||
x + width - 116,
|
||||
y + 18,
|
||||
`${displayUnits.length > 0 ? this.sortieRosterScroll + 1 : 0}-${rangeEnd}/${displayUnits.length}`,
|
||||
`${displayUnits.length > 0 ? pageStart + 1 : 0}-${rangeEnd}/${displayUnits.length} · ${this.sortiePortraitRosterPage + 1}/${pageCount}`,
|
||||
this.textStyle(11, '#9fb0bf', true)
|
||||
)
|
||||
);
|
||||
rangeText.setOrigin(1, 0);
|
||||
rangeText.setDepth(depth + 1);
|
||||
this.renderFirstSortieInlineButton('‹', x + width - 100, y + 9, 40, 30, this.sortieRosterScroll > 0, false, () => {
|
||||
this.renderFirstSortieInlineButton('‹', previousButtonBounds.x, previousButtonBounds.y, previousButtonBounds.width, previousButtonBounds.height, previousEnabled, false, () => {
|
||||
soundDirector.playSelect();
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll - visibleCount, maxScroll);
|
||||
this.setSortiePortraitRosterPage(this.sortiePortraitRosterPage - 1, pageCount);
|
||||
}, depth + 1);
|
||||
this.renderFirstSortieInlineButton('›', x + width - 52, y + 9, 40, 30, this.sortieRosterScroll < maxScroll, false, () => {
|
||||
this.renderFirstSortieInlineButton('›', nextButtonBounds.x, nextButtonBounds.y, nextButtonBounds.width, nextButtonBounds.height, nextEnabled, false, () => {
|
||||
soundDirector.playSelect();
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll + visibleCount, maxScroll);
|
||||
this.setSortiePortraitRosterPage(this.sortiePortraitRosterPage + 1, pageCount);
|
||||
}, depth + 1);
|
||||
|
||||
const handleRosterWheel = (
|
||||
@@ -13088,26 +13114,28 @@ export class CampScene extends Phaser.Scene {
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
const direction = Math.sign(deltaY);
|
||||
if (direction === 0 || maxScroll <= 0) {
|
||||
if (direction === 0 || pageCount <= 1) {
|
||||
return;
|
||||
}
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll + direction * 2, maxScroll);
|
||||
this.setSortiePortraitRosterPage(this.sortiePortraitRosterPage + direction, pageCount);
|
||||
};
|
||||
if (maxScroll > 0) {
|
||||
if (pageCount > 1) {
|
||||
bg.setInteractive({ useHandCursor: false });
|
||||
bg.on('wheel', handleRosterWheel);
|
||||
}
|
||||
|
||||
const columnGap = 10;
|
||||
const rowGap = 8;
|
||||
const rowGap = 10;
|
||||
const cardWidth = Math.floor((width - 36 - columnGap) / 2);
|
||||
const cardTop = y + 50;
|
||||
const cardHeight = Math.floor((height - 64 - rowGap * 3) / 4);
|
||||
const cardHeight = Math.floor((height - 64 - rowGap * 2) / 3);
|
||||
const cardBounds: SortiePortraitRosterLayout['cardBounds'] = [];
|
||||
visibleUnits.forEach((unit, index) => {
|
||||
const column = index % 2;
|
||||
const rowIndex = Math.floor(index / 2);
|
||||
const cardX = x + 18 + column * (cardWidth + columnGap);
|
||||
const cardY = cardTop + rowIndex * (cardHeight + rowGap);
|
||||
cardBounds.push({ unitId: unit.id, x: cardX, y: cardY, width: cardWidth, height: cardHeight });
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const focused = this.sortieFocusedUnitId === unit.id;
|
||||
const pinnedSwapCandidate = this.sortiePinnedSwapCandidateUnitId === unit.id;
|
||||
@@ -13159,9 +13187,9 @@ export class CampScene extends Phaser.Scene {
|
||||
accent.setOrigin(0);
|
||||
accent.setDepth(depth + 2);
|
||||
const portraitView = this.renderFirstSortiePortrait(
|
||||
cardX + 39,
|
||||
cardX + 45,
|
||||
cardY + cardHeight / 2,
|
||||
56,
|
||||
70,
|
||||
unit,
|
||||
depth + 2,
|
||||
availability.available ? 1 : 0.42,
|
||||
@@ -13169,7 +13197,7 @@ export class CampScene extends Phaser.Scene {
|
||||
accentColor
|
||||
);
|
||||
if (pinnedSwapCandidate && portraitView.image) {
|
||||
portraitView.image.setDisplaySize(60, 60);
|
||||
portraitView.image.setDisplaySize(74, 74);
|
||||
}
|
||||
card.on('pointerover', () => {
|
||||
this.sortieHoveredUnitId = unit.id;
|
||||
@@ -13179,7 +13207,7 @@ export class CampScene extends Phaser.Scene {
|
||||
accent.setFillStyle(palette.gold, 0.98);
|
||||
portraitView.frame.setStrokeStyle(2, palette.gold, 0.98);
|
||||
if (availability.available && portraitView.image) {
|
||||
portraitView.image.setDisplaySize(60, 60);
|
||||
portraitView.image.setDisplaySize(74, 74);
|
||||
}
|
||||
});
|
||||
card.on('pointerout', () => {
|
||||
@@ -13192,22 +13220,22 @@ export class CampScene extends Phaser.Scene {
|
||||
card.setStrokeStyle(2, palette.gold, 0.98);
|
||||
accent.setFillStyle(palette.gold, 0.98);
|
||||
portraitView.frame.setStrokeStyle(2, palette.gold, 0.98);
|
||||
portraitView.image?.setDisplaySize(60, 60);
|
||||
portraitView.image?.setDisplaySize(74, 74);
|
||||
return;
|
||||
}
|
||||
card.setFillStyle(fill, !availability.available ? 0.62 : 0.96);
|
||||
card.setStrokeStyle(restingStrokeWidth, availability.available ? accentColor : 0x53606c, restingStrokeAlpha);
|
||||
accent.setFillStyle(accentColor, focused || selected ? 0.94 : recommendation ? 0.72 : 0.34);
|
||||
portraitView.frame.setStrokeStyle(2, accentColor, focused ? 0.92 : selected ? 0.78 : recommendation ? 0.62 : 0.42);
|
||||
portraitView.image?.setDisplaySize(56, 56);
|
||||
portraitView.image?.setDisplaySize(70, 70);
|
||||
});
|
||||
const statusLabel = required ? '필수' : selected ? '출전' : pinnedSwapCandidate ? '교체안' : recommendation ? '추천' : '대기';
|
||||
const statusColor = required || recommendation || pinnedSwapCandidate ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf';
|
||||
const status = this.trackSortie(this.add.text(cardX + cardWidth - 10, cardY + 8, statusLabel, this.textStyle(9, statusColor, true)));
|
||||
const status = this.trackSortie(this.add.text(cardX + cardWidth - 10, cardY + 9, statusLabel, this.textStyle(10, statusColor, true)));
|
||||
status.setOrigin(1, 0);
|
||||
status.setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 75, cardY + 8, `${unit.name} Lv${unit.level}`, this.textStyle(14, availability.available ? '#f2e3bf' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 75, cardY + 28, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(9, availability.available ? '#c8d2dd' : '#68727e', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 92, cardY + 9, `${unit.name} Lv${unit.level}`, this.textStyle(15, availability.available ? '#f2e3bf' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 92, cardY + 33, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(11, availability.available ? '#c8d2dd' : '#68727e', true))).setDepth(depth + 2);
|
||||
const previewColor = preview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: preview.mode === 'add' || preview.mode === 'current'
|
||||
@@ -13217,20 +13245,20 @@ export class CampScene extends Phaser.Scene {
|
||||
: '#87919c';
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
cardX + 75,
|
||||
cardY + 45,
|
||||
this.compactText(`${this.sortieFormationRoleLabel(role)} · ${preview.compactLabel}`, 28),
|
||||
this.textStyle(9, availability.available ? previewColor : '#68727e', selected || Boolean(recommendation))
|
||||
cardX + 92,
|
||||
cardY + 55,
|
||||
this.compactText(`${this.sortieFormationRoleLabel(role)} · ${preview.compactLabel}`, 24),
|
||||
this.textStyle(10, availability.available ? previewColor : '#68727e', selected || Boolean(recommendation))
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
|
||||
const canToggle = availability.available && !(selected && required);
|
||||
this.renderFirstSortieInlineButton(
|
||||
required ? '필수 출전' : selected ? '출전 해제' : pinnedSwapCandidate ? '후보 고정' : canPinSwap ? '교체 비교' : '출전 등록',
|
||||
cardX + cardWidth - 80,
|
||||
cardY + cardHeight - 25,
|
||||
70,
|
||||
20,
|
||||
cardX + cardWidth - 94,
|
||||
cardY + cardHeight - 32,
|
||||
84,
|
||||
24,
|
||||
canToggle,
|
||||
selected || pinnedSwapCandidate,
|
||||
() => {
|
||||
@@ -13244,6 +13272,19 @@ export class CampScene extends Phaser.Scene {
|
||||
depth + 3
|
||||
);
|
||||
});
|
||||
|
||||
this.sortiePortraitRosterLayout = {
|
||||
page: this.sortiePortraitRosterPage,
|
||||
pageCount,
|
||||
pageSize: sortiePortraitRosterPageSize,
|
||||
totalCount: displayUnits.length,
|
||||
visibleUnitIds: visibleUnits.map((unit) => unit.id),
|
||||
previousButtonBounds,
|
||||
nextButtonBounds,
|
||||
cardBounds,
|
||||
previousEnabled,
|
||||
nextEnabled
|
||||
};
|
||||
}
|
||||
|
||||
private renderCampaignSortieOrderBrowser(x: number, y: number, width: number, height: number, depth: number) {
|
||||
@@ -14705,6 +14746,15 @@ export class CampScene extends Phaser.Scene {
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private setSortiePortraitRosterPage(nextPage: number, pageCount: number) {
|
||||
const clamped = Phaser.Math.Clamp(Math.round(nextPage), 0, Math.max(0, pageCount - 1));
|
||||
if (clamped === this.sortiePortraitRosterPage) {
|
||||
return;
|
||||
}
|
||||
this.sortiePortraitRosterPage = clamped;
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private sortieRosterViewState(totalUnits: number, height = 286) {
|
||||
const denseRoster = totalUnits > 12;
|
||||
const rowGap = denseRoster ? 22 : totalUnits > 7 ? 30 : totalUnits > 5 ? 38 : totalUnits > 4 ? 43 : 48;
|
||||
@@ -15959,6 +16009,7 @@ export class CampScene extends Phaser.Scene {
|
||||
? this.sortieFocusedUnitId
|
||||
: projection.selectedUnitIds[0];
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
const definition = this.sortiePresetDefinition(presetId);
|
||||
this.sortiePlanFeedback = `${definition.label} 편성책 적용 완료 · 장비·보급 재점검`;
|
||||
this.persistSortieSelection();
|
||||
@@ -15989,6 +16040,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieFocusedUnitId = undo.before.focusedUnitId;
|
||||
this.sortiePlanFeedback = undo.before.planFeedback;
|
||||
this.sortieRosterScroll = undo.before.rosterScroll;
|
||||
this.sortiePortraitRosterPage = undo.before.portraitRosterPage;
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
const definition = this.sortiePresetDefinition(undo.presetId);
|
||||
@@ -16077,7 +16129,8 @@ export class CampScene extends Phaser.Scene {
|
||||
itemAssignments: this.cloneSortieItemAssignments(this.sortieItemAssignments),
|
||||
focusedUnitId: this.sortieFocusedUnitId,
|
||||
planFeedback: this.sortiePlanFeedback,
|
||||
rosterScroll: this.sortieRosterScroll
|
||||
rosterScroll: this.sortieRosterScroll,
|
||||
portraitRosterPage: this.sortiePortraitRosterPage
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16146,6 +16199,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
this.sortiePlanFeedback = `${projection.outgoingUnit.name} → ${projection.incomingUnit.name} 교체 완료 · 장비·보급 재점검`;
|
||||
this.persistSortieSelection();
|
||||
this.sortieSwapUndoState = {
|
||||
@@ -16178,6 +16232,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieFocusedUnitId = undo.before.focusedUnitId;
|
||||
this.sortiePlanFeedback = undo.before.planFeedback;
|
||||
this.sortieRosterScroll = undo.before.rosterScroll;
|
||||
this.sortiePortraitRosterPage = undo.before.portraitRosterPage;
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.persistSortieSelection();
|
||||
@@ -17414,6 +17469,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private hideSortiePrep(clearPinnedSwap = true) {
|
||||
this.sortieObjects.forEach((object) => object.destroy());
|
||||
this.sortieObjects = [];
|
||||
this.sortiePortraitRosterLayout = undefined;
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
if (clearPinnedSwap) {
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
@@ -18991,6 +19047,7 @@ export class CampScene extends Phaser.Scene {
|
||||
? this.sortieFocusedUnitId
|
||||
: projection.selectedUnitIds[0];
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
this.sortiePlanFeedback = `${bestRecord.battleTitle} 최고 편성 적용 · 장비·보급 재점검`;
|
||||
this.persistSortieSelection();
|
||||
this.reportFormationHistoryUndoState = {
|
||||
@@ -19025,6 +19082,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieFocusedUnitId = undo.before.focusedUnitId;
|
||||
this.sortiePlanFeedback = undo.before.planFeedback;
|
||||
this.sortieRosterScroll = undo.before.rosterScroll;
|
||||
this.sortiePortraitRosterPage = undo.before.portraitRosterPage;
|
||||
this.persistSortieSelection();
|
||||
this.reportFormationHistoryFeedback = '최고 편성 적용을 되돌리고 이전 명단·역할·보급으로 복원했습니다.';
|
||||
soundDirector.playSelect();
|
||||
@@ -20408,6 +20466,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(Array.from(selected));
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
const selectionLabel = hasBattle ? '출전' : '동행';
|
||||
const synergyResultLine = synergyPreview.headline.replace(/^(합류|해제) 시 /, '');
|
||||
this.sortiePlanFeedback = `${unit.name} ${selected.has(unitId) ? `${selectionLabel} 추가` : `${selectionLabel} 해제`} · ${synergyResultLine}`;
|
||||
@@ -20714,9 +20773,14 @@ export class CampScene extends Phaser.Scene {
|
||||
const sortieStepIndex = firstSortiePrepSteps.findIndex((step) => step.id === this.sortiePrepStep);
|
||||
const nextSortiePrepStep = stagedSortiePrep ? firstSortiePrepSteps[sortieStepIndex + 1]?.id ?? null : null;
|
||||
const portraitRosterUnits = this.sortieRosterDisplayUnits(this.sortieRosterUnits());
|
||||
const portraitRosterVisibleCount = 8;
|
||||
const portraitRosterMaxScroll = Math.max(0, portraitRosterUnits.length - portraitRosterVisibleCount);
|
||||
const portraitRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, portraitRosterMaxScroll);
|
||||
const portraitRosterPageCount = Math.max(1, Math.ceil(portraitRosterUnits.length / sortiePortraitRosterPageSize));
|
||||
const portraitRosterPage = Phaser.Math.Clamp(this.sortiePortraitRosterPage, 0, portraitRosterPageCount - 1);
|
||||
const portraitRosterScroll = portraitRosterPage * sortiePortraitRosterPageSize;
|
||||
const portraitRosterVisibleUnits = portraitRosterUnits.slice(
|
||||
portraitRosterScroll,
|
||||
portraitRosterScroll + sortiePortraitRosterPageSize
|
||||
);
|
||||
const portraitRosterMaxScroll = Math.max(0, (portraitRosterPageCount - 1) * sortiePortraitRosterPageSize);
|
||||
const sortieComparisonPreview = this.sortieFormationComparisonPreview();
|
||||
const sortieComparisonAction = this.sortieComparisonAction(sortieComparisonPreview);
|
||||
const sortiePursuit = this.sortiePursuitPairs();
|
||||
@@ -21058,11 +21122,24 @@ export class CampScene extends Phaser.Scene {
|
||||
sortiePortraitRosterView: {
|
||||
enabled: stagedSortiePrep && !this.isFirstSortiePrepSlice(),
|
||||
total: portraitRosterUnits.length,
|
||||
visibleCount: Math.min(portraitRosterVisibleCount, portraitRosterUnits.length),
|
||||
visibleCount: portraitRosterVisibleUnits.length,
|
||||
start: portraitRosterUnits.length > 0 ? portraitRosterScroll + 1 : 0,
|
||||
end: portraitRosterScroll + Math.min(portraitRosterVisibleCount, portraitRosterUnits.length - portraitRosterScroll),
|
||||
end: portraitRosterScroll + portraitRosterVisibleUnits.length,
|
||||
maxScroll: portraitRosterMaxScroll,
|
||||
scroll: portraitRosterScroll
|
||||
scroll: portraitRosterScroll,
|
||||
page: portraitRosterPage,
|
||||
pageCount: portraitRosterPageCount,
|
||||
pageSize: sortiePortraitRosterPageSize,
|
||||
visibleUnitIds: portraitRosterVisibleUnits.map((unit) => unit.id),
|
||||
previousEnabled: portraitRosterPage > 0,
|
||||
nextEnabled: portraitRosterPage < portraitRosterPageCount - 1,
|
||||
previousButtonBounds: this.sortiePortraitRosterLayout
|
||||
? { ...this.sortiePortraitRosterLayout.previousButtonBounds }
|
||||
: null,
|
||||
nextButtonBounds: this.sortiePortraitRosterLayout
|
||||
? { ...this.sortiePortraitRosterLayout.nextButtonBounds }
|
||||
: null,
|
||||
cardBounds: this.sortiePortraitRosterLayout?.cardBounds.map((bounds) => ({ ...bounds })) ?? []
|
||||
},
|
||||
sortiePortraitRoster: portraitRosterUnits.map((unit) => {
|
||||
const portraitKey = campaignPortraitKeysByUnitId[unit.id];
|
||||
|
||||
Reference in New Issue
Block a user