feat: reopen formation reviews in camp
This commit is contained in:
@@ -217,6 +217,135 @@ try {
|
|||||||
`Expected campaign report debug state to retain first battle unlock reward: ${JSON.stringify(firstCampProbe.state?.report?.campaignRewards)}`
|
`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 firstCampSelectedSortieUnitIds = firstCampProbe.state?.selectedSortieUnitIds ?? [];
|
||||||
const firstCampDeploymentPreview = firstCampProbe.state?.sortieDeploymentPreview ?? [];
|
const firstCampDeploymentPreview = firstCampProbe.state?.sortieDeploymentPreview ?? [];
|
||||||
await page.mouse.click(1160, 38);
|
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);
|
const midFormationFirstPage = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()?.sortiePortraitRosterView);
|
||||||
await page.mouse.click(658, 196);
|
await page.mouse.click(658, 196);
|
||||||
await page.waitForFunction((previousScroll) => {
|
await page.waitForFunction((previousScroll) => {
|
||||||
@@ -1363,6 +1530,146 @@ try {
|
|||||||
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-formation-evaluation-updated.png`, fullPage: true });
|
await page.screenshot({ path: `${screenshotDir}/rc-mid-camp-formation-evaluation-updated.png`, fullPage: true });
|
||||||
await assertCanvasPainted(page, 'mid camp formation evaluation update');
|
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, {
|
await seedCampaignSave(page, {
|
||||||
battleId: 'sixty-sixth-battle-wuzhang-final',
|
battleId: 'sixty-sixth-battle-wuzhang-final',
|
||||||
battleTitle: '오장원 최종전',
|
battleTitle: '오장원 최종전',
|
||||||
@@ -1381,6 +1688,13 @@ try {
|
|||||||
assert(finalCampState.sortieHasBattle === false, `Expected final camp to expose no playable sortie target: ${JSON.stringify(finalCampState)}`);
|
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.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.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(
|
assert(
|
||||||
Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0,
|
Array.isArray(finalCampState.sortieDeploymentPreview) && finalCampState.sortieDeploymentPreview.length === 0,
|
||||||
`Expected final camp deployment preview to stay empty: ${JSON.stringify(finalCampState?.sortieDeploymentPreview)}`
|
`Expected final camp deployment preview to stay empty: ${JSON.stringify(finalCampState?.sortieDeploymentPreview)}`
|
||||||
@@ -1723,6 +2037,80 @@ async function readBattleResultEvaluationControlPoint(page, control, presetId) {
|
|||||||
return probe;
|
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) {
|
async function readSortieComparisonActionPoint(page) {
|
||||||
const probe = await page.evaluate(() => {
|
const probe = await page.evaluate(() => {
|
||||||
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
const scene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||||
|
|||||||
195
src/game/data/sortiePerformanceReview.ts
Normal file
195
src/game/data/sortiePerformanceReview.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { coreSortieSynergyRoles } from './sortieSynergy';
|
||||||
|
import type {
|
||||||
|
CampaignSortiePerformanceSnapshot,
|
||||||
|
CampaignSortiePreset
|
||||||
|
} from '../state/campaignState';
|
||||||
|
|
||||||
|
export type SortiePerformanceGrade = 'S' | 'A' | 'B' | 'C' | 'D';
|
||||||
|
|
||||||
|
export type SortiePerformanceContributionTotals = {
|
||||||
|
bonusDamage: number;
|
||||||
|
preventedDamage: number;
|
||||||
|
bonusHealing: number;
|
||||||
|
extendedBondTriggers: number;
|
||||||
|
counterplays: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortiePerformanceEndUnit = {
|
||||||
|
id: string;
|
||||||
|
hp: number;
|
||||||
|
maxHp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SortiePerformanceReview = {
|
||||||
|
score: number;
|
||||||
|
grade: SortiePerformanceGrade;
|
||||||
|
gradeColor: number;
|
||||||
|
gradeTextColor: string;
|
||||||
|
headline: string;
|
||||||
|
strengths: string[];
|
||||||
|
improvement: string;
|
||||||
|
objectiveAchieved: number;
|
||||||
|
objectiveTotal: number;
|
||||||
|
survivorCount: number;
|
||||||
|
deployedCount: number;
|
||||||
|
recoveryNeededCount: number;
|
||||||
|
totalDamage: number;
|
||||||
|
totalDamageTaken: number;
|
||||||
|
totalDefeats: number;
|
||||||
|
totalSupport: number;
|
||||||
|
roleCoverage: number;
|
||||||
|
activeBondCount: number;
|
||||||
|
contributionTotals: SortiePerformanceContributionTotals;
|
||||||
|
snapshot: CampaignSortiePerformanceSnapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortiePerformanceReviewInput = {
|
||||||
|
outcome: 'victory' | 'defeat';
|
||||||
|
objectives: { achieved: boolean }[];
|
||||||
|
enemyCount: number;
|
||||||
|
activeBondCount: number;
|
||||||
|
endUnits: SortiePerformanceEndUnit[];
|
||||||
|
snapshot: CampaignSortiePerformanceSnapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gradeTone: Record<SortiePerformanceGrade, { color: number; text: string }> = {
|
||||||
|
S: { color: 0xd8b15f, text: '#fff0b5' },
|
||||||
|
A: { color: 0x59d18c, text: '#caffdc' },
|
||||||
|
B: { color: 0x72a7d8, text: '#d8ecff' },
|
||||||
|
C: { color: 0xc87552, text: '#ffd0bd' },
|
||||||
|
D: { color: 0x9a5b55, text: '#ffb6a6' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const gradeHeadlines: Record<SortiePerformanceGrade, string> = {
|
||||||
|
S: '빈틈없이 목표를 완수한 모범 편성',
|
||||||
|
A: '안정적으로 전선을 지켜낸 우수 편성',
|
||||||
|
B: '강점이 분명하고 보완 여지가 있는 편성',
|
||||||
|
C: '역할과 생존 계획을 다시 다듬을 편성',
|
||||||
|
D: '재도전 전 전면적인 재편이 필요한 편성'
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleLabels = {
|
||||||
|
front: '전열 방호',
|
||||||
|
flank: '돌파 선봉',
|
||||||
|
support: '후원 지휘'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function evaluateSortiePerformanceReview(input: SortiePerformanceReviewInput): SortiePerformanceReview {
|
||||||
|
const { snapshot } = input;
|
||||||
|
const unitsById = new Map(input.endUnits.map((unit) => [unit.id, unit]));
|
||||||
|
const deployedUnits = snapshot.selectedUnitIds
|
||||||
|
.map((unitId) => unitsById.get(unitId))
|
||||||
|
.filter((unit): unit is SortiePerformanceEndUnit => Boolean(unit));
|
||||||
|
const objectiveAchieved = input.objectives.filter((objective) => objective.achieved).length;
|
||||||
|
const survivorCount = deployedUnits.filter((unit) => unit.hp > 0).length;
|
||||||
|
const recoveryNeededCount = deployedUnits.filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp).length;
|
||||||
|
const totalDamage = snapshot.unitStats.reduce((sum, stats) => sum + stats.damageDealt, 0);
|
||||||
|
const totalDamageTaken = snapshot.unitStats.reduce((sum, stats) => sum + stats.damageTaken, 0);
|
||||||
|
const totalDefeats = snapshot.unitStats.reduce((sum, stats) => sum + stats.defeats, 0);
|
||||||
|
const totalSupport = snapshot.unitStats.reduce((sum, stats) => sum + stats.support, 0);
|
||||||
|
const roleCoverage = coreSortieSynergyRoles.filter((role) =>
|
||||||
|
snapshot.selectedUnitIds.some((unitId) => snapshot.formationAssignments[unitId] === role)
|
||||||
|
).length;
|
||||||
|
const contributionTotals = snapshot.unitStats.reduce<SortiePerformanceContributionTotals>((totals, stats) => {
|
||||||
|
totals.bonusDamage += stats.sortieBonusDamage;
|
||||||
|
totals.preventedDamage += stats.sortiePreventedDamage;
|
||||||
|
totals.bonusHealing += stats.sortieBonusHealing;
|
||||||
|
totals.extendedBondTriggers += stats.sortieExtendedBondTriggers;
|
||||||
|
totals.counterplays += stats.intentDefeats + stats.intentLineBreaks + stats.intentGuardSuccesses;
|
||||||
|
return totals;
|
||||||
|
}, {
|
||||||
|
bonusDamage: 0,
|
||||||
|
preventedDamage: 0,
|
||||||
|
bonusHealing: 0,
|
||||||
|
extendedBondTriggers: 0,
|
||||||
|
counterplays: 0
|
||||||
|
});
|
||||||
|
const objectiveRate = input.objectives.length > 0 ? objectiveAchieved / input.objectives.length : 1;
|
||||||
|
const survivalRate = deployedUnits.length > 0 ? survivorCount / deployedUnits.length : 0;
|
||||||
|
const defeatRate = input.enemyCount > 0 ? Math.min(1, totalDefeats / input.enemyCount) : 1;
|
||||||
|
const contributionScore = contributionTotals.bonusDamage + contributionTotals.preventedDamage +
|
||||||
|
contributionTotals.bonusHealing + contributionTotals.extendedBondTriggers * 4 + contributionTotals.counterplays * 3;
|
||||||
|
const score = Math.min(100, Math.max(0, Math.round(
|
||||||
|
(input.outcome === 'victory' ? 35 : 10) +
|
||||||
|
objectiveRate * 25 +
|
||||||
|
survivalRate * 20 +
|
||||||
|
defeatRate * 10 +
|
||||||
|
(roleCoverage / coreSortieSynergyRoles.length) * 5 +
|
||||||
|
Math.min(5, input.activeBondCount * 2) +
|
||||||
|
Math.min(5, contributionScore / 12)
|
||||||
|
)));
|
||||||
|
const grade: SortiePerformanceGrade = score >= 90 ? 'S' : score >= 75 ? 'A' : score >= 60 ? 'B' : score >= 45 ? 'C' : 'D';
|
||||||
|
const strengths = [
|
||||||
|
objectiveAchieved === input.objectives.length && input.objectives.length > 0 ? `목표 ${objectiveAchieved}/${input.objectives.length} 완수` : '',
|
||||||
|
survivorCount === deployedUnits.length && deployedUnits.length > 0 ? '출전 전원 생존' : '',
|
||||||
|
roleCoverage === coreSortieSynergyRoles.length ? '핵심 역할 3/3 충족' : '',
|
||||||
|
input.activeBondCount > 0 ? `활성 공명 ${input.activeBondCount}개` : '',
|
||||||
|
contributionScore > 0 ? `편성 효과 기여 ${contributionScore}` : '',
|
||||||
|
totalDefeats > 0 ? `적 ${totalDefeats}명 격파` : ''
|
||||||
|
].filter(Boolean).slice(0, 3);
|
||||||
|
const missingRoleLabels = coreSortieSynergyRoles
|
||||||
|
.filter((role) => !snapshot.selectedUnitIds.some((unitId) => snapshot.formationAssignments[unitId] === role))
|
||||||
|
.map((role) => roleLabels[role]);
|
||||||
|
const improvement = input.outcome === 'defeat'
|
||||||
|
? '패배 조건을 먼저 차단하고 필수 무장의 생존 동선을 확보하십시오.'
|
||||||
|
: survivorCount < deployedUnits.length
|
||||||
|
? `퇴각 ${deployedUnits.length - survivorCount}명 · 전열 방호와 보급 배정을 강화하십시오.`
|
||||||
|
: missingRoleLabels.length > 0
|
||||||
|
? `${missingRoleLabels.join('·')} 공백 · 역할을 다시 배정하십시오.`
|
||||||
|
: objectiveAchieved < input.objectives.length
|
||||||
|
? `미달 목표 ${input.objectives.length - objectiveAchieved}개 · 기동 담당과 동선을 조정하십시오.`
|
||||||
|
: recoveryNeededCount > 0 && totalSupport === 0
|
||||||
|
? `회복 필요 ${recoveryNeededCount}명 · 후원 담당의 회복·보급 사용을 늘리십시오.`
|
||||||
|
: '현재 명단과 역할은 편성책에 기록해 다시 활용할 가치가 있습니다.';
|
||||||
|
|
||||||
|
return {
|
||||||
|
score,
|
||||||
|
grade,
|
||||||
|
gradeColor: gradeTone[grade].color,
|
||||||
|
gradeTextColor: gradeTone[grade].text,
|
||||||
|
headline: gradeHeadlines[grade],
|
||||||
|
strengths: strengths.length > 0 ? strengths : ['전투 기록 확보'],
|
||||||
|
improvement,
|
||||||
|
objectiveAchieved,
|
||||||
|
objectiveTotal: input.objectives.length,
|
||||||
|
survivorCount,
|
||||||
|
deployedCount: deployedUnits.length,
|
||||||
|
recoveryNeededCount,
|
||||||
|
totalDamage,
|
||||||
|
totalDamageTaken,
|
||||||
|
totalDefeats,
|
||||||
|
totalSupport,
|
||||||
|
roleCoverage,
|
||||||
|
activeBondCount: input.activeBondCount,
|
||||||
|
contributionTotals,
|
||||||
|
snapshot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortiePerformanceRoleContributionLine(review: SortiePerformanceReview) {
|
||||||
|
const statsById = new Map(review.snapshot.unitStats.map((stats) => [stats.unitId, stats]));
|
||||||
|
return coreSortieSynergyRoles.map((role) => {
|
||||||
|
const roleStats = review.snapshot.selectedUnitIds
|
||||||
|
.filter((unitId) => review.snapshot.formationAssignments[unitId] === role)
|
||||||
|
.map((unitId) => statsById.get(unitId))
|
||||||
|
.filter((stats): stats is NonNullable<typeof stats> => Boolean(stats));
|
||||||
|
const value = role === 'front'
|
||||||
|
? roleStats.reduce((sum, stats) => sum + stats.sortiePreventedDamage + stats.sortieBonusDamage, 0)
|
||||||
|
: role === 'flank'
|
||||||
|
? roleStats.reduce((sum, stats) => sum + stats.sortieBonusDamage, 0)
|
||||||
|
: roleStats.reduce((sum, stats) => sum + stats.sortieBonusDamage + stats.sortieBonusHealing, 0);
|
||||||
|
const label = role === 'front' ? '전열' : role === 'flank' ? '돌파' : '후원';
|
||||||
|
return `${label} ${value}`;
|
||||||
|
}).join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortiePerformanceMatchesPreset(
|
||||||
|
snapshot: CampaignSortiePerformanceSnapshot,
|
||||||
|
preset?: CampaignSortiePreset
|
||||||
|
) {
|
||||||
|
return Boolean(
|
||||||
|
preset &&
|
||||||
|
JSON.stringify(preset.selectedUnitIds) === JSON.stringify(snapshot.selectedUnitIds) &&
|
||||||
|
snapshot.selectedUnitIds.every((unitId) => preset.formationAssignments[unitId] === snapshot.formationAssignments[unitId])
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -49,6 +49,12 @@ import {
|
|||||||
sortieBondBonusForLevel,
|
sortieBondBonusForLevel,
|
||||||
sortieRoleEffectSummaries
|
sortieRoleEffectSummaries
|
||||||
} from '../data/sortieSynergy';
|
} from '../data/sortieSynergy';
|
||||||
|
import {
|
||||||
|
evaluateSortiePerformanceReview,
|
||||||
|
sortiePerformanceMatchesPreset,
|
||||||
|
sortiePerformanceRoleContributionLine,
|
||||||
|
type SortiePerformanceReview
|
||||||
|
} from '../data/sortiePerformanceReview';
|
||||||
import {
|
import {
|
||||||
campaignSortiePresetIds,
|
campaignSortiePresetIds,
|
||||||
campaignSaveSlotCount,
|
campaignSaveSlotCount,
|
||||||
@@ -1277,39 +1283,6 @@ type UnitBattleStats = {
|
|||||||
intentGuardSuccesses: number;
|
intentGuardSuccesses: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResultFormationGrade = 'S' | 'A' | 'B' | 'C' | 'D';
|
|
||||||
|
|
||||||
type ResultSortieContributionTotals = {
|
|
||||||
bonusDamage: number;
|
|
||||||
preventedDamage: number;
|
|
||||||
bonusHealing: number;
|
|
||||||
extendedBondTriggers: number;
|
|
||||||
counterplays: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResultFormationReview = {
|
|
||||||
score: number;
|
|
||||||
grade: ResultFormationGrade;
|
|
||||||
gradeColor: number;
|
|
||||||
gradeTextColor: string;
|
|
||||||
headline: string;
|
|
||||||
strengths: string[];
|
|
||||||
improvement: string;
|
|
||||||
objectiveAchieved: number;
|
|
||||||
objectiveTotal: number;
|
|
||||||
survivorCount: number;
|
|
||||||
deployedCount: number;
|
|
||||||
recoveryNeededCount: number;
|
|
||||||
totalDamage: number;
|
|
||||||
totalDamageTaken: number;
|
|
||||||
totalDefeats: number;
|
|
||||||
totalSupport: number;
|
|
||||||
roleCoverage: number;
|
|
||||||
activeBondCount: number;
|
|
||||||
contributionTotals: ResultSortieContributionTotals;
|
|
||||||
snapshot: CampaignSortiePerformanceSnapshot;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResultFormationReviewView = {
|
type ResultFormationReviewView = {
|
||||||
closeButton: Phaser.GameObjects.Rectangle;
|
closeButton: Phaser.GameObjects.Rectangle;
|
||||||
presetButtons: Partial<Record<CampaignSortiePresetId, Phaser.GameObjects.Rectangle>>;
|
presetButtons: Partial<Record<CampaignSortiePresetId, Phaser.GameObjects.Rectangle>>;
|
||||||
@@ -10777,125 +10750,21 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private resultFormationReview(
|
private resultFormationReview(
|
||||||
outcome: BattleOutcome,
|
outcome: BattleOutcome,
|
||||||
snapshot: CampaignSortiePerformanceSnapshot
|
snapshot: CampaignSortiePerformanceSnapshot
|
||||||
): ResultFormationReview {
|
): SortiePerformanceReview {
|
||||||
const unitsById = new Map(battleUnits.filter((unit) => unit.faction === 'ally').map((unit) => [unit.id, unit]));
|
return evaluateSortiePerformanceReview({
|
||||||
const deployedUnits = snapshot.selectedUnitIds
|
outcome,
|
||||||
.map((unitId) => unitsById.get(unitId))
|
objectives: this.resultObjectives(outcome),
|
||||||
.filter((unit): unit is UnitData => Boolean(unit));
|
enemyCount: battleUnits.filter((unit) => unit.faction === 'enemy').length,
|
||||||
const objectives = this.resultObjectives(outcome);
|
activeBondCount: this.activeSortieBonds().length,
|
||||||
const objectiveAchieved = objectives.filter((objective) => objective.achieved).length;
|
endUnits: battleUnits
|
||||||
const survivorCount = deployedUnits.filter((unit) => unit.hp > 0).length;
|
.filter((unit) => unit.faction === 'ally')
|
||||||
const recoveryNeededCount = deployedUnits.filter((unit) => unit.hp > 0 && unit.hp < unit.maxHp).length;
|
.map((unit) => ({ id: unit.id, hp: unit.hp, maxHp: unit.maxHp })),
|
||||||
const totalDamage = snapshot.unitStats.reduce((sum, stats) => sum + stats.damageDealt, 0);
|
|
||||||
const totalDamageTaken = snapshot.unitStats.reduce((sum, stats) => sum + stats.damageTaken, 0);
|
|
||||||
const totalDefeats = snapshot.unitStats.reduce((sum, stats) => sum + stats.defeats, 0);
|
|
||||||
const totalSupport = snapshot.unitStats.reduce((sum, stats) => sum + stats.support, 0);
|
|
||||||
const roleCoverage = coreSortieSynergyRoles.filter((role) =>
|
|
||||||
snapshot.selectedUnitIds.some((unitId) => snapshot.formationAssignments[unitId] === role)
|
|
||||||
).length;
|
|
||||||
const activeBondCount = this.activeSortieBonds().length;
|
|
||||||
const contributionTotals = this.sortieContributionTotals();
|
|
||||||
const objectiveRate = objectives.length > 0 ? objectiveAchieved / objectives.length : 1;
|
|
||||||
const survivalRate = deployedUnits.length > 0 ? survivorCount / deployedUnits.length : 0;
|
|
||||||
const enemyCount = battleUnits.filter((unit) => unit.faction === 'enemy').length;
|
|
||||||
const defeatRate = enemyCount > 0 ? Math.min(1, totalDefeats / enemyCount) : 1;
|
|
||||||
const contributionScore = contributionTotals.bonusDamage + contributionTotals.preventedDamage +
|
|
||||||
contributionTotals.bonusHealing + contributionTotals.extendedBondTriggers * 4 + contributionTotals.counterplays * 3;
|
|
||||||
const score = Phaser.Math.Clamp(Math.round(
|
|
||||||
(outcome === 'victory' ? 35 : 10) +
|
|
||||||
objectiveRate * 25 +
|
|
||||||
survivalRate * 20 +
|
|
||||||
defeatRate * 10 +
|
|
||||||
(roleCoverage / coreSortieSynergyRoles.length) * 5 +
|
|
||||||
Math.min(5, activeBondCount * 2) +
|
|
||||||
Math.min(5, contributionScore / 12)
|
|
||||||
), 0, 100);
|
|
||||||
const grade: ResultFormationGrade = score >= 90 ? 'S' : score >= 75 ? 'A' : score >= 60 ? 'B' : score >= 45 ? 'C' : 'D';
|
|
||||||
const gradeTone: Record<ResultFormationGrade, { color: number; text: string }> = {
|
|
||||||
S: { color: 0xd8b15f, text: '#fff0b5' },
|
|
||||||
A: { color: 0x59d18c, text: '#caffdc' },
|
|
||||||
B: { color: 0x72a7d8, text: '#d8ecff' },
|
|
||||||
C: { color: 0xc87552, text: '#ffd0bd' },
|
|
||||||
D: { color: 0x9a5b55, text: '#ffb6a6' }
|
|
||||||
};
|
|
||||||
const headlines: Record<ResultFormationGrade, string> = {
|
|
||||||
S: '빈틈없이 목표를 완수한 모범 편성',
|
|
||||||
A: '안정적으로 전선을 지켜낸 우수 편성',
|
|
||||||
B: '강점이 분명하고 보완 여지가 있는 편성',
|
|
||||||
C: '역할과 생존 계획을 다시 다듬을 편성',
|
|
||||||
D: '재도전 전 전면적인 재편이 필요한 편성'
|
|
||||||
};
|
|
||||||
const strengths = [
|
|
||||||
objectiveAchieved === objectives.length && objectives.length > 0 ? `목표 ${objectiveAchieved}/${objectives.length} 완수` : '',
|
|
||||||
survivorCount === deployedUnits.length && deployedUnits.length > 0 ? '출전 전원 생존' : '',
|
|
||||||
roleCoverage === coreSortieSynergyRoles.length ? '핵심 역할 3/3 충족' : '',
|
|
||||||
activeBondCount > 0 ? `활성 공명 ${activeBondCount}개` : '',
|
|
||||||
contributionScore > 0 ? `편성 효과 기여 ${contributionScore}` : '',
|
|
||||||
totalDefeats > 0 ? `적 ${totalDefeats}명 격파` : ''
|
|
||||||
].filter(Boolean).slice(0, 3);
|
|
||||||
const missingRoleLabels = coreSortieSynergyRoles
|
|
||||||
.filter((role) => !snapshot.selectedUnitIds.some((unitId) => snapshot.formationAssignments[unitId] === role))
|
|
||||||
.map((role) => sortieRoleEffects[role]?.label ?? role);
|
|
||||||
const improvement = outcome === 'defeat'
|
|
||||||
? '패배 조건을 먼저 차단하고 필수 무장의 생존 동선을 확보하십시오.'
|
|
||||||
: survivorCount < deployedUnits.length
|
|
||||||
? `퇴각 ${deployedUnits.length - survivorCount}명 · 전열 방호와 보급 배정을 강화하십시오.`
|
|
||||||
: missingRoleLabels.length > 0
|
|
||||||
? `${missingRoleLabels.join('·')} 공백 · 역할을 다시 배정하십시오.`
|
|
||||||
: objectiveAchieved < objectives.length
|
|
||||||
? `미달 목표 ${objectives.length - objectiveAchieved}개 · 기동 담당과 동선을 조정하십시오.`
|
|
||||||
: recoveryNeededCount > 0 && totalSupport === 0
|
|
||||||
? `회복 필요 ${recoveryNeededCount}명 · 후원 담당의 회복·보급 사용을 늘리십시오.`
|
|
||||||
: '현재 명단과 역할은 편성책에 기록해 다시 활용할 가치가 있습니다.';
|
|
||||||
|
|
||||||
return {
|
|
||||||
score,
|
|
||||||
grade,
|
|
||||||
gradeColor: gradeTone[grade].color,
|
|
||||||
gradeTextColor: gradeTone[grade].text,
|
|
||||||
headline: headlines[grade],
|
|
||||||
strengths: strengths.length > 0 ? strengths : ['전투 기록 확보'],
|
|
||||||
improvement,
|
|
||||||
objectiveAchieved,
|
|
||||||
objectiveTotal: objectives.length,
|
|
||||||
survivorCount,
|
|
||||||
deployedCount: deployedUnits.length,
|
|
||||||
recoveryNeededCount,
|
|
||||||
totalDamage,
|
|
||||||
totalDamageTaken,
|
|
||||||
totalDefeats,
|
|
||||||
totalSupport,
|
|
||||||
roleCoverage,
|
|
||||||
activeBondCount,
|
|
||||||
contributionTotals,
|
|
||||||
snapshot
|
snapshot
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private resultFormationRoleContributionLine(review: ResultFormationReview) {
|
|
||||||
const statsById = new Map(review.snapshot.unitStats.map((stats) => [stats.unitId, stats]));
|
|
||||||
return coreSortieSynergyRoles.map((role) => {
|
|
||||||
const roleStats = review.snapshot.selectedUnitIds
|
|
||||||
.filter((unitId) => review.snapshot.formationAssignments[unitId] === role)
|
|
||||||
.map((unitId) => statsById.get(unitId))
|
|
||||||
.filter((stats): stats is NonNullable<typeof stats> => Boolean(stats));
|
|
||||||
const value = role === 'front'
|
|
||||||
? roleStats.reduce((sum, stats) => sum + stats.sortiePreventedDamage + stats.sortieBonusDamage, 0)
|
|
||||||
: role === 'flank'
|
|
||||||
? roleStats.reduce((sum, stats) => sum + stats.sortieBonusDamage, 0)
|
|
||||||
: roleStats.reduce((sum, stats) => sum + stats.sortieBonusDamage + stats.sortieBonusHealing, 0);
|
|
||||||
const label = role === 'front' ? '전열' : role === 'flank' ? '돌파' : '후원';
|
|
||||||
return `${label} ${value}`;
|
|
||||||
}).join(' · ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resultFormationSnapshotMatchesPreset(presetId: CampaignSortiePresetId, snapshot: CampaignSortiePerformanceSnapshot) {
|
private resultFormationSnapshotMatchesPreset(presetId: CampaignSortiePresetId, snapshot: CampaignSortiePerformanceSnapshot) {
|
||||||
const preset = getCampaignState().sortieFormationPresets[presetId];
|
return sortiePerformanceMatchesPreset(snapshot, getCampaignState().sortieFormationPresets[presetId]);
|
||||||
return Boolean(
|
|
||||||
preset &&
|
|
||||||
JSON.stringify(preset.selectedUnitIds) === JSON.stringify(snapshot.selectedUnitIds) &&
|
|
||||||
snapshot.selectedUnitIds.every((unitId) => preset.formationAssignments[unitId] === snapshot.formationAssignments[unitId])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resultFormationSourcePresetIds(snapshot: CampaignSortiePerformanceSnapshot) {
|
private resultFormationSourcePresetIds(snapshot: CampaignSortiePerformanceSnapshot) {
|
||||||
@@ -10980,7 +10849,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.renderResultFormationAnalysisPanel(
|
this.renderResultFormationAnalysisPanel(
|
||||||
'역할·공명 기여',
|
'역할·공명 기여',
|
||||||
[
|
[
|
||||||
`${this.resultFormationRoleContributionLine(review)} · 역할 ${review.roleCoverage}/3`,
|
`${sortiePerformanceRoleContributionLine(review)} · 역할 ${review.roleCoverage}/3`,
|
||||||
`공명 ${review.activeBondCount}개 · 연장 ${review.contributionTotals.extendedBondTriggers}회 · 파훼 ${review.contributionTotals.counterplays}`
|
`공명 ${review.activeBondCount}개 · 연장 ${review.contributionTotals.extendedBondTriggers}회 · 파훼 ${review.contributionTotals.counterplays}`
|
||||||
],
|
],
|
||||||
x + 624,
|
x + 624,
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ import {
|
|||||||
type SortieSynergyComparison,
|
type SortieSynergyComparison,
|
||||||
type SortieSynergySnapshot
|
type SortieSynergySnapshot
|
||||||
} from '../data/sortieSynergy';
|
} from '../data/sortieSynergy';
|
||||||
|
import {
|
||||||
|
evaluateSortiePerformanceReview,
|
||||||
|
sortiePerformanceMatchesPreset,
|
||||||
|
sortiePerformanceRoleContributionLine,
|
||||||
|
type SortiePerformanceReview
|
||||||
|
} from '../data/sortiePerformanceReview';
|
||||||
import {
|
import {
|
||||||
caoBreakRecruitBonds,
|
caoBreakRecruitBonds,
|
||||||
caoBreakRecruitUnits,
|
caoBreakRecruitUnits,
|
||||||
@@ -132,6 +138,7 @@ import {
|
|||||||
type CampaignReserveTrainingFocusId,
|
type CampaignReserveTrainingFocusId,
|
||||||
type CampaignSortieItemAssignments,
|
type CampaignSortieItemAssignments,
|
||||||
type CampaignSortieFormationPresets,
|
type CampaignSortieFormationPresets,
|
||||||
|
type CampaignSortiePerformanceSnapshot,
|
||||||
type CampaignSortiePresetId,
|
type CampaignSortiePresetId,
|
||||||
type FirstBattleReport
|
type FirstBattleReport
|
||||||
} from '../state/campaignState';
|
} from '../state/campaignState';
|
||||||
@@ -435,6 +442,11 @@ type SortiePresetBrowserView = {
|
|||||||
cards: Partial<Record<CampaignSortiePresetId, SortiePresetCardView>>;
|
cards: Partial<Record<CampaignSortiePresetId, SortiePresetCardView>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ReportFormationReviewView = {
|
||||||
|
closeButton: Phaser.GameObjects.Rectangle;
|
||||||
|
presetButtons: Partial<Record<CampaignSortiePresetId, Phaser.GameObjects.Rectangle>>;
|
||||||
|
};
|
||||||
|
|
||||||
type SortieComparisonAction = {
|
type SortieComparisonAction = {
|
||||||
kind: 'confirm-swap' | 'undo-swap' | 'confirm-preset' | 'undo-preset';
|
kind: 'confirm-swap' | 'undo-swap' | 'confirm-preset' | 'undo-preset';
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10894,6 +10906,12 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private saveSlotObjects: Phaser.GameObjects.GameObject[] = [];
|
private saveSlotObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private saveSlotConfirmObjects: Phaser.GameObjects.GameObject[] = [];
|
private saveSlotConfirmObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
private pendingSaveSlot?: number;
|
private pendingSaveSlot?: number;
|
||||||
|
private reportFormationReviewObjects: Phaser.GameObjects.GameObject[] = [];
|
||||||
|
private reportFormationReviewVisible = false;
|
||||||
|
private reportFormationReviewFeedback = '';
|
||||||
|
private reportFormationPresetOverwriteConfirmId?: CampaignSortiePresetId;
|
||||||
|
private reportFormationReviewToggleButton?: Phaser.GameObjects.Rectangle;
|
||||||
|
private reportFormationReviewView?: ReportFormationReviewView;
|
||||||
private tabButtons: CampTabButtonView[] = [];
|
private tabButtons: CampTabButtonView[] = [];
|
||||||
private visitedTabs = new Set<CampTab>();
|
private visitedTabs = new Set<CampTab>();
|
||||||
private selectedSortieUnitIds: string[] = [];
|
private selectedSortieUnitIds: string[] = [];
|
||||||
@@ -10944,6 +10962,12 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.saveSlotObjects = [];
|
this.saveSlotObjects = [];
|
||||||
this.saveSlotConfirmObjects = [];
|
this.saveSlotConfirmObjects = [];
|
||||||
this.pendingSaveSlot = undefined;
|
this.pendingSaveSlot = undefined;
|
||||||
|
this.reportFormationReviewObjects = [];
|
||||||
|
this.reportFormationReviewVisible = false;
|
||||||
|
this.reportFormationReviewFeedback = '';
|
||||||
|
this.reportFormationPresetOverwriteConfirmId = undefined;
|
||||||
|
this.reportFormationReviewToggleButton = undefined;
|
||||||
|
this.reportFormationReviewView = undefined;
|
||||||
this.tabButtons = [];
|
this.tabButtons = [];
|
||||||
this.visitedTabs = new Set(['status']);
|
this.visitedTabs = new Set(['status']);
|
||||||
this.campaign = getCampaignState();
|
this.campaign = getCampaignState();
|
||||||
@@ -12350,6 +12374,12 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.reportFormationReviewVisible) {
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.hideReportFormationReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.sortieObjects.length > 0) {
|
if (this.sortieObjects.length > 0) {
|
||||||
soundDirector.playSelect();
|
soundDirector.playSelect();
|
||||||
this.hideSortiePrep();
|
this.hideSortiePrep();
|
||||||
@@ -12434,6 +12464,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private render() {
|
private render() {
|
||||||
|
this.hideReportFormationReview();
|
||||||
this.hideCampSaveSlotPanel();
|
this.hideCampSaveSlotPanel();
|
||||||
this.clearContent();
|
this.clearContent();
|
||||||
this.visitedTabs.add(this.activeTab);
|
this.visitedTabs.add(this.activeTab);
|
||||||
@@ -12471,6 +12502,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private showSortiePrep() {
|
private showSortiePrep() {
|
||||||
const wasVisible = this.sortieObjects.length > 0;
|
const wasVisible = this.sortieObjects.length > 0;
|
||||||
|
this.hideReportFormationReview();
|
||||||
this.hideCampSaveSlotPanel();
|
this.hideCampSaveSlotPanel();
|
||||||
this.hideSortiePrep(false);
|
this.hideSortiePrep(false);
|
||||||
this.campaign = getCampaignState();
|
this.campaign = getCampaignState();
|
||||||
@@ -16605,6 +16637,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reportFormationReviewToggleButton = undefined;
|
||||||
const x = 42;
|
const x = 42;
|
||||||
const y = 590;
|
const y = 590;
|
||||||
const bg = this.track(this.add.rectangle(x, y, 1180, 84, 0x101820, 0.86));
|
const bg = this.track(this.add.rectangle(x, y, 1180, 84, 0x101820, 0.86));
|
||||||
@@ -16615,7 +16648,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.add.text(
|
this.add.text(
|
||||||
x + 18,
|
x + 18,
|
||||||
y + 39,
|
y + 39,
|
||||||
`MVP ${report.mvp?.name ?? '-'} · 전투 보상 ${report.rewardGold} · 전리품 ${report.itemRewards.join(', ') || '없음'}`,
|
this.compactText(`MVP ${report.mvp?.name ?? '-'} · 전투 보상 ${report.rewardGold} · 전리품 ${report.itemRewards.join(', ') || '없음'}`, 66),
|
||||||
this.textStyle(14, '#d4dce6')
|
this.textStyle(14, '#d4dce6')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -16623,10 +16656,80 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.add.text(
|
this.add.text(
|
||||||
x + 18,
|
x + 18,
|
||||||
y + 62,
|
y + 62,
|
||||||
this.reportCampaignRewardLine(report),
|
this.compactText(this.reportCampaignRewardLine(report), 70),
|
||||||
this.textStyle(12, '#d8b15f', true)
|
this.textStyle(12, '#d8b15f', true)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const review = this.reportFormationReview();
|
||||||
|
if (review) {
|
||||||
|
this.reportFormationReviewToggleButton = this.addReportFormationReviewToggleButton(
|
||||||
|
`편성 평가 ${review.grade}`,
|
||||||
|
x + 1084,
|
||||||
|
y + 42,
|
||||||
|
158,
|
||||||
|
() => this.showReportFormationReview()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportFormationReview(): SortiePerformanceReview | undefined {
|
||||||
|
const report = this.report;
|
||||||
|
if (!report || (this.campaign?.latestBattleId && report.battleId !== this.campaign.latestBattleId)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settlement = this.campaign?.battleHistory[report.battleId];
|
||||||
|
const snapshot = settlement?.sortiePerformance ?? report.sortiePerformance;
|
||||||
|
if (!snapshot) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endUnits = settlement
|
||||||
|
? settlement.units.map((unit) => ({ id: unit.unitId, hp: unit.hp, maxHp: unit.maxHp }))
|
||||||
|
: report.units
|
||||||
|
.filter((unit) => unit.faction === 'ally')
|
||||||
|
.map((unit) => ({ id: unit.id, hp: unit.hp, maxHp: unit.maxHp }));
|
||||||
|
const endUnitIds = new Set(endUnits.map((unit) => unit.id));
|
||||||
|
if (!snapshot.selectedUnitIds.some((unitId) => endUnitIds.has(unitId))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIds = new Set(snapshot.selectedUnitIds);
|
||||||
|
const activeBondCount = report.bonds.filter((bond) => bond.unitIds.every((unitId) => selectedIds.has(unitId))).length;
|
||||||
|
return evaluateSortiePerformanceReview({
|
||||||
|
outcome: settlement?.outcome ?? report.outcome,
|
||||||
|
objectives: settlement?.objectives ?? report.objectives,
|
||||||
|
enemyCount: report.totalEnemies,
|
||||||
|
activeBondCount,
|
||||||
|
endUnits,
|
||||||
|
snapshot
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportFormationSourcePresetIds(snapshot: CampaignSortiePerformanceSnapshot) {
|
||||||
|
const presets = this.sortieFormationPresets();
|
||||||
|
return campaignSortiePresetIds.filter((presetId) => sortiePerformanceMatchesPreset(snapshot, presets[presetId]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private addReportFormationReviewToggleButton(
|
||||||
|
label: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
action: () => void
|
||||||
|
) {
|
||||||
|
const background = this.track(this.add.rectangle(x, y, width, 36, 0x1a2630, 0.98));
|
||||||
|
background.setStrokeStyle(1, palette.gold, 0.82);
|
||||||
|
background.setInteractive({ useHandCursor: true });
|
||||||
|
background.on('pointerover', () => background.setFillStyle(0x283947, 1).setStrokeStyle(1, palette.gold, 0.98));
|
||||||
|
background.on('pointerout', () => background.setFillStyle(0x1a2630, 0.98).setStrokeStyle(1, palette.gold, 0.82));
|
||||||
|
background.on('pointerdown', () => {
|
||||||
|
soundDirector.playSelect();
|
||||||
|
action();
|
||||||
|
});
|
||||||
|
this.track(this.add.text(x, y, label, this.textStyle(14, '#f2e3bf', true))).setOrigin(0.5);
|
||||||
|
return background;
|
||||||
}
|
}
|
||||||
|
|
||||||
private reportCampaignRewardLine(report: FirstBattleReport) {
|
private reportCampaignRewardLine(report: FirstBattleReport) {
|
||||||
@@ -16648,6 +16751,314 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return parts.length > 0 ? `캠페인 반영: ${parts.join(' · ')}` : '캠페인 보상: 추가 해금 없음';
|
return parts.length > 0 ? `캠페인 반영: ${parts.join(' · ')}` : '캠페인 보상: 추가 해금 없음';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showReportFormationReview() {
|
||||||
|
if (!this.reportFormationReview()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hideCampSaveSlotPanel();
|
||||||
|
this.reportFormationReviewVisible = true;
|
||||||
|
this.renderReportFormationReview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderReportFormationReview() {
|
||||||
|
const review = this.reportFormationReview();
|
||||||
|
const report = this.report;
|
||||||
|
if (!review || !report) {
|
||||||
|
this.hideReportFormationReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reportFormationReviewObjects.forEach((object) => object.destroy());
|
||||||
|
this.reportFormationReviewObjects = [];
|
||||||
|
this.reportFormationReviewView = undefined;
|
||||||
|
const depth = 52;
|
||||||
|
const width = 980;
|
||||||
|
const height = 420;
|
||||||
|
const x = Math.floor((this.scale.width - width) / 2);
|
||||||
|
const y = 150;
|
||||||
|
|
||||||
|
const shade = this.trackReportFormationReview(this.add.rectangle(0, 0, this.scale.width, this.scale.height, 0x020406, 0.76));
|
||||||
|
shade.setOrigin(0);
|
||||||
|
shade.setDepth(depth);
|
||||||
|
shade.setInteractive();
|
||||||
|
const background = this.trackReportFormationReview(this.add.rectangle(x, y, width, height, 0x0b1219, 0.995));
|
||||||
|
background.setOrigin(0);
|
||||||
|
background.setDepth(depth + 1);
|
||||||
|
background.setStrokeStyle(2, review.gradeColor, 0.9);
|
||||||
|
background.setInteractive();
|
||||||
|
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 18, y + 12, '최근 전투 편성 평가', this.textStyle(20, '#f2e3bf', true)))
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
const closeButton = this.addReportFormationReviewButton(
|
||||||
|
'전투 보고로',
|
||||||
|
x + width - 112,
|
||||||
|
y + 9,
|
||||||
|
96,
|
||||||
|
28,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
() => {
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.hideReportFormationReview();
|
||||||
|
},
|
||||||
|
depth + 3
|
||||||
|
);
|
||||||
|
|
||||||
|
const gradeSeal = this.trackReportFormationReview(this.add.circle(x + 58, y + 82, 34, review.gradeColor, 0.22));
|
||||||
|
gradeSeal.setDepth(depth + 2);
|
||||||
|
gradeSeal.setStrokeStyle(2, review.gradeColor, 0.96);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 58, y + 82, review.grade, this.textStyle(34, review.gradeTextColor, true)))
|
||||||
|
.setOrigin(0.5)
|
||||||
|
.setDepth(depth + 3);
|
||||||
|
this.trackReportFormationReview(
|
||||||
|
this.add.text(
|
||||||
|
x + 104,
|
||||||
|
y + 49,
|
||||||
|
this.compactText(`${report.battleTitle} · ${review.headline} · ${review.score}점`, 52),
|
||||||
|
this.textStyle(17, review.gradeTextColor, true)
|
||||||
|
)
|
||||||
|
).setDepth(depth + 2);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 104, y + 75, `강점 · ${review.strengths.join(' · ')}`, this.textStyle(11, '#d4dce6', true)))
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 104, y + 96, `보완 · ${this.compactText(review.improvement, 70)}`, this.textStyle(11, '#ffdf7b', true)))
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
|
||||||
|
const metricY = y + 126;
|
||||||
|
const metricGap = 10;
|
||||||
|
const metricWidth = Math.floor((width - 36 - metricGap * 3) / 4);
|
||||||
|
const metrics = [
|
||||||
|
{ label: '목표 달성', value: `${review.objectiveAchieved}/${review.objectiveTotal}` },
|
||||||
|
{ label: '생존·회복', value: `${review.survivorCount}/${review.deployedCount} · 회복 ${review.recoveryNeededCount}` },
|
||||||
|
{ label: '전과', value: `피해 ${review.totalDamage} · 격파 ${review.totalDefeats}` },
|
||||||
|
{ label: '지원·피격', value: `지원 ${review.totalSupport} · 피격 ${review.totalDamageTaken}` }
|
||||||
|
];
|
||||||
|
metrics.forEach((metric, index) => {
|
||||||
|
this.renderReportFormationMetric(metric.label, metric.value, x + 18 + index * (metricWidth + metricGap), metricY, metricWidth, depth + 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const analysisY = y + 184;
|
||||||
|
this.renderReportFormationAnalysisPanel(
|
||||||
|
'전술 판단',
|
||||||
|
[
|
||||||
|
`강점 · ${review.strengths.join(' · ')}`,
|
||||||
|
`보완 · ${this.compactText(review.improvement, 62)}`
|
||||||
|
],
|
||||||
|
x + 18,
|
||||||
|
analysisY,
|
||||||
|
596,
|
||||||
|
72,
|
||||||
|
review.gradeColor,
|
||||||
|
depth + 2
|
||||||
|
);
|
||||||
|
this.renderReportFormationAnalysisPanel(
|
||||||
|
'역할·공명 기여',
|
||||||
|
[
|
||||||
|
`${sortiePerformanceRoleContributionLine(review)} · 역할 ${review.roleCoverage}/3`,
|
||||||
|
`공명 ${review.activeBondCount}개 · 연장 ${review.contributionTotals.extendedBondTriggers}회 · 파훼 ${review.contributionTotals.counterplays}`
|
||||||
|
],
|
||||||
|
x + 624,
|
||||||
|
analysisY,
|
||||||
|
338,
|
||||||
|
72,
|
||||||
|
palette.blue,
|
||||||
|
depth + 2
|
||||||
|
);
|
||||||
|
|
||||||
|
const rosterY = y + 264;
|
||||||
|
const hiddenUnitCount = Math.max(0, review.snapshot.selectedUnitIds.length - 8);
|
||||||
|
this.trackReportFormationReview(
|
||||||
|
this.add.text(
|
||||||
|
x + 18,
|
||||||
|
rosterY,
|
||||||
|
`출전 명단·역할${hiddenUnitCount > 0 ? ` · 외 ${hiddenUnitCount}명` : ''}`,
|
||||||
|
this.textStyle(13, '#f2e3bf', true)
|
||||||
|
)
|
||||||
|
).setDepth(depth + 2);
|
||||||
|
const visibleUnitIds = review.snapshot.selectedUnitIds.slice(0, 8);
|
||||||
|
const chipGap = 6;
|
||||||
|
const chipWidth = Math.floor((width - 36 - chipGap * Math.max(0, visibleUnitIds.length - 1)) / Math.max(1, visibleUnitIds.length));
|
||||||
|
const settlement = this.campaign?.battleHistory[report.battleId];
|
||||||
|
visibleUnitIds.forEach((unitId, index) => {
|
||||||
|
const settledUnit = settlement?.units.find((unit) => unit.unitId === unitId);
|
||||||
|
const reportUnit = report.units.find((unit) => unit.id === unitId);
|
||||||
|
const hp = settledUnit?.hp ?? reportUnit?.hp ?? 0;
|
||||||
|
const maxHp = settledUnit?.maxHp ?? reportUnit?.maxHp ?? 0;
|
||||||
|
const role = review.snapshot.formationAssignments[unitId];
|
||||||
|
const roleLabel = role === 'front' ? '전' : role === 'flank' ? '돌' : role === 'support' ? '후' : '예';
|
||||||
|
const chipX = x + 18 + index * (chipWidth + chipGap);
|
||||||
|
const chip = this.trackReportFormationReview(this.add.rectangle(chipX, rosterY + 23, chipWidth, 38, hp > 0 ? 0x172a22 : 0x2a1b18, 0.94));
|
||||||
|
chip.setOrigin(0);
|
||||||
|
chip.setDepth(depth + 2);
|
||||||
|
chip.setStrokeStyle(1, hp > 0 ? palette.green : palette.red, 0.58);
|
||||||
|
this.trackReportFormationReview(this.add.text(chipX + 7, rosterY + 29, `${roleLabel} ${this.compactText(reportUnit?.name ?? this.unitName(unitId), 7)}`, this.textStyle(10, '#f2e3bf', true)))
|
||||||
|
.setDepth(depth + 3);
|
||||||
|
this.trackReportFormationReview(this.add.text(chipX + 7, rosterY + 45, `HP ${hp}/${maxHp}`, this.textStyle(9, hp > 0 ? '#a8ffd0' : '#ffb6a6', true)))
|
||||||
|
.setDepth(depth + 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
const presetTitleY = y + 337;
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 18, presetTitleY, '이 전투 편성을 편성책에 반영', this.textStyle(13, '#f2e3bf', true)))
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 218, presetTitleY + 2, '명단·역할만 저장 · 장비·보급 제외', this.textStyle(10, '#9fb0bf', true)))
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
const presets = this.sortieFormationPresets();
|
||||||
|
const sourcePresetIds = this.reportFormationSourcePresetIds(review.snapshot);
|
||||||
|
const presetButtons: ReportFormationReviewView['presetButtons'] = {};
|
||||||
|
const presetGap = 10;
|
||||||
|
const presetWidth = Math.floor((width - 36 - presetGap * 2) / 3);
|
||||||
|
sortiePresetDefinitions.forEach((definition, index) => {
|
||||||
|
const preset = presets[definition.id];
|
||||||
|
const matching = sourcePresetIds.includes(definition.id);
|
||||||
|
const confirming = this.reportFormationPresetOverwriteConfirmId === definition.id;
|
||||||
|
const cardX = x + 18 + index * (presetWidth + presetGap);
|
||||||
|
const label = matching
|
||||||
|
? `${definition.label} · 현재와 동일`
|
||||||
|
: preset
|
||||||
|
? `${definition.label} · 저장 ${preset.selectedUnitIds.length}명`
|
||||||
|
: `${definition.label} · 비어 있음`;
|
||||||
|
const actionLabel = matching ? '저장됨' : confirming ? '갱신 확정' : preset ? '갱신' : '저장';
|
||||||
|
presetButtons[definition.id] = this.addReportFormationReviewButton(
|
||||||
|
`${label} ${actionLabel}`,
|
||||||
|
cardX,
|
||||||
|
presetTitleY + 24,
|
||||||
|
presetWidth,
|
||||||
|
42,
|
||||||
|
!matching,
|
||||||
|
confirming || matching,
|
||||||
|
() => this.requestReportFormationPresetUpdate(definition.id),
|
||||||
|
depth + 3,
|
||||||
|
definition.accent
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const feedback = this.reportFormationReviewFeedback || '전투 기록을 비교한 뒤 저장할 편성책을 선택하십시오.';
|
||||||
|
const feedbackColor = this.reportFormationPresetOverwriteConfirmId
|
||||||
|
? '#ffdf7b'
|
||||||
|
: this.reportFormationReviewFeedback
|
||||||
|
? '#a8ffd0'
|
||||||
|
: '#9fb0bf';
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 18, y + height - 3, this.compactText(feedback, 78), this.textStyle(10, feedbackColor, true)))
|
||||||
|
.setOrigin(0, 1)
|
||||||
|
.setDepth(depth + 2);
|
||||||
|
this.reportFormationReviewView = { closeButton, presetButtons };
|
||||||
|
}
|
||||||
|
|
||||||
|
private hideReportFormationReview(resetFeedback = true) {
|
||||||
|
this.reportFormationReviewObjects.forEach((object) => object.destroy());
|
||||||
|
this.reportFormationReviewObjects = [];
|
||||||
|
this.reportFormationReviewVisible = false;
|
||||||
|
this.reportFormationPresetOverwriteConfirmId = undefined;
|
||||||
|
this.reportFormationReviewView = undefined;
|
||||||
|
if (resetFeedback) {
|
||||||
|
this.reportFormationReviewFeedback = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestReportFormationPresetUpdate(presetId: CampaignSortiePresetId) {
|
||||||
|
const review = this.reportFormationReview();
|
||||||
|
if (!review) {
|
||||||
|
this.hideReportFormationReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presets = this.sortieFormationPresets();
|
||||||
|
const existing = presets[presetId];
|
||||||
|
const definition = this.sortiePresetDefinition(presetId);
|
||||||
|
if (sortiePerformanceMatchesPreset(review.snapshot, existing)) {
|
||||||
|
this.reportFormationReviewFeedback = `${definition.label} 편성책이 이미 이 전투 명단·역할과 같습니다.`;
|
||||||
|
this.renderReportFormationReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing && this.reportFormationPresetOverwriteConfirmId !== presetId) {
|
||||||
|
this.reportFormationPresetOverwriteConfirmId = presetId;
|
||||||
|
this.reportFormationReviewFeedback = `${definition.label} 편성책을 갱신하려면 한 번 더 누르십시오.`;
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.renderReportFormationReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaign = getCampaignState();
|
||||||
|
campaign.sortieFormationPresets = {
|
||||||
|
...campaign.sortieFormationPresets,
|
||||||
|
[presetId]: {
|
||||||
|
selectedUnitIds: [...review.snapshot.selectedUnitIds],
|
||||||
|
formationAssignments: { ...review.snapshot.formationAssignments }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.campaign = saveCampaignState(campaign);
|
||||||
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||||
|
this.reportFormationPresetOverwriteConfirmId = undefined;
|
||||||
|
this.reportFormationReviewFeedback = `${definition.label} 편성책 갱신 완료 · 장비·보급은 저장하지 않았습니다.`;
|
||||||
|
soundDirector.playSelect();
|
||||||
|
this.renderReportFormationReview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderReportFormationMetric(label: string, value: string, x: number, y: number, width: number, depth: number) {
|
||||||
|
const background = this.trackReportFormationReview(this.add.rectangle(x, y, width, 48, 0x111c26, 0.96));
|
||||||
|
background.setOrigin(0);
|
||||||
|
background.setDepth(depth);
|
||||||
|
background.setStrokeStyle(1, palette.gold, 0.38);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 10, y + 7, label, this.textStyle(10, '#9fb0bf', true))).setDepth(depth + 1);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + width - 10, y + 25, this.compactText(value, 24), this.textStyle(12, '#f2e3bf', true)))
|
||||||
|
.setOrigin(1, 0)
|
||||||
|
.setDepth(depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderReportFormationAnalysisPanel(
|
||||||
|
title: string,
|
||||||
|
lines: string[],
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
accent: number,
|
||||||
|
depth: number
|
||||||
|
) {
|
||||||
|
const background = this.trackReportFormationReview(this.add.rectangle(x, y, width, height, 0x101820, 0.96));
|
||||||
|
background.setOrigin(0);
|
||||||
|
background.setDepth(depth);
|
||||||
|
background.setStrokeStyle(1, accent, 0.5);
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 12, y + 8, title, this.textStyle(12, '#f2e3bf', true))).setDepth(depth + 1);
|
||||||
|
lines.slice(0, 2).forEach((line, index) => {
|
||||||
|
this.trackReportFormationReview(this.add.text(x + 12, y + 30 + index * 18, this.compactText(line, width > 500 ? 72 : 42), this.textStyle(10, index === 0 ? '#d4dce6' : '#ffdf7b', true)))
|
||||||
|
.setDepth(depth + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addReportFormationReviewButton(
|
||||||
|
label: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
enabled: boolean,
|
||||||
|
active: boolean,
|
||||||
|
action: () => void,
|
||||||
|
depth: number,
|
||||||
|
accent: number = palette.gold
|
||||||
|
) {
|
||||||
|
const fill = active ? 0x263a2d : enabled ? 0x1a2630 : 0x111820;
|
||||||
|
const button = this.trackReportFormationReview(this.add.rectangle(x, y, width, height, fill, enabled || active ? 0.98 : 0.6));
|
||||||
|
button.setOrigin(0);
|
||||||
|
button.setDepth(depth);
|
||||||
|
button.setStrokeStyle(1, active ? palette.green : enabled ? accent : 0x53606c, active ? 0.82 : enabled ? 0.62 : 0.3);
|
||||||
|
if (enabled) {
|
||||||
|
button.setInteractive({ useHandCursor: true });
|
||||||
|
button.on('pointerover', () => button.setFillStyle(active ? 0x365044 : 0x283947, 1).setStrokeStyle(1, palette.gold, 0.94));
|
||||||
|
button.on('pointerout', () => button.setFillStyle(fill, 0.98).setStrokeStyle(1, active ? palette.green : accent, active ? 0.82 : 0.62));
|
||||||
|
button.on('pointerdown', action);
|
||||||
|
}
|
||||||
|
this.trackReportFormationReview(this.add.text(x + width / 2, y + height / 2, this.compactText(label, 28), this.textStyle(10, enabled || active ? '#f2e3bf' : '#77818c', true)))
|
||||||
|
.setOrigin(0.5)
|
||||||
|
.setDepth(depth + 1);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trackReportFormationReview<T extends Phaser.GameObjects.GameObject>(object: T) {
|
||||||
|
this.reportFormationReviewObjects.push(object);
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
private renderProgressPanel() {
|
private renderProgressPanel() {
|
||||||
const x = 394;
|
const x = 394;
|
||||||
const y = 120;
|
const y = 120;
|
||||||
@@ -18220,6 +18631,7 @@ export class CampScene extends Phaser.Scene {
|
|||||||
private clearContent() {
|
private clearContent() {
|
||||||
this.contentObjects.forEach((object) => object.destroy());
|
this.contentObjects.forEach((object) => object.destroy());
|
||||||
this.contentObjects = [];
|
this.contentObjects = [];
|
||||||
|
this.reportFormationReviewToggleButton = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortieDeploymentPreviewDebug() {
|
private sortieDeploymentPreviewDebug() {
|
||||||
@@ -18256,9 +18668,66 @@ export class CampScene extends Phaser.Scene {
|
|||||||
const sortieComparisonAction = this.sortieComparisonAction(sortieComparisonPreview);
|
const sortieComparisonAction = this.sortieComparisonAction(sortieComparisonPreview);
|
||||||
const recommendedPresetId = this.recommendedSortiePresetId();
|
const recommendedPresetId = this.recommendedSortiePresetId();
|
||||||
const sortieFormationPresets = this.sortieFormationPresets();
|
const sortieFormationPresets = this.sortieFormationPresets();
|
||||||
|
const reportFormationReview = this.reportFormationReview();
|
||||||
|
const reportFormationSourcePresetIds = reportFormationReview
|
||||||
|
? this.reportFormationSourcePresetIds(reportFormationReview.snapshot)
|
||||||
|
: [];
|
||||||
return {
|
return {
|
||||||
scene: this.scene.key,
|
scene: this.scene.key,
|
||||||
activeTab: this.activeTab,
|
activeTab: this.activeTab,
|
||||||
|
reportFormationEvaluation: reportFormationReview
|
||||||
|
? {
|
||||||
|
available: true,
|
||||||
|
open: this.reportFormationReviewVisible,
|
||||||
|
battleId: this.report?.battleId ?? null,
|
||||||
|
battleTitle: this.report?.battleTitle ?? null,
|
||||||
|
grade: reportFormationReview.grade,
|
||||||
|
score: reportFormationReview.score,
|
||||||
|
headline: reportFormationReview.headline,
|
||||||
|
strengths: [...reportFormationReview.strengths],
|
||||||
|
improvement: reportFormationReview.improvement,
|
||||||
|
objectiveAchieved: reportFormationReview.objectiveAchieved,
|
||||||
|
objectiveTotal: reportFormationReview.objectiveTotal,
|
||||||
|
survivorCount: reportFormationReview.survivorCount,
|
||||||
|
deployedCount: reportFormationReview.deployedCount,
|
||||||
|
recoveryNeededCount: reportFormationReview.recoveryNeededCount,
|
||||||
|
totalDamage: reportFormationReview.totalDamage,
|
||||||
|
totalDamageTaken: reportFormationReview.totalDamageTaken,
|
||||||
|
totalDefeats: reportFormationReview.totalDefeats,
|
||||||
|
totalSupport: reportFormationReview.totalSupport,
|
||||||
|
roleCoverage: reportFormationReview.roleCoverage,
|
||||||
|
activeBondCount: reportFormationReview.activeBondCount,
|
||||||
|
contributionTotals: { ...reportFormationReview.contributionTotals },
|
||||||
|
snapshot: {
|
||||||
|
selectedUnitIds: [...reportFormationReview.snapshot.selectedUnitIds],
|
||||||
|
formationAssignments: { ...reportFormationReview.snapshot.formationAssignments },
|
||||||
|
unitStats: reportFormationReview.snapshot.unitStats.map((stats) => ({ ...stats }))
|
||||||
|
},
|
||||||
|
sourcePresetIds: [...reportFormationSourcePresetIds],
|
||||||
|
overwriteConfirmId: this.reportFormationPresetOverwriteConfirmId ?? null,
|
||||||
|
feedback: this.reportFormationReviewFeedback,
|
||||||
|
toggleButtonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewToggleButton),
|
||||||
|
closeButtonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewView?.closeButton),
|
||||||
|
presetCards: sortiePresetDefinitions.map((definition) => ({
|
||||||
|
id: definition.id,
|
||||||
|
label: definition.label,
|
||||||
|
saved: Boolean(sortieFormationPresets[definition.id]),
|
||||||
|
matching: reportFormationSourcePresetIds.includes(definition.id),
|
||||||
|
actionButtonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewView?.presetButtons[definition.id])
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
available: false,
|
||||||
|
open: false,
|
||||||
|
battleId: this.report?.battleId ?? null,
|
||||||
|
battleTitle: this.report?.battleTitle ?? null,
|
||||||
|
sourcePresetIds: [],
|
||||||
|
overwriteConfirmId: null,
|
||||||
|
feedback: '',
|
||||||
|
toggleButtonBounds: null,
|
||||||
|
closeButtonBounds: null,
|
||||||
|
presetCards: []
|
||||||
|
},
|
||||||
sortieVisible: this.sortieObjects.length > 0,
|
sortieVisible: this.sortieObjects.length > 0,
|
||||||
sortiePrepStep: this.sortiePrepStep,
|
sortiePrepStep: this.sortiePrepStep,
|
||||||
stagedSortiePrep,
|
stagedSortiePrep,
|
||||||
@@ -18462,9 +18931,18 @@ export class CampScene extends Phaser.Scene {
|
|||||||
: null,
|
: null,
|
||||||
report: this.report
|
report: this.report
|
||||||
? {
|
? {
|
||||||
|
battleId: this.report.battleId,
|
||||||
|
battleTitle: this.report.battleTitle,
|
||||||
rewardGold: this.report.rewardGold,
|
rewardGold: this.report.rewardGold,
|
||||||
objectives: this.report.objectives,
|
objectives: this.report.objectives,
|
||||||
campaignRewards: this.report.campaignRewards,
|
campaignRewards: this.report.campaignRewards,
|
||||||
|
sortiePerformance: this.report.sortiePerformance
|
||||||
|
? {
|
||||||
|
selectedUnitIds: [...this.report.sortiePerformance.selectedUnitIds],
|
||||||
|
formationAssignments: { ...this.report.sortiePerformance.formationAssignments },
|
||||||
|
unitStats: this.report.sortiePerformance.unitStats.map((stats) => ({ ...stats }))
|
||||||
|
}
|
||||||
|
: null,
|
||||||
completedCampDialogues: this.report.completedCampDialogues,
|
completedCampDialogues: this.report.completedCampDialogues,
|
||||||
completedCampVisits: this.report.completedCampVisits,
|
completedCampVisits: this.report.completedCampVisits,
|
||||||
bonds: this.report.bonds.map((bond) => ({ id: bond.id, level: bond.level, exp: bond.exp, battleExp: bond.battleExp }))
|
bonds: this.report.bonds.map((bond) => ({ id: bond.id, level: bond.level, exp: bond.exp, battleExp: bond.battleExp }))
|
||||||
|
|||||||
Reference in New Issue
Block a user