feat: show sortie recommendation reasons

This commit is contained in:
2026-07-13 08:10:04 +09:00
parent cd1abbe815
commit 7b58317f67
2 changed files with 398 additions and 5 deletions

View File

@@ -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 &&

View File

@@ -582,6 +582,37 @@ type SortieRecommendationBrowserView = {
cards: Partial<Record<SortieRecommendationPlanId, SortieRecommendationCardView>>;
};
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<Record<CampaignSortieOrderId, Phaser.GameObjects.Rectangle>>;
@@ -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<SortieRecommendationReasonRow>((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<SortieRecommendationReasonState, string> = {
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,