From 7b58317f67c16d2f0f554d815c935651cf649f7c Mon Sep 17 00:00:00 2001 From: Wickedness Date: Mon, 13 Jul 2026 08:10:04 +0900 Subject: [PATCH] feat: show sortie recommendation reasons --- scripts/verify-release-candidate.mjs | 137 ++++++++++++++ src/game/scenes/CampScene.ts | 266 ++++++++++++++++++++++++++- 2 files changed, 398 insertions(+), 5 deletions(-) diff --git a/scripts/verify-release-candidate.mjs b/scripts/verify-release-candidate.mjs index da7759b..00f0b1c 100644 --- a/scripts/verify-release-candidate.mjs +++ b/scripts/verify-release-candidate.mjs @@ -404,6 +404,9 @@ try { if (!damageStrategy) { throw new Error(`Expected ${partner.id} to have a damaging strategy for the pursuit-ready UI fixture.`); } + partner.x = target.x; + partner.y = target.y - 1; + scene.positionUnitView(partner); stageTargeting({ user: partner, action: damageStrategy.command, @@ -3911,6 +3914,140 @@ try { })}` ); + const midRecommendationToggleBounds = midFormationPortraitState?.sortieRecommendationBrowser?.toggleButtonBounds; + const midRecommendationSelectionBefore = { + selectedUnitIds: [...(midFormationPortraitState?.selectedSortieUnitIds ?? [])], + formationAssignments: { ...(midFormationPortraitState?.sortieFormationAssignments ?? {}) }, + itemAssignments: { ...(midFormationPortraitState?.sortieItemAssignments ?? {}) } + }; + assert( + isFiniteBounds(midRecommendationToggleBounds), + `Expected the mid-campaign formation to expose the recommendation browser toggle: ${JSON.stringify(midFormationPortraitState?.sortieRecommendationBrowser)}` + ); + await page.mouse.click( + midRecommendationToggleBounds.x + midRecommendationToggleBounds.width / 2, + midRecommendationToggleBounds.y + midRecommendationToggleBounds.height / 2 + ); + await page.waitForFunction( + () => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.open === true, + undefined, + { timeout: 30000 } + ); + const midRecommendationOpenState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + const midRecommendationTarget = midRecommendationOpenState?.sortieRecommendationBrowser?.cards?.find( + (card) => card.applicable && !card.current && + card.selectedUnitIds.some((unitId) => !midRecommendationSelectionBefore.selectedUnitIds.includes(unitId)) && + midRecommendationSelectionBefore.selectedUnitIds.some((unitId) => !card.selectedUnitIds.includes(unitId)) && + isFiniteBounds(card.backgroundBounds) && isFiniteBounds(card.compareButtonBounds) + ); + assert( + midRecommendationTarget && isFiniteBounds(midRecommendationOpenState?.sortieRecommendationBrowser?.closeButtonBounds), + `Expected an applicable recommendation card that differs from the current formation: ${JSON.stringify(midRecommendationOpenState?.sortieRecommendationBrowser)}` + ); + await page.mouse.move( + midRecommendationTarget.backgroundBounds.x + midRecommendationTarget.backgroundBounds.width / 2, + midRecommendationTarget.backgroundBounds.y + midRecommendationTarget.backgroundBounds.height / 2 + ); + await page.waitForFunction( + (planId) => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.reasonPanel?.planId === planId, + midRecommendationTarget.id, + { timeout: 30000 } + ); + const midRecommendationHoverState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + const midRecommendationHoverBrowser = midRecommendationHoverState?.sortieRecommendationBrowser; + const midRecommendationReasonPanel = midRecommendationHoverBrowser?.reasonPanel; + const midRecommendationReasonRows = midRecommendationReasonPanel?.rows ?? []; + const midRecommendationRequiredRow = midRecommendationReasonRows.find((row) => row.unitId === 'zhuge-liang'); + const midRecommendationIncomingIds = midRecommendationTarget.selectedUnitIds.filter( + (unitId) => !midRecommendationSelectionBefore.selectedUnitIds.includes(unitId) + ); + const midRecommendationOutgoingIds = midRecommendationSelectionBefore.selectedUnitIds.filter( + (unitId) => !midRecommendationTarget.selectedUnitIds.includes(unitId) + ); + const midRecommendationOfficerNames = Object.fromEntries( + (midRecommendationHoverState?.sortiePortraitRoster ?? []).map((unit) => [unit.id, unit.name]) + ); + const midRecommendationFirstOutgoingName = midRecommendationOfficerNames[midRecommendationOutgoingIds[0]]; + assert( + midRecommendationHoverBrowser?.hoveredPlanId === midRecommendationTarget.id && + midRecommendationReasonPanel?.emptyVisible === false && + sameJsonValue(midRecommendationReasonRows.map((row) => row.unitId), midRecommendationTarget.selectedUnitIds) && + midRecommendationReasonRows.every( + (row) => row.fullReason === midRecommendationTarget.unitReasons[row.unitId] && + isFiniteBounds(row.bounds) && boundsInside(row.bounds, midRecommendationReasonPanel.bounds) && + isFiniteBounds(row.memberBounds) && boundsInside(row.memberBounds, row.bounds) && + isFiniteBounds(row.reasonBounds) && boundsInside(row.reasonBounds, row.bounds) + ) && + midRecommendationIncomingIds.every( + (unitId) => midRecommendationReasonRows.find((row) => row.unitId === unitId)?.state === 'incoming' + ) && + midRecommendationIncomingIds.length > 0 && + midRecommendationOutgoingIds.length > 0 && + midRecommendationFirstOutgoingName && + midRecommendationReasonPanel.changeSummary.includes(`OUT ${midRecommendationFirstOutgoingName}`) && + (midRecommendationRequiredRow?.state === 'required' || midRecommendationRequiredRow?.state === 'required-role') && + midRecommendationRequiredRow.fullReason === '이번 전투의 필수 출전 무장입니다. · 고유 동남풍 화계' && + midRecommendationRequiredRow.reason.startsWith('고유 동남풍 화계') && + sameJsonValue( + { + selectedUnitIds: midRecommendationHoverState?.selectedSortieUnitIds, + formationAssignments: midRecommendationHoverState?.sortieFormationAssignments, + itemAssignments: midRecommendationHoverState?.sortieItemAssignments + }, + midRecommendationSelectionBefore + ), + `Expected recommendation hover to render every source-backed officer reason inside the FHD panel without mutating formation: ${JSON.stringify({ + target: midRecommendationTarget, + browser: midRecommendationHoverBrowser, + before: midRecommendationSelectionBefore, + after: { + selectedUnitIds: midRecommendationHoverState?.selectedSortieUnitIds, + formationAssignments: midRecommendationHoverState?.sortieFormationAssignments, + itemAssignments: midRecommendationHoverState?.sortieItemAssignments + } + })}` + ); + + await page.mouse.click( + midRecommendationTarget.compareButtonBounds.x + midRecommendationTarget.compareButtonBounds.width / 2, + midRecommendationTarget.compareButtonBounds.y + midRecommendationTarget.compareButtonBounds.height / 2 + ); + await page.mouse.move(40, 40); + await page.waitForFunction( + (planId) => { + const state = window.__HEROS_DEBUG__?.camp(); + const browser = state?.sortieRecommendationBrowser; + return browser?.pinnedPlanId === planId && browser?.hoveredPlanId === null && browser?.reasonPanel?.planId === planId && + state?.sortieComparisonPreview?.source === 'pinned-recommendation' && + state?.sortieComparisonAction?.kind === 'confirm-recommendation'; + }, + midRecommendationTarget.id, + { timeout: 30000 } + ); + const midRecommendationPinnedState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp()); + assert( + midRecommendationPinnedState?.sortieRecommendationBrowser?.reasonPanel?.rows?.length === midRecommendationTarget.selectedUnitIds.length && + sameJsonValue( + { + selectedUnitIds: midRecommendationPinnedState?.selectedSortieUnitIds, + formationAssignments: midRecommendationPinnedState?.sortieFormationAssignments, + itemAssignments: midRecommendationPinnedState?.sortieItemAssignments + }, + midRecommendationSelectionBefore + ), + `Expected a pinned recommendation to preserve its officer reasons without applying the formation: ${JSON.stringify(midRecommendationPinnedState?.sortieRecommendationBrowser)}` + ); + const midRecommendationCloseBounds = midRecommendationPinnedState.sortieRecommendationBrowser.closeButtonBounds; + await page.mouse.click( + midRecommendationCloseBounds.x + midRecommendationCloseBounds.width / 2, + midRecommendationCloseBounds.y + midRecommendationCloseBounds.height / 2 + ); + await page.waitForFunction( + () => window.__HEROS_DEBUG__?.camp()?.sortieRecommendationBrowser?.open === false, + undefined, + { timeout: 30000 } + ); + const midCoreSelectorToggle = midFormationPortraitState?.sortiePursuitPreview; assert( midCoreSelectorToggle?.selectorOpen === false && diff --git a/src/game/scenes/CampScene.ts b/src/game/scenes/CampScene.ts index 802f4ae..a25f2ef 100644 --- a/src/game/scenes/CampScene.ts +++ b/src/game/scenes/CampScene.ts @@ -582,6 +582,37 @@ type SortieRecommendationBrowserView = { cards: Partial>; }; +type SortieRecommendationReasonState = 'required' | 'required-role' | 'incoming' | 'role' | 'retained'; + +type SortieRecommendationReasonRow = { + unitId: string; + name: string; + roleLabel: string; + previousRoleLabel?: string; + state: SortieRecommendationReasonState; + fullReason: string; + displayReason: string; +}; + +type SortieRecommendationReasonRowView = { + unitId?: string; + state?: SortieRecommendationReasonState; + fullReason?: string; + background: Phaser.GameObjects.Rectangle; + member: Phaser.GameObjects.Text; + reason: Phaser.GameObjects.Text; +}; + +type SortieRecommendationReasonPanelView = { + planId?: SortieRecommendationPlanId; + background: Phaser.GameObjects.Rectangle; + title: Phaser.GameObjects.Text; + count: Phaser.GameObjects.Text; + changeSummary: Phaser.GameObjects.Text; + emptyText: Phaser.GameObjects.Text; + rows: SortieRecommendationReasonRowView[]; +}; + type SortieOrderBrowserView = { closeButton: Phaser.GameObjects.Rectangle; cards: Partial>; @@ -11179,6 +11210,7 @@ export class CampScene extends Phaser.Scene { private sortieRecommendationUndoState?: SortieRecommendationUndoState; private sortieRecommendationPlanCache?: { key: string; plans: SortieRecommendationPlan[] }; private sortieRecommendationBrowserView?: SortieRecommendationBrowserView; + private sortieRecommendationReasonPanelView?: SortieRecommendationReasonPanelView; private sortieRecommendationToggleButton?: Phaser.GameObjects.Rectangle; private sortieHoveredPresetId?: CampaignSortiePresetId; private sortiePinnedPresetId?: CampaignSortiePresetId; @@ -11235,6 +11267,7 @@ export class CampScene extends Phaser.Scene { this.sortieRecommendationUndoState = undefined; this.sortieRecommendationPlanCache = undefined; this.sortieRecommendationBrowserView = undefined; + this.sortieRecommendationReasonPanelView = undefined; this.sortieRecommendationToggleButton = undefined; this.sortieHoveredPresetId = undefined; this.sortiePinnedPresetId = undefined; @@ -13650,7 +13683,7 @@ export class CampScene extends Phaser.Scene { this.add.text( x + 18, y + height - 24, - '카드 호버 → 오른쪽 장단점 비교 → 비교 고정 → 적용 확정', + '카드 호버 → 오른쪽 무장별 근거·장단점 → 비교 고정 → 적용 확정', this.textStyle(10, '#9fb0bf', true) ) ).setDepth(depth + 1); @@ -14076,6 +14109,7 @@ export class CampScene extends Phaser.Scene { const preview = this.sortieCoreResonanceSelectorOpen ? undefined : this.sortieFormationComparisonPreview(); const action = this.sortieCoreResonanceSelectorOpen ? undefined : this.sortieComparisonAction(preview); + this.refreshSortieRecommendationReasonPanel(preview); this.refreshSortieComparisonActionButton(view, action); const isUndoAction = action?.kind === 'undo-swap' || action?.kind === 'undo-recommendation' || action?.kind === 'undo-preset'; const current = preview?.current ?? this.sortieSynergySnapshot(); @@ -14267,7 +14301,13 @@ export class CampScene extends Phaser.Scene { const recommendation = this.sortieRecommendationProjection(preview.recommendationId); const pursuitDelta = pursuitChanges.projectedPairs.length - pursuitChanges.currentPairs.length; const strategyDelta = projectedStrategyCoverage.count - currentStrategyCoverage.count; - const highlightLine = recommendation?.plan.highlights[0] || preview.detail; + const reasonRows = recommendation ? this.sortieRecommendationReasonRows(recommendation) : []; + const representativeReason = reasonRows.find((row) => row.state === 'incoming') ?? + reasonRows.find((row) => row.state === 'required' || row.state === 'required-role') ?? + reasonRows.find((row) => row.state === 'role') ?? reasonRows[0]; + const highlightLine = representativeReason + ? `${representativeReason.name} · ${representativeReason.displayReason}` + : recommendation?.plan.highlights[0] || preview.detail; const tradeoffLine = recommendation?.plan.tradeoff || '현재 편성과 비교해 역할과 보급을 다시 확인하십시오.'; view.impact .setText(this.compactText(highlightLine, 42)) @@ -15247,7 +15287,14 @@ export class CampScene extends Phaser.Scene { const canUseSortieOrder = this.usesStagedSortiePrep() && this.sortiePrepStep === 'formation' && hasBattle; const canRecommend = this.canApplyRecommendedSortiePlan(scenario) && this.sortieFormationPanelMode === 'roster'; const canBrowseRecommendations = this.canApplyRecommendedSortiePlan(scenario); - this.trackSortie(this.add.text(x + 18, y + 14, hasBattle ? '출전 슬롯' : '동행 명단', this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1); + this.trackSortie( + this.add.text( + x + 18, + y + 14, + this.sortieRecommendationBrowserOpen ? '추천 무장 근거' : hasBattle ? '출전 슬롯' : '동행 명단', + this.textStyle(18, '#f2e3bf', true) + ) + ).setDepth(depth + 1); if (canUsePresetBook && canUseSortieOrder) { this.sortieRecommendationToggleButton = this.renderSortiePanelButton( '추천안', @@ -15294,13 +15341,20 @@ export class CampScene extends Phaser.Scene { this.add.text( x + 18, y + 38, - hasBattle + this.sortieRecommendationBrowserOpen + ? '왼쪽 추천 카드에 마우스를 올리면 무장별 선택 이유가 표시됩니다.' + : hasBattle ? this.compactText(`군령 ${selectedOrder?.label ?? '미선택'} · ${plan.selectedCount}/${plan.maxCount}명 · 전법 ${strategyCoverage.count}/${strategyCoverage.total} · 추격 ${pursuit.activePairs.length}조 · 지형 ${plan.terrainGrade} · ${this.sortieRecommendedClassLine()}`, 54) : `${plan.selectedCount}/${plan.maxCount}명 · 전법 ${strategyCoverage.count}/${strategyCoverage.total} · 군영 의정 · ${this.sortieRecommendedClassLine()}`, this.textStyle(11, '#d8b15f', true) ) ).setDepth(depth + 1); + if (this.sortieRecommendationBrowserOpen) { + this.renderSortieRecommendationReasonPanel(x + 16, y + 58, width - 32, height - 66, depth + 1); + return; + } + this.renderSortieDeploymentMap(x + 16, y + 58, width - 32, 62, depth + 1); this.renderSortieSlotCards(x + 16, y + 126, width - 32, 104, plan, depth + 1); const planFeedback = this.sortiePlanFeedback || plan.warningLine; @@ -15316,6 +15370,185 @@ export class CampScene extends Phaser.Scene { this.renderSortieRoleQuickButtons(x + 16, y + 256, width - 32, 22, depth + 1); } + private renderSortieRecommendationReasonPanel(x: number, y: number, width: number, height: number, depth: number) { + const background = this.trackSortie(this.add.rectangle(x, y, width, height, 0x111922, 0.94)); + background.setOrigin(0); + background.setDepth(depth); + background.setStrokeStyle(1, palette.blue, 0.42); + + const title = this.trackSortie(this.add.text(x + 10, y + 8, '추천 카드 선택 대기', this.textStyle(11, '#d4dce6', true))); + title.setDepth(depth + 1); + const count = this.trackSortie(this.add.text(x + width - 10, y + 9, '0명', this.textStyle(9, '#9fb0bf', true))); + count.setOrigin(1, 0); + count.setDepth(depth + 1); + const changeSummary = this.trackSortie( + this.add.text(x + 10, y + 22, 'IN/OUT 및 역할 변화가 여기에 표시됩니다.', this.textStyle(8, '#77818c', true)) + ); + changeSummary.setDepth(depth + 1); + const emptyText = this.trackSortie( + this.add.text(x + width / 2, y + height / 2 + 6, '추천 카드에 마우스를 올려 선택 근거를 확인하십시오.', this.textStyle(11, '#77818c', true)) + ); + emptyText.setOrigin(0.5); + emptyText.setDepth(depth + 1); + + const rowHeight = 24; + const rowGap = 2; + const rowTop = y + 38; + const rows = Array.from({ length: 7 }, (_, index): SortieRecommendationReasonRowView => { + const rowY = rowTop + index * (rowHeight + rowGap); + const rowBackground = this.trackSortie(this.add.rectangle(x + 6, rowY, width - 12, rowHeight, 0x151f2a, 0.92)); + rowBackground.setOrigin(0); + rowBackground.setDepth(depth + 1); + rowBackground.setStrokeStyle(1, 0x53606c, 0.3); + rowBackground.setVisible(false); + + const member = this.trackSortie(this.add.text(x + 14, rowY + 6, '', this.textStyle(9, '#c8d2dd', true))); + member.setDepth(depth + 2); + member.setVisible(false); + const reason = this.trackSortie(this.add.text(x + 132, rowY + 6, '', this.textStyle(9, '#9fb0bf', true))); + reason.setDepth(depth + 2); + reason.setVisible(false); + return { background: rowBackground, member, reason }; + }); + + this.sortieRecommendationReasonPanelView = { + background, + title, + count, + changeSummary, + emptyText, + rows + }; + } + + private sortieRecommendationReasonDisplay(reason: string) { + const signatureDelimiter = ' · 고유 '; + const signatureIndex = reason.lastIndexOf(signatureDelimiter); + if (signatureIndex <= 0) { + return reason; + } + const baseReason = reason.slice(0, signatureIndex); + const signatureName = reason.slice(signatureIndex + signatureDelimiter.length); + return `고유 ${signatureName} · ${baseReason}`; + } + + private sortieRecommendationReasonRows(projection: SortieRecommendationProjection): SortieRecommendationReasonRow[] { + const currentIds = new Set(this.selectedSortieUnitIds); + const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit])); + return projection.selectedUnitIds.flatMap((unitId) => { + const unit = rosterById.get(unitId); + if (!unit) { + return []; + } + const projectedRole = projection.formationAssignments[unitId] ?? this.sortieFormationRole(unit); + const currentRole = currentIds.has(unitId) ? this.sortieFormationRole(unit) : undefined; + const roleChanged = Boolean(currentRole && currentRole !== projectedRole); + const state: SortieRecommendationReasonState = this.isRequiredSortieUnit(unitId) + ? roleChanged ? 'required-role' : 'required' + : !currentIds.has(unitId) + ? 'incoming' + : roleChanged + ? 'role' + : 'retained'; + const fullReason = projection.plan.unitReasons[unitId] ?? '추천 편성의 역할 균형을 보완합니다.'; + return [{ + unitId, + name: unit.name, + roleLabel: this.sortieFormationRoleLabel(projectedRole, true), + previousRoleLabel: roleChanged && currentRole ? this.sortieFormationRoleLabel(currentRole, true) : undefined, + state, + fullReason, + displayReason: this.sortieRecommendationReasonDisplay(fullReason) + }]; + }); + } + + private refreshSortieRecommendationReasonPanel(preview?: SortieFormationComparisonPreview) { + const view = this.sortieRecommendationReasonPanelView; + if (!view) { + return; + } + const recommendationId = preview?.mode === 'recommendation' ? preview.recommendationId : undefined; + const projection = recommendationId ? this.sortieRecommendationProjection(recommendationId) : undefined; + const reasonRows = projection ? this.sortieRecommendationReasonRows(projection) : []; + const projectedIds = new Set(projection?.selectedUnitIds ?? []); + const outgoingNames = projection + ? this.selectedSortieUnitIds.filter((unitId) => !projectedIds.has(unitId)).map((unitId) => this.unitName(unitId)) + : []; + const incomingNames = reasonRows.filter((row) => row.state === 'incoming').map((row) => row.name); + const roleChangeNames = reasonRows + .filter((row) => row.state === 'role' || row.state === 'required-role') + .map((row) => row.name); + const changeParts = [ + outgoingNames.length > 0 ? `OUT ${outgoingNames.join('·')}` : '', + incomingNames.length > 0 ? `IN ${incomingNames.join('·')}` : '', + roleChangeNames.length > 0 ? `역할 ${roleChangeNames.join('·')}` : '' + ].filter(Boolean); + + view.planId = projection?.recommendationId; + view.title.setText(projection ? `${projection.label} 추천 명단` : '추천 카드 선택 대기'); + view.title.setColor(projection ? '#f2e3bf' : '#d4dce6'); + view.count.setText(projection ? `${reasonRows.length}명 · 무장별 근거` : '0명'); + view.changeSummary + .setText(projection ? this.compactText(changeParts.join(' · ') || '현재 명단·역할 유지', 54) : 'IN/OUT 및 역할 변화가 여기에 표시됩니다.') + .setColor(outgoingNames.length > 0 ? '#ffdf7b' : incomingNames.length > 0 || roleChangeNames.length > 0 ? '#a8ffd0' : '#77818c'); + view.emptyText.setVisible(reasonRows.length === 0); + + const stateLabel: Record = { + required: '필수', + 'required-role': '필수·역할', + incoming: 'IN', + role: '역할', + retained: '유지' + }; + view.rows.forEach((rowView, index) => { + const row = reasonRows[index]; + if (!row) { + rowView.unitId = undefined; + rowView.state = undefined; + rowView.fullReason = undefined; + rowView.background.setVisible(false); + rowView.member.setVisible(false); + rowView.reason.setVisible(false); + return; + } + + const tone = row.state === 'required' || row.state === 'required-role' + ? palette.gold + : row.state === 'incoming' + ? palette.green + : row.state === 'role' + ? palette.blue + : 0x53606c; + const fill = row.state === 'required' || row.state === 'required-role' + ? 0x2a2418 + : row.state === 'incoming' + ? 0x172a22 + : row.state === 'role' + ? 0x172432 + : 0x151f2a; + const memberColor = row.state === 'required' || row.state === 'required-role' + ? '#ffdf7b' + : row.state === 'incoming' + ? '#a8ffd0' + : row.state === 'role' + ? '#d8ecff' + : '#c8d2dd'; + rowView.unitId = row.unitId; + rowView.state = row.state; + rowView.fullReason = row.fullReason; + rowView.background.setVisible(true).setFillStyle(fill, 0.94).setStrokeStyle(1, tone, 0.52); + rowView.member + .setVisible(true) + .setText(`${stateLabel[row.state]} ${row.name}·${row.previousRoleLabel ? `${row.previousRoleLabel}→${row.roleLabel}` : row.roleLabel}`) + .setColor(memberColor); + rowView.reason + .setVisible(true) + .setText(this.compactText(row.displayReason, 34)) + .setColor(row.state === 'incoming' || row.state === 'required' || row.state === 'required-role' ? '#d4dce6' : '#9fb0bf'); + }); + } + private renderSortieSlotCards(x: number, y: number, width: number, height: number, plan: SortiePlanSummary, depth: number) { const selectedUnits = this.selectedSortieUnits(); const maxCount = plan.maxCount; @@ -18325,6 +18558,7 @@ export class CampScene extends Phaser.Scene { this.sortieCoreResonanceToggleButton = undefined; this.sortieCoreResonanceToggleLabel = undefined; this.sortieRecommendationBrowserView = undefined; + this.sortieRecommendationReasonPanelView = undefined; this.sortieRecommendationToggleButton = undefined; this.sortiePresetBrowserView = undefined; this.sortiePresetToggleButton = undefined; @@ -22118,7 +22352,29 @@ export class CampScene extends Phaser.Scene { backgroundBounds: this.sortieObjectBoundsDebug(view?.background), compareButtonBounds: this.sortieObjectBoundsDebug(view?.compareButton) }; - }) + }), + reasonPanel: this.sortieRecommendationReasonPanelView + ? { + planId: this.sortieRecommendationReasonPanelView.planId ?? null, + bounds: this.sortieObjectBoundsDebug(this.sortieRecommendationReasonPanelView.background), + title: this.sortieRecommendationReasonPanelView.title.text, + count: this.sortieRecommendationReasonPanelView.count.text, + changeSummary: this.sortieRecommendationReasonPanelView.changeSummary.text, + emptyVisible: this.sortieRecommendationReasonPanelView.emptyText.visible, + rows: this.sortieRecommendationReasonPanelView.rows + .filter((row) => Boolean(row.unitId)) + .map((row) => ({ + unitId: row.unitId, + state: row.state ?? null, + fullReason: row.fullReason ?? null, + member: row.member.text, + reason: row.reason.text, + bounds: this.sortieObjectBoundsDebug(row.background), + memberBounds: this.sortieTextBoundsDebug(row.member), + reasonBounds: this.sortieTextBoundsDebug(row.reason) + })) + } + : null }, selectedUnitId: this.selectedUnitId, selectedDialogueId: this.selectedDialogueId,