feat: show sortie recommendation reasons
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user