feat: add tactical sortie recommendations
This commit is contained in:
@@ -65,6 +65,11 @@ import {
|
||||
type CampaignSortieOrderResultSnapshot,
|
||||
type SortieOrderHistorySummary
|
||||
} from '../data/sortieOrders';
|
||||
import {
|
||||
buildSortieRecommendationPlans,
|
||||
type SortieRecommendationPlan,
|
||||
type SortieRecommendationPlanId
|
||||
} from '../data/sortieRecommendations';
|
||||
import {
|
||||
caoBreakRecruitBonds,
|
||||
caoBreakRecruitUnits,
|
||||
@@ -283,7 +288,7 @@ type SortieChecklistItem = {
|
||||
};
|
||||
|
||||
type SortiePrepStep = 'briefing' | 'formation' | 'loadout';
|
||||
type SortieFormationPanelMode = 'roster' | 'preset' | 'order';
|
||||
type SortieFormationPanelMode = 'roster' | 'recommendation' | 'preset' | 'order';
|
||||
|
||||
const firstSortiePrepSteps: { id: SortiePrepStep; number: string; label: string; hint: string }[] = [
|
||||
{ id: 'briefing', number: '01', label: '전장 파악', hint: '목표와 위험 확인' },
|
||||
@@ -424,9 +429,10 @@ type SortieComparisonMetric = {
|
||||
};
|
||||
|
||||
type SortieFormationComparisonPreview = {
|
||||
source: 'focus' | 'hover-add' | 'hover-swap' | 'pinned-swap' | 'hover-blocked' | 'hover-preset' | 'pinned-preset';
|
||||
mode: SortieUnitSynergyPreview['mode'] | 'swap' | 'preset';
|
||||
source: 'focus' | 'hover-add' | 'hover-swap' | 'pinned-swap' | 'hover-blocked' | 'hover-recommendation' | 'pinned-recommendation' | 'hover-preset' | 'pinned-preset';
|
||||
mode: SortieUnitSynergyPreview['mode'] | 'swap' | 'recommendation' | 'preset';
|
||||
unitId: string;
|
||||
recommendationId?: SortieRecommendationPlanId;
|
||||
presetId?: CampaignSortiePresetId;
|
||||
incomingUnitId: string | null;
|
||||
incomingUnitName: string | null;
|
||||
@@ -524,6 +530,25 @@ type SortiePresetProjection = {
|
||||
omittedUnitNames: string[];
|
||||
};
|
||||
|
||||
type SortieRecommendationProjection = {
|
||||
recommendationId: SortieRecommendationPlanId;
|
||||
plan: SortieRecommendationPlan;
|
||||
label: string;
|
||||
summary: string;
|
||||
accent: number;
|
||||
selectedUnitIds: string[];
|
||||
formationAssignments: SortieFormationAssignments;
|
||||
itemAssignments: CampaignSortieItemAssignments;
|
||||
reasonLines: string[];
|
||||
};
|
||||
|
||||
type SortieRecommendationUndoState = {
|
||||
recommendationId: SortieRecommendationPlanId;
|
||||
beforeCoreBondId?: string;
|
||||
before: SortieConfigurationSnapshot;
|
||||
applied: SortieConfigurationSnapshot;
|
||||
};
|
||||
|
||||
type SortiePresetUndoState = {
|
||||
presetId: CampaignSortiePresetId;
|
||||
before: SortieConfigurationSnapshot;
|
||||
@@ -541,6 +566,17 @@ type SortiePresetBrowserView = {
|
||||
cards: Partial<Record<CampaignSortiePresetId, SortiePresetCardView>>;
|
||||
};
|
||||
|
||||
type SortieRecommendationCardView = {
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
compareButton: Phaser.GameObjects.Rectangle;
|
||||
};
|
||||
|
||||
type SortieRecommendationBrowserView = {
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
closeButton: Phaser.GameObjects.Rectangle;
|
||||
cards: Partial<Record<SortieRecommendationPlanId, SortieRecommendationCardView>>;
|
||||
};
|
||||
|
||||
type SortieOrderBrowserView = {
|
||||
closeButton: Phaser.GameObjects.Rectangle;
|
||||
cards: Partial<Record<CampaignSortieOrderId, Phaser.GameObjects.Rectangle>>;
|
||||
@@ -610,10 +646,11 @@ type ReportFormationHistoryView = {
|
||||
};
|
||||
|
||||
type SortieComparisonAction = {
|
||||
kind: 'confirm-swap' | 'undo-swap' | 'confirm-preset' | 'undo-preset';
|
||||
kind: 'confirm-swap' | 'undo-swap' | 'confirm-recommendation' | 'undo-recommendation' | 'confirm-preset' | 'undo-preset';
|
||||
label: string;
|
||||
outgoingUnitId?: string;
|
||||
incomingUnitId?: string;
|
||||
recommendationId?: SortieRecommendationPlanId;
|
||||
presetId?: CampaignSortiePresetId;
|
||||
};
|
||||
|
||||
@@ -927,6 +964,17 @@ const sortiePresetDefinitions: {
|
||||
{ id: 'siege', label: '공성', summary: '전열·원거리 중심', accent: 0xc87552 }
|
||||
];
|
||||
|
||||
const sortieRecommendationDefinitions: {
|
||||
id: SortieRecommendationPlanId;
|
||||
label: string;
|
||||
summary: string;
|
||||
accent: number;
|
||||
}[] = [
|
||||
{ id: 'order', label: '군령 달성형', summary: '선택 군령의 달성 조건 우선', accent: 0xd8b15f },
|
||||
{ id: 'resonance', label: '공명·추격형', summary: '활성 공명과 추격 연계 우선', accent: 0x8f78cf },
|
||||
{ id: 'terrain-strategy', label: '지형·전법형', summary: '지형 적성과 전법 범위 우선', accent: 0x72a7d8 }
|
||||
];
|
||||
|
||||
const reportFormationHistoryPageSize = 7;
|
||||
const sortiePortraitRosterPageSize = 6;
|
||||
|
||||
@@ -11121,6 +11169,12 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortiePinnedSwapCandidateUnitId?: string;
|
||||
private sortieSwapUndoState?: SortieSwapUndoState;
|
||||
private sortieFormationPanelMode: SortieFormationPanelMode = 'roster';
|
||||
private sortieHoveredRecommendationId?: SortieRecommendationPlanId;
|
||||
private sortiePinnedRecommendationId?: SortieRecommendationPlanId;
|
||||
private sortieRecommendationUndoState?: SortieRecommendationUndoState;
|
||||
private sortieRecommendationPlanCache?: { key: string; plans: SortieRecommendationPlan[] };
|
||||
private sortieRecommendationBrowserView?: SortieRecommendationBrowserView;
|
||||
private sortieRecommendationToggleButton?: Phaser.GameObjects.Rectangle;
|
||||
private sortieHoveredPresetId?: CampaignSortiePresetId;
|
||||
private sortiePinnedPresetId?: CampaignSortiePresetId;
|
||||
private sortiePresetOverwriteConfirmId?: CampaignSortiePresetId;
|
||||
@@ -11171,6 +11225,12 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.sortieFormationPanelMode = 'roster';
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.sortieRecommendationPlanCache = undefined;
|
||||
this.sortieRecommendationBrowserView = undefined;
|
||||
this.sortieRecommendationToggleButton = undefined;
|
||||
this.sortieHoveredPresetId = undefined;
|
||||
this.sortiePinnedPresetId = undefined;
|
||||
this.sortiePresetOverwriteConfirmId = undefined;
|
||||
@@ -12662,6 +12722,8 @@ export class CampScene extends Phaser.Scene {
|
||||
const returnToSortiePrep = this.sortieObjects.length > 0;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
this.sortiePresetUndoState = undefined;
|
||||
this.closeSortiePresetBrowserState();
|
||||
this.hideCampSaveSlotPanel();
|
||||
@@ -13214,6 +13276,8 @@ export class CampScene extends Phaser.Scene {
|
||||
const planWidth = width - rosterWidth - gap;
|
||||
if (this.sortieOrderBrowserOpen) {
|
||||
this.renderCampaignSortieOrderBrowser(x, y, rosterWidth, height, depth);
|
||||
} else if (this.sortieRecommendationBrowserOpen) {
|
||||
this.renderCampaignSortieRecommendationBrowser(x, y, rosterWidth, height, depth);
|
||||
} else if (this.sortiePresetBrowserOpen) {
|
||||
this.renderCampaignSortiePresetBrowser(x, y, rosterWidth, height, depth);
|
||||
} else {
|
||||
@@ -13459,6 +13523,135 @@ export class CampScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
private renderCampaignSortieRecommendationBrowser(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.98));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.68);
|
||||
|
||||
this.trackSortie(this.add.text(x + 18, y + 13, '출전 전략 추천', this.textStyle(19, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 166, y + 18, '군령·공명·지형 기준 3안', this.textStyle(11, '#9fb0bf', true))
|
||||
).setDepth(depth + 1);
|
||||
|
||||
const closeButton = this.trackSortie(this.add.rectangle(x + width - 96, y + 9, 78, 30, 0x18232e, 0.96));
|
||||
closeButton.setOrigin(0);
|
||||
closeButton.setDepth(depth + 2);
|
||||
closeButton.setStrokeStyle(1, palette.blue, 0.58);
|
||||
closeButton.setInteractive({ useHandCursor: true });
|
||||
closeButton.on('pointerover', () => closeButton.setFillStyle(0x243442, 1).setStrokeStyle(1, palette.gold, 0.9));
|
||||
closeButton.on('pointerout', () => closeButton.setFillStyle(0x18232e, 0.96).setStrokeStyle(1, palette.blue, 0.58));
|
||||
closeButton.on('pointerdown', () => this.toggleSortieRecommendationBrowser(false));
|
||||
this.trackSortie(this.add.text(x + width - 57, y + 24, '무장 목록', this.textStyle(11, '#d8ecff', true)))
|
||||
.setOrigin(0.5)
|
||||
.setDepth(depth + 3);
|
||||
|
||||
const cards: SortieRecommendationBrowserView['cards'] = {};
|
||||
const recommendationPlans = new Map(this.sortieRecommendationPlans().map((plan) => [plan.id, plan]));
|
||||
const plans = new Map(this.sortieRecommendationProjections().map((plan) => [plan.recommendationId, plan]));
|
||||
const cardX = x + 18;
|
||||
const cardWidth = width - 36;
|
||||
const cardHeight = 104;
|
||||
const cardGap = 8;
|
||||
sortieRecommendationDefinitions.forEach((definition, index) => {
|
||||
const cardY = y + 50 + index * (cardHeight + cardGap);
|
||||
const recommendationPlan = recommendationPlans.get(definition.id);
|
||||
const plan = plans.get(definition.id);
|
||||
const pending = this.sortiePinnedRecommendationId === definition.id;
|
||||
const hovered = this.sortieHoveredRecommendationId === definition.id;
|
||||
const current = Boolean(plan && this.sortieRecommendationProjectionMatchesCurrent(plan));
|
||||
const cardFill = pending ? 0x263229 : current ? 0x1d3028 : plan ? 0x151f2a : 0x111820;
|
||||
const cardStroke = pending || hovered ? palette.gold : current ? palette.green : plan ? definition.accent : 0x53606c;
|
||||
const card = this.trackSortie(this.add.rectangle(cardX, cardY, cardWidth, cardHeight, cardFill, plan ? 0.98 : 0.82));
|
||||
card.setOrigin(0);
|
||||
card.setDepth(depth + 1);
|
||||
card.setStrokeStyle(pending || hovered ? 2 : 1, cardStroke, pending || hovered ? 0.96 : 0.52);
|
||||
card.setInteractive({ useHandCursor: Boolean(plan) });
|
||||
card.on('pointerover', () => {
|
||||
if (!plan) {
|
||||
return;
|
||||
}
|
||||
this.sortieHoveredRecommendationId = definition.id;
|
||||
card.setFillStyle(0x22302d, 1).setStrokeStyle(2, palette.gold, 0.96);
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
});
|
||||
card.on('pointerout', () => {
|
||||
if (this.sortieHoveredRecommendationId === definition.id) {
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
}
|
||||
card.setFillStyle(cardFill, plan ? 0.98 : 0.82).setStrokeStyle(pending ? 2 : 1, cardStroke, pending ? 0.96 : 0.52);
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
});
|
||||
card.on('pointerdown', () => {
|
||||
if (plan) {
|
||||
this.pinSortieRecommendation(definition.id);
|
||||
}
|
||||
});
|
||||
|
||||
const accent = this.trackSortie(this.add.rectangle(cardX + 4, cardY + 4, 5, cardHeight - 8, definition.accent, plan ? 0.94 : 0.42));
|
||||
accent.setOrigin(0);
|
||||
accent.setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 22, cardY + 11, definition.label, this.textStyle(17, plan ? '#f2e3bf' : '#a6afb9', true))).setDepth(depth + 2);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
cardX + 142,
|
||||
cardY + 16,
|
||||
this.compactText(recommendationPlan?.summary ?? definition.summary, 34),
|
||||
this.textStyle(10, '#9fb0bf', true)
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
const badge = current ? '현재 적용' : pending ? '비교 중' : '';
|
||||
if (badge) {
|
||||
this.trackSortie(this.add.text(cardX + cardWidth - 16, cardY + 13, badge, this.textStyle(10, current ? '#a8ffd0' : '#ffdf7b', true)))
|
||||
.setOrigin(1, 0)
|
||||
.setDepth(depth + 2);
|
||||
}
|
||||
|
||||
const memberLine = plan
|
||||
? this.sortiePresetMemberLine(plan.selectedUnitIds.map((unitId) => this.unitName(unitId)))
|
||||
: recommendationPlan?.blockedReason ?? '추천안을 계산할 수 없습니다.';
|
||||
this.trackSortie(this.add.text(cardX + 22, cardY + 39, this.compactText(memberLine, 54), this.textStyle(12, plan ? '#d4dce6' : '#77818c', Boolean(plan))))
|
||||
.setDepth(depth + 2);
|
||||
|
||||
const snapshot = plan
|
||||
? this.sortieSynergySnapshot(plan.selectedUnitIds, this.nextSortieScenario(), plan.formationAssignments)
|
||||
: undefined;
|
||||
const pursuit = plan
|
||||
? this.sortiePursuitPairs(plan.selectedUnitIds, this.nextSortieScenario(), plan.formationAssignments)
|
||||
: undefined;
|
||||
const strategyCoverage = plan ? this.sortieStrategyCoverage(plan.selectedUnitIds) : undefined;
|
||||
const statusLine = snapshot && pursuit && strategyCoverage
|
||||
? `출전 ${plan?.selectedUnitIds.length ?? 0}명 · 역할 ${snapshot.coveredRoleCount}/3 · 공명 ${snapshot.activeBondCount} · 추격 ${pursuit.activePairs.length} · 전법 ${strategyCoverage.count}/${strategyCoverage.total} · 지형 ${snapshot.terrainGrade}`
|
||||
: recommendationPlan?.blockedReason ?? '필수·출전 제한을 만족하는 추천안이 없습니다.';
|
||||
this.trackSortie(this.add.text(cardX + 22, cardY + 62, this.compactText(statusLine, 65), this.textStyle(10, snapshot ? '#d8b15f' : '#7f8994', true)))
|
||||
.setDepth(depth + 2);
|
||||
|
||||
const compareEnabled = Boolean(plan && !current);
|
||||
const compareButton = this.renderSortiePresetBrowserButton(
|
||||
current ? '적용 중' : pending ? '비교 고정' : '비교',
|
||||
cardX + cardWidth - 94,
|
||||
cardY + 75,
|
||||
78,
|
||||
21,
|
||||
compareEnabled,
|
||||
pending,
|
||||
() => this.pinSortieRecommendation(definition.id),
|
||||
depth + 3
|
||||
);
|
||||
cards[definition.id] = { background: card, compareButton };
|
||||
});
|
||||
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + 18,
|
||||
y + height - 24,
|
||||
'카드 호버 → 오른쪽 장단점 비교 → 비교 고정 → 적용 확정',
|
||||
this.textStyle(10, '#9fb0bf', true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
this.sortieRecommendationBrowserView = { background: bg, closeButton, cards };
|
||||
}
|
||||
|
||||
private renderCampaignSortieOrderBrowser(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.98));
|
||||
bg.setOrigin(0);
|
||||
@@ -13764,7 +13957,7 @@ export class CampScene extends Phaser.Scene {
|
||||
actionButton.on('pointerout', () => {
|
||||
const action = this.sortieComparisonAction();
|
||||
if (actionButton.visible && action) {
|
||||
const undo = action.kind === 'undo-swap' || action.kind === 'undo-preset';
|
||||
const undo = action.kind === 'undo-swap' || action.kind === 'undo-recommendation' || action.kind === 'undo-preset';
|
||||
actionButton
|
||||
.setFillStyle(undo ? 0x243442 : 0x263a2d, 0.98)
|
||||
.setStrokeStyle(1, undo ? palette.blue : palette.green, 0.78);
|
||||
@@ -13879,7 +14072,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const preview = this.sortieCoreResonanceSelectorOpen ? undefined : this.sortieFormationComparisonPreview();
|
||||
const action = this.sortieCoreResonanceSelectorOpen ? undefined : this.sortieComparisonAction(preview);
|
||||
this.refreshSortieComparisonActionButton(view, action);
|
||||
const isUndoAction = action?.kind === 'undo-swap' || action?.kind === 'undo-preset';
|
||||
const isUndoAction = action?.kind === 'undo-swap' || action?.kind === 'undo-recommendation' || action?.kind === 'undo-preset';
|
||||
const current = preview?.current ?? this.sortieSynergySnapshot();
|
||||
const projected = isUndoAction ? current : preview?.projected ?? current;
|
||||
const showMetricTransition = !isUndoAction && Boolean(preview?.projected);
|
||||
@@ -13893,6 +14086,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const strategyCoverageDelta = strategyCoverageComparison.delta;
|
||||
this.setCampaignSortieCoreSelectorVisible(false);
|
||||
const isHoverPreview = preview?.source === 'hover-add' || preview?.source === 'hover-swap' || preview?.source === 'pinned-swap' ||
|
||||
preview?.source === 'hover-recommendation' || preview?.source === 'pinned-recommendation' ||
|
||||
preview?.source === 'hover-preset' || preview?.source === 'pinned-preset';
|
||||
const lostRole = Boolean(comparison?.lostRoles.length);
|
||||
const lostStrategyCoverage = strategyCoverageDelta < 0;
|
||||
@@ -13960,10 +14154,14 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = action?.kind === 'undo-preset' && this.sortiePresetUndoState
|
||||
const title = action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState
|
||||
? `${this.sortieRecommendationDefinition(this.sortieRecommendationUndoState.recommendationId).label} 적용 완료`
|
||||
: action?.kind === 'undo-preset' && this.sortiePresetUndoState
|
||||
? `${this.sortiePresetDefinition(this.sortiePresetUndoState.presetId).label} 적용 완료`
|
||||
: isUndoAction && this.sortieSwapUndoState
|
||||
? `교체 완료 · ${this.sortieSwapUndoState.outgoingUnitName} → ${this.sortieSwapUndoState.incomingUnitName}`
|
||||
: preview.source === 'pinned-recommendation' || preview.source === 'hover-recommendation'
|
||||
? `${this.sortieRecommendationDefinition(preview.recommendationId ?? 'order').label} 비교`
|
||||
: preview.source === 'pinned-preset' || preview.source === 'hover-preset'
|
||||
? `${this.sortiePresetDefinition(preview.presetId ?? 'elite').label} 편성책 비교`
|
||||
: preview.source === 'pinned-swap'
|
||||
@@ -13981,6 +14179,7 @@ export class CampScene extends Phaser.Scene {
|
||||
)
|
||||
);
|
||||
const headlineColor = isUndoAction || preview.source === 'hover-swap' || preview.source === 'pinned-swap' ||
|
||||
preview.source === 'hover-recommendation' || preview.source === 'pinned-recommendation' ||
|
||||
preview.source === 'hover-preset' || preview.source === 'pinned-preset' || preview.mode === 'add'
|
||||
? '#a8ffd0'
|
||||
: preview.mode === 'remove'
|
||||
@@ -13990,12 +14189,14 @@ export class CampScene extends Phaser.Scene {
|
||||
: preview.mode === 'blocked'
|
||||
? '#87919c'
|
||||
: '#d4dce6';
|
||||
const headline = action?.kind === 'undo-preset' && this.sortiePresetUndoState
|
||||
const headline = action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState
|
||||
? `추천 명단 ${current.selectedUnitIds.length}명·역할 적용 · 장비·보급 재점검`
|
||||
: action?.kind === 'undo-preset' && this.sortiePresetUndoState
|
||||
? `명단 ${current.selectedUnitIds.length}명·역할 저장값 적용 · 장비·보급 재점검`
|
||||
: isUndoAction && this.sortieSwapUndoState
|
||||
? `${this.sortieSwapUndoState.incomingUnitName} 편성 완료 · 장비·보급 재점검`
|
||||
: preview.headline;
|
||||
view.headline.setText(this.compactText(headline, action ? 44 : 64)).setColor(headlineColor);
|
||||
view.headline.setText(this.compactText(headline, action ? 34 : 64)).setColor(headlineColor);
|
||||
|
||||
view.metrics.forEach((metricView, index) => {
|
||||
const metric = preview.metrics[index];
|
||||
@@ -14017,6 +14218,16 @@ export class CampScene extends Phaser.Scene {
|
||||
.setColor(color);
|
||||
});
|
||||
|
||||
if (action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState) {
|
||||
view.impact
|
||||
.setText(`현재 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText('공통 무장 보급만 보존했습니다. 직전 추천안 적용은 한 번 되돌릴 수 있습니다.')
|
||||
.setColor('#ffdf7b');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action?.kind === 'undo-preset' && this.sortiePresetUndoState) {
|
||||
view.impact
|
||||
.setText(`현재 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
@@ -14047,6 +14258,26 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.mode === 'recommendation' && preview.recommendationId) {
|
||||
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 tradeoffLine = recommendation?.plan.tradeoff || '현재 편성과 비교해 역할과 보급을 다시 확인하십시오.';
|
||||
view.impact
|
||||
.setText(this.compactText(highlightLine, 42))
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText(
|
||||
this.compactText(
|
||||
`${tradeoffLine} · 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total}→${projectedStrategyCoverage.count}/${projectedStrategyCoverage.total} (${strategyDelta > 0 ? '+' : ''}${strategyDelta}) · 추격 ${pursuitChanges.currentPairs.length}→${pursuitChanges.projectedPairs.length}조 (${pursuitDelta > 0 ? '+' : ''}${pursuitDelta}) · 실제 편성 미변경`,
|
||||
52
|
||||
)
|
||||
)
|
||||
.setColor('#ffdf7b');
|
||||
return;
|
||||
}
|
||||
|
||||
const gainedRoleLabels = comparison?.gainedRoles.map((role) => this.sortieFormationRoleLabel(role)) ?? [];
|
||||
const lostRoleLabels = comparison?.lostRoles.map((role) => this.sortieFormationRoleLabel(role)) ?? [];
|
||||
const roleChanges = [
|
||||
@@ -14098,7 +14329,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const undo = action.kind === 'undo-swap' || action.kind === 'undo-preset';
|
||||
const undo = action.kind === 'undo-swap' || action.kind === 'undo-recommendation' || action.kind === 'undo-preset';
|
||||
view.actionButton
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setFillStyle(undo ? 0x243442 : 0x263a2d, 0.98)
|
||||
@@ -14109,6 +14340,19 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortieComparisonAction(
|
||||
preview = this.sortieFormationComparisonPreview()
|
||||
): SortieComparisonAction | undefined {
|
||||
if (
|
||||
preview?.source === 'pinned-recommendation' &&
|
||||
preview.mode === 'recommendation' &&
|
||||
preview.recommendationId &&
|
||||
this.sortieRecommendationProjection(preview.recommendationId)
|
||||
) {
|
||||
return {
|
||||
kind: 'confirm-recommendation',
|
||||
label: '적용 확정',
|
||||
recommendationId: preview.recommendationId
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
preview?.source === 'pinned-preset' &&
|
||||
preview.mode === 'preset' &&
|
||||
@@ -14140,11 +14384,20 @@ export class CampScene extends Phaser.Scene {
|
||||
if (
|
||||
preview &&
|
||||
(preview.source === 'hover-add' || preview.source === 'hover-swap' ||
|
||||
preview.source === 'hover-blocked' || preview.source === 'hover-preset')
|
||||
preview.source === 'hover-blocked' || preview.source === 'hover-recommendation' || preview.source === 'hover-preset')
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const recommendationUndo = this.sortieRecommendationUndoState;
|
||||
if (recommendationUndo && this.sortiePersistedConfigurationMatches(recommendationUndo.applied)) {
|
||||
return {
|
||||
kind: 'undo-recommendation',
|
||||
label: '되돌리기',
|
||||
recommendationId: recommendationUndo.recommendationId
|
||||
};
|
||||
}
|
||||
|
||||
const presetUndo = this.sortiePresetUndoState;
|
||||
if (presetUndo && this.sortiePersistedConfigurationMatches(presetUndo.applied)) {
|
||||
return {
|
||||
@@ -14168,6 +14421,10 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private handleSortieComparisonAction() {
|
||||
const action = this.sortieComparisonAction();
|
||||
if (action?.kind === 'confirm-recommendation' && action.recommendationId) {
|
||||
this.confirmPinnedSortieRecommendation(action.recommendationId);
|
||||
return;
|
||||
}
|
||||
if (action?.kind === 'confirm-preset' && action.presetId) {
|
||||
this.confirmPinnedSortiePreset(action.presetId);
|
||||
return;
|
||||
@@ -14182,6 +14439,10 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
if (action?.kind === 'undo-preset') {
|
||||
this.undoLastSortiePreset();
|
||||
return;
|
||||
}
|
||||
if (action?.kind === 'undo-recommendation') {
|
||||
this.undoLastSortieRecommendation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14980,9 +15241,20 @@ export class CampScene extends Phaser.Scene {
|
||||
const canUsePresetBook = this.usesStagedSortiePrep() && this.sortiePrepStep === 'formation' && !this.isFirstSortiePrepSlice();
|
||||
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);
|
||||
if (canUsePresetBook && canUseSortieOrder) {
|
||||
this.renderSortiePanelButton('추천', x + width - 230, y + 10, 48, 22, canRecommend, false, () => this.applyRecommendedSortiePlan(), depth + 1);
|
||||
this.sortieRecommendationToggleButton = this.renderSortiePanelButton(
|
||||
'추천안',
|
||||
x + width - 230,
|
||||
y + 10,
|
||||
48,
|
||||
22,
|
||||
canBrowseRecommendations,
|
||||
this.sortieRecommendationBrowserOpen,
|
||||
() => this.toggleSortieRecommendationBrowser(),
|
||||
depth + 1
|
||||
);
|
||||
this.sortiePresetToggleButton = this.renderSortiePanelButton(
|
||||
'편성책',
|
||||
x + width - 176,
|
||||
@@ -15007,6 +15279,7 @@ export class CampScene extends Phaser.Scene {
|
||||
depth + 1
|
||||
);
|
||||
} else {
|
||||
this.sortieRecommendationToggleButton = undefined;
|
||||
this.sortiePresetToggleButton = undefined;
|
||||
this.sortieOrderToggleButton = undefined;
|
||||
this.renderSortiePanelButton(hasBattle ? '추천 편성' : '추천 동행', x + width - 104, y + 10, 86, 22, canRecommend, false, () => this.applyRecommendedSortiePlan(), depth + 1);
|
||||
@@ -15674,6 +15947,117 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.campaign?.sortieFormationPresets ?? getCampaignState().sortieFormationPresets;
|
||||
}
|
||||
|
||||
private sortieRecommendationDefinition(recommendationId: SortieRecommendationPlanId) {
|
||||
return sortieRecommendationDefinitions.find((definition) => definition.id === recommendationId) ?? sortieRecommendationDefinitions[0];
|
||||
}
|
||||
|
||||
private sortieRecommendationPlans(scenario = this.nextSortieScenario()) {
|
||||
if (!scenario) {
|
||||
return [];
|
||||
}
|
||||
const rule = this.nextSortieRule(scenario);
|
||||
const recommendationById = new Map(rule.recommended.map((entry) => [entry.unitId, entry]));
|
||||
const candidates = this.sortieRosterUnits().map((unit, rosterIndex) => {
|
||||
const availability = this.sortieUnitAvailability(unit, scenario, rule);
|
||||
return {
|
||||
unit,
|
||||
available: availability.available,
|
||||
blockedReason: availability.available ? undefined : availability.reason,
|
||||
terrainScore: this.sortieTerrainScore(unit, scenario),
|
||||
preferredRole: recommendationById.get(unit.id)?.role ?? this.defaultSortieFormationRole(unit),
|
||||
rosterIndex
|
||||
};
|
||||
});
|
||||
const bonds = this.sortieSynergyBonds(scenario);
|
||||
const cacheKey = JSON.stringify({
|
||||
battleId: scenario.id,
|
||||
maxUnits: this.sortieMaxUnits(scenario),
|
||||
orderId: this.currentSortieOrderId(),
|
||||
coreBondId: this.currentSortieResonanceBondId(scenario),
|
||||
candidates: candidates.map((candidate) => ({
|
||||
id: candidate.unit.id,
|
||||
available: candidate.available,
|
||||
hp: candidate.unit.hp,
|
||||
maxHp: candidate.unit.maxHp,
|
||||
attack: candidate.unit.attack,
|
||||
move: candidate.unit.move,
|
||||
stats: candidate.unit.stats,
|
||||
terrainScore: candidate.terrainScore,
|
||||
preferredRole: candidate.preferredRole,
|
||||
rosterIndex: candidate.rosterIndex
|
||||
})),
|
||||
bonds: bonds.map((bond) => ({ id: bond.id, unitIds: bond.unitIds, level: bond.level, exp: bond.exp }))
|
||||
});
|
||||
if (this.sortieRecommendationPlanCache?.key === cacheKey) {
|
||||
return this.sortieRecommendationPlanCache.plans;
|
||||
}
|
||||
const plans = buildSortieRecommendationPlans({
|
||||
battleId: scenario.id,
|
||||
maxUnits: this.sortieMaxUnits(scenario),
|
||||
orderId: this.currentSortieOrderId(),
|
||||
candidates,
|
||||
requiredUnitIds: rule.requiredUnitIds ?? defaultRequiredSortieUnitIds,
|
||||
excludedUnitIds: rule.excludedUnitIds ?? [],
|
||||
recommendedUnits: rule.recommended,
|
||||
recommendedClasses: rule.recommendedClasses ?? [],
|
||||
bonds,
|
||||
coreBondId: this.currentSortieResonanceBondId(scenario)
|
||||
});
|
||||
this.sortieRecommendationPlanCache = { key: cacheKey, plans };
|
||||
return plans;
|
||||
}
|
||||
|
||||
private sortieRecommendationProjections(scenario = this.nextSortieScenario()): SortieRecommendationProjection[] {
|
||||
const selectedItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
||||
return this.sortieRecommendationPlans(scenario).flatMap<SortieRecommendationProjection>((plan) => {
|
||||
if (!plan.applicable || plan.selectedUnitIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const selectedUnitIds = this.normalizedSortieUnitIds(plan.selectedUnitIds);
|
||||
const selectedIds = new Set(selectedUnitIds);
|
||||
const formationAssignments = normalizeSortieFormationAssignments(plan.formationAssignments, selectedIds);
|
||||
const itemAssignments = Object.entries(selectedItemAssignments).reduce<CampaignSortieItemAssignments>(
|
||||
(assignments, [unitId, stocks]) => {
|
||||
if (selectedIds.has(unitId)) {
|
||||
assignments[unitId] = { ...stocks };
|
||||
}
|
||||
return assignments;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const definition = this.sortieRecommendationDefinition(plan.id);
|
||||
return [{
|
||||
recommendationId: plan.id,
|
||||
plan,
|
||||
label: plan.label || definition.label,
|
||||
summary: plan.summary || definition.summary,
|
||||
accent: definition.accent,
|
||||
selectedUnitIds,
|
||||
formationAssignments,
|
||||
itemAssignments,
|
||||
reasonLines: [...plan.highlights.slice(0, 2), plan.tradeoff].filter(Boolean)
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
private sortieRecommendationProjection(
|
||||
recommendationId: SortieRecommendationPlanId,
|
||||
scenario = this.nextSortieScenario()
|
||||
) {
|
||||
return this.sortieRecommendationProjections(scenario).find((projection) => projection.recommendationId === recommendationId);
|
||||
}
|
||||
|
||||
private sortieRecommendationProjectionMatchesCurrent(projection: SortieRecommendationProjection) {
|
||||
if (JSON.stringify(projection.selectedUnitIds) !== JSON.stringify(this.selectedSortieUnitIds)) {
|
||||
return false;
|
||||
}
|
||||
const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit]));
|
||||
return projection.selectedUnitIds.every((unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
return Boolean(unit && projection.formationAssignments[unitId] === this.sortieFormationRole(unit));
|
||||
});
|
||||
}
|
||||
|
||||
private sortiePresetDefinition(presetId: CampaignSortiePresetId) {
|
||||
return sortiePresetDefinitions.find((definition) => definition.id === presetId) ?? sortiePresetDefinitions[0];
|
||||
}
|
||||
@@ -15786,7 +16170,7 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private sortiePresetComparisonMetrics(
|
||||
projection: SortiePresetProjection,
|
||||
projection: Pick<SortiePresetProjection, 'selectedUnitIds' | 'formationAssignments'>,
|
||||
scenario = this.nextSortieScenario()
|
||||
): SortieComparisonMetric[] {
|
||||
const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit]));
|
||||
@@ -15814,6 +16198,13 @@ export class CampScene extends Phaser.Scene {
|
||||
}));
|
||||
}
|
||||
|
||||
private sortieRecommendationComparisonMetrics(
|
||||
projection: SortieRecommendationProjection,
|
||||
scenario = this.nextSortieScenario()
|
||||
) {
|
||||
return this.sortiePresetComparisonMetrics(projection, scenario);
|
||||
}
|
||||
|
||||
private sortieFormationComparisonPreview(): SortieFormationComparisonPreview | undefined {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const selectedUnitIds = [...this.selectedSortieUnitIds];
|
||||
@@ -15822,6 +16213,70 @@ export class CampScene extends Phaser.Scene {
|
||||
if (this.sortieOrderBrowserOpen) {
|
||||
return undefined;
|
||||
}
|
||||
const recommendationId = this.sortieHoveredRecommendationId ?? this.sortiePinnedRecommendationId;
|
||||
if (recommendationId) {
|
||||
const projection = this.sortieRecommendationProjection(recommendationId);
|
||||
if (projection) {
|
||||
const projected = this.sortieSynergySnapshot(
|
||||
projection.selectedUnitIds,
|
||||
scenario,
|
||||
projection.formationAssignments
|
||||
);
|
||||
const comparison = compareSortieSynergy(current, projected);
|
||||
const projectedIds = new Set(projection.selectedUnitIds);
|
||||
const retainedCount = selectedUnitIds.filter((unitId) => projectedIds.has(unitId)).length;
|
||||
const outgoingCount = selectedUnitIds.length - retainedCount;
|
||||
const incomingCount = projection.selectedUnitIds.length - retainedCount;
|
||||
const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit]));
|
||||
const roleChangeCount = projection.selectedUnitIds.filter((unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
return Boolean(unit && selectedIds.has(unitId) && projection.formationAssignments[unitId] !== this.sortieFormationRole(unit, scenario));
|
||||
}).length;
|
||||
return {
|
||||
source: this.sortiePinnedRecommendationId === recommendationId && !this.sortieHoveredRecommendationId
|
||||
? 'pinned-recommendation'
|
||||
: 'hover-recommendation',
|
||||
mode: 'recommendation',
|
||||
unitId: recommendationId,
|
||||
recommendationId,
|
||||
incomingUnitId: null,
|
||||
incomingUnitName: null,
|
||||
outgoingUnitId: null,
|
||||
outgoingUnitName: null,
|
||||
selectedUnitIds,
|
||||
projectedSelectedUnitIds: projection.selectedUnitIds,
|
||||
headline: `유지 ${retainedCount}명 · 교체 ${outgoingCount}OUT/${incomingCount}IN · 역할 ${roleChangeCount}명 변경 · ${incomingCount > 0 ? `신규 ${incomingCount}명 보급 미배정` : '보급 유지'}`,
|
||||
detail: projection.reasonLines.join(' · ') || `${projection.label} 기준으로 계산한 출전안입니다.`,
|
||||
metrics: this.sortieRecommendationComparisonMetrics(projection, scenario),
|
||||
current,
|
||||
projected,
|
||||
comparison
|
||||
};
|
||||
}
|
||||
}
|
||||
const appliedRecommendation = this.sortieRecommendationUndoState;
|
||||
if (appliedRecommendation && this.sortiePersistedConfigurationMatches(appliedRecommendation.applied)) {
|
||||
const projection = this.sortieRecommendationProjection(appliedRecommendation.recommendationId);
|
||||
if (projection) {
|
||||
return {
|
||||
source: 'focus',
|
||||
mode: 'recommendation',
|
||||
unitId: appliedRecommendation.recommendationId,
|
||||
recommendationId: appliedRecommendation.recommendationId,
|
||||
incomingUnitId: null,
|
||||
incomingUnitName: null,
|
||||
outgoingUnitId: null,
|
||||
outgoingUnitName: null,
|
||||
selectedUnitIds,
|
||||
projectedSelectedUnitIds: selectedUnitIds,
|
||||
headline: `${projection.label} 적용 완료`,
|
||||
detail: '유지 가능한 보급만 보존했습니다.',
|
||||
metrics: this.sortieRecommendationComparisonMetrics(projection, scenario),
|
||||
current,
|
||||
projected: current
|
||||
};
|
||||
}
|
||||
}
|
||||
const presetId = this.sortieHoveredPresetId ?? this.sortiePinnedPresetId;
|
||||
if (presetId) {
|
||||
const projection = this.sortiePresetProjection(presetId, scenario);
|
||||
@@ -16057,6 +16512,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private toggleSortieOrderBrowser(forceOpen?: boolean) {
|
||||
const open = forceOpen ?? !this.sortieOrderBrowserOpen;
|
||||
if (open) {
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
this.closeSortiePresetBrowserState();
|
||||
this.sortieFormationPanelMode = 'order';
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
@@ -16086,6 +16542,7 @@ export class CampScene extends Phaser.Scene {
|
||||
if (!open) {
|
||||
this.closeSortiePresetBrowserState();
|
||||
} else {
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
this.sortieFormationPanelMode = 'preset';
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
@@ -16106,10 +16563,41 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private closeSortieFormationPanelBrowser() {
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
this.closeSortiePresetBrowserState();
|
||||
this.sortieFormationPanelMode = 'roster';
|
||||
}
|
||||
|
||||
private toggleSortieRecommendationBrowser(forceOpen?: boolean) {
|
||||
const open = forceOpen ?? !this.sortieRecommendationBrowserOpen;
|
||||
if (!open) {
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
} else {
|
||||
this.closeSortiePresetBrowserState();
|
||||
this.sortieFormationPanelMode = 'recommendation';
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.sortieCoreResonanceSelectorOpen = false;
|
||||
this.sortieCoreResonancePage = 0;
|
||||
}
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private closeSortieRecommendationBrowserState() {
|
||||
if (this.sortieFormationPanelMode === 'recommendation') {
|
||||
this.sortieFormationPanelMode = 'roster';
|
||||
}
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
}
|
||||
|
||||
private get sortieRecommendationBrowserOpen() {
|
||||
return this.sortieFormationPanelMode === 'recommendation';
|
||||
}
|
||||
|
||||
private get sortiePresetBrowserOpen() {
|
||||
return this.sortieFormationPanelMode === 'preset';
|
||||
}
|
||||
@@ -16118,6 +16606,30 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.sortieFormationPanelMode === 'order';
|
||||
}
|
||||
|
||||
private pinSortieRecommendation(recommendationId: SortieRecommendationPlanId) {
|
||||
const projection = this.sortieRecommendationProjection(recommendationId);
|
||||
const definition = this.sortieRecommendationDefinition(recommendationId);
|
||||
if (!projection) {
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice(`${definition.label}을 계산할 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
if (this.sortieRecommendationProjectionMatchesCurrent(projection)) {
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice(`${definition.label}이 이미 현재 명단과 역할에 적용되어 있습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sortiePinnedRecommendationId = recommendationId;
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortiePresetOverwriteConfirmId = undefined;
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private pinSortiePreset(presetId: CampaignSortiePresetId) {
|
||||
const projection = this.sortiePresetProjection(presetId);
|
||||
const definition = this.sortiePresetDefinition(presetId);
|
||||
@@ -16187,6 +16699,82 @@ export class CampScene extends Phaser.Scene {
|
||||
this.showCampNotice(`${definition.label} 편성책 저장 완료 · 보급은 포함하지 않았습니다.`);
|
||||
}
|
||||
|
||||
private confirmPinnedSortieRecommendation(recommendationId: SortieRecommendationPlanId) {
|
||||
const preview = this.sortieFormationComparisonPreview();
|
||||
if (preview?.source !== 'pinned-recommendation' || preview.recommendationId !== recommendationId) {
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice('고정된 추천안이 없습니다. 비교 버튼을 먼저 누르십시오.');
|
||||
return;
|
||||
}
|
||||
|
||||
const projection = this.sortieRecommendationProjection(recommendationId);
|
||||
if (
|
||||
!projection ||
|
||||
JSON.stringify(projection.selectedUnitIds) !== JSON.stringify(preview.projectedSelectedUnitIds)
|
||||
) {
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice('편성 조건이 달라졌습니다. 추천안을 다시 비교하십시오.');
|
||||
return;
|
||||
}
|
||||
|
||||
const scenario = this.nextSortieScenario();
|
||||
const beforeCoreBondId = this.currentSortieResonanceBondId(scenario);
|
||||
const before = this.captureSortieConfiguration();
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.sortiePresetUndoState = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.selectedSortieUnitIds = [...projection.selectedUnitIds];
|
||||
this.sortieFormationAssignments = { ...projection.formationAssignments };
|
||||
this.sortieItemAssignments = this.cloneSortieItemAssignments(projection.itemAssignments);
|
||||
this.sortieFocusedUnitId = projection.selectedUnitIds.includes(this.sortieFocusedUnitId)
|
||||
? this.sortieFocusedUnitId
|
||||
: projection.selectedUnitIds[0];
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
this.sortiePlanFeedback = `${projection.label} 적용 완료 · 장비·보급 재점검`;
|
||||
this.persistSortieSelection();
|
||||
this.sortieRecommendationUndoState = {
|
||||
recommendationId,
|
||||
beforeCoreBondId,
|
||||
before,
|
||||
applied: this.captureSortieConfiguration()
|
||||
};
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
this.showCampNotice(`${projection.label} 적용 완료 · 공통 무장의 보급만 유지했습니다.`);
|
||||
}
|
||||
|
||||
private undoLastSortieRecommendation() {
|
||||
const undo = this.sortieRecommendationUndoState;
|
||||
if (!undo || !this.sortiePersistedConfigurationMatches(undo.applied)) {
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice('이후 편성이 변경되어 직전 추천안 적용을 되돌릴 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.selectedSortieUnitIds = [...undo.before.selectedUnitIds];
|
||||
this.sortieFormationAssignments = { ...undo.before.formationAssignments };
|
||||
this.sortieItemAssignments = this.cloneSortieItemAssignments(undo.before.itemAssignments);
|
||||
this.sortieFocusedUnitId = undo.before.focusedUnitId;
|
||||
this.sortiePlanFeedback = undo.before.planFeedback;
|
||||
this.sortieRosterScroll = undo.before.rosterScroll;
|
||||
this.sortiePortraitRosterPage = undo.before.portraitRosterPage;
|
||||
this.persistSortieSelection();
|
||||
const scenario = this.nextSortieScenario();
|
||||
if (scenario) {
|
||||
this.campaign = setCampaignSortieResonanceSelection(scenario.id, undo.beforeCoreBondId);
|
||||
}
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
this.showCampNotice(`${this.sortieRecommendationDefinition(undo.recommendationId).label} 적용을 되돌렸습니다.`);
|
||||
}
|
||||
|
||||
private confirmPinnedSortiePreset(presetId: CampaignSortiePresetId) {
|
||||
const preview = this.sortieFormationComparisonPreview();
|
||||
if (preview?.source !== 'pinned-preset' || preview.presetId !== presetId) {
|
||||
@@ -17699,6 +18287,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieHoveredUnitId = undefined;
|
||||
if (clearPinnedSwap) {
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.closeSortieRecommendationBrowserState();
|
||||
this.closeSortiePresetBrowserState();
|
||||
this.sortieFormationPanelMode = 'roster';
|
||||
this.sortieCoreResonanceSelectorOpen = false;
|
||||
@@ -17708,6 +18297,8 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortiePursuitPanelView = undefined;
|
||||
this.sortieCoreResonanceToggleButton = undefined;
|
||||
this.sortieCoreResonanceToggleLabel = undefined;
|
||||
this.sortieRecommendationBrowserView = undefined;
|
||||
this.sortieRecommendationToggleButton = undefined;
|
||||
this.sortiePresetBrowserView = undefined;
|
||||
this.sortiePresetToggleButton = undefined;
|
||||
this.sortieOrderBrowserView = undefined;
|
||||
@@ -20726,6 +21317,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.reportFormationHistoryUndoState = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.sortiePresetUndoState = undefined;
|
||||
this.sortieHoveredPresetId = undefined;
|
||||
this.sortiePinnedPresetId = undefined;
|
||||
@@ -21085,6 +21679,8 @@ export class CampScene extends Phaser.Scene {
|
||||
const sortiePursuit = this.sortiePursuitPairs();
|
||||
const sortieCoreResonanceCandidates = this.sortieCoreResonanceCandidates();
|
||||
const sortieCoreResonanceSelection = this.campaign?.sortieResonanceSelection;
|
||||
const sortieRecommendationPlanResults = this.sortieRecommendationPlans();
|
||||
const sortieRecommendationPlans = this.sortieRecommendationProjections();
|
||||
const recommendedPresetId = this.recommendedSortiePresetId();
|
||||
const sortieFormationPresets = this.sortieFormationPresets();
|
||||
const sortieOrderId = this.currentSortieOrderId();
|
||||
@@ -21464,6 +22060,39 @@ export class CampScene extends Phaser.Scene {
|
||||
nextSortieBattleId: sortieScenario?.id ?? null,
|
||||
nextSortieBattleTitle: sortieScenario?.title ?? null,
|
||||
sortieRecommendationEnabled: this.canApplyRecommendedSortiePlan(sortieScenario),
|
||||
sortieRecommendationBrowser: {
|
||||
open: this.sortieRecommendationBrowserOpen,
|
||||
hoveredPlanId: this.sortieHoveredRecommendationId ?? null,
|
||||
pinnedPlanId: this.sortiePinnedRecommendationId ?? null,
|
||||
toggleButtonBounds: this.sortieObjectBoundsDebug(this.sortieRecommendationToggleButton),
|
||||
browserBounds: this.sortieObjectBoundsDebug(this.sortieRecommendationBrowserView?.background),
|
||||
closeButtonBounds: this.sortieObjectBoundsDebug(this.sortieRecommendationBrowserView?.closeButton),
|
||||
cards: sortieRecommendationDefinitions.map((definition) => {
|
||||
const recommendationPlan = sortieRecommendationPlanResults.find((plan) => plan.id === definition.id);
|
||||
const projection = sortieRecommendationPlans.find((plan) => plan.recommendationId === definition.id);
|
||||
const view = this.sortieRecommendationBrowserView?.cards[definition.id];
|
||||
return {
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
title: recommendationPlan?.title ?? null,
|
||||
applicable: Boolean(recommendationPlan?.applicable),
|
||||
blockedReason: recommendationPlan?.blockedReason ?? null,
|
||||
current: Boolean(projection && this.sortieRecommendationProjectionMatchesCurrent(projection)),
|
||||
selectedUnitIds: projection ? [...projection.selectedUnitIds] : [],
|
||||
selectedUnitNames: projection ? projection.selectedUnitIds.map((unitId) => this.unitName(unitId)) : [],
|
||||
formationAssignments: projection ? { ...projection.formationAssignments } : {},
|
||||
projectedItemAssignments: projection ? this.cloneSortieItemAssignments(projection.itemAssignments) : {},
|
||||
score: recommendationPlan?.score ?? 0,
|
||||
summary: recommendationPlan?.summary ?? null,
|
||||
highlights: recommendationPlan ? [...recommendationPlan.highlights] : [],
|
||||
tradeoff: recommendationPlan?.tradeoff ?? null,
|
||||
metrics: recommendationPlan?.metrics ? { ...recommendationPlan.metrics } : null,
|
||||
unitReasons: recommendationPlan?.unitReasons ? { ...recommendationPlan.unitReasons } : {},
|
||||
backgroundBounds: this.sortieObjectBoundsDebug(view?.background),
|
||||
compareButtonBounds: this.sortieObjectBoundsDebug(view?.compareButton)
|
||||
};
|
||||
})
|
||||
},
|
||||
selectedUnitId: this.selectedUnitId,
|
||||
selectedDialogueId: this.selectedDialogueId,
|
||||
selectedVisitId: this.selectedVisitId,
|
||||
@@ -21528,7 +22157,23 @@ export class CampScene extends Phaser.Scene {
|
||||
strategyCoverage: sortieComparisonStrategyCoverage
|
||||
}
|
||||
: null,
|
||||
sortieComparisonAction: sortieComparisonAction ? { ...sortieComparisonAction, enabled: true } : null,
|
||||
sortieComparisonPanel: this.sortieComparisonPanelView
|
||||
? {
|
||||
bounds: this.sortieObjectBoundsDebug(this.sortieComparisonPanelView.background),
|
||||
title: this.sortieComparisonPanelView.title.text,
|
||||
summary: this.sortieComparisonPanelView.summary.text,
|
||||
headline: this.sortieComparisonPanelView.headline.text,
|
||||
impact: this.sortieComparisonPanelView.impact.text,
|
||||
detail: this.sortieComparisonPanelView.detail.text
|
||||
}
|
||||
: null,
|
||||
sortieComparisonAction: sortieComparisonAction
|
||||
? {
|
||||
...sortieComparisonAction,
|
||||
enabled: true,
|
||||
buttonBounds: this.sortieInteractiveObjectBoundsDebug(this.sortieComparisonPanelView?.actionButton)
|
||||
}
|
||||
: null,
|
||||
sortieSwapUndo: this.sortieSwapUndoState
|
||||
? {
|
||||
available: sortieComparisonAction?.kind === 'undo-swap',
|
||||
@@ -21542,6 +22187,13 @@ export class CampScene extends Phaser.Scene {
|
||||
presetId: this.sortiePresetUndoState.presetId
|
||||
}
|
||||
: null,
|
||||
sortieRecommendationUndo: this.sortieRecommendationUndoState
|
||||
? {
|
||||
available: sortieComparisonAction?.kind === 'undo-recommendation',
|
||||
recommendationId: this.sortieRecommendationUndoState.recommendationId,
|
||||
beforeCoreBondId: this.sortieRecommendationUndoState.beforeCoreBondId ?? null
|
||||
}
|
||||
: null,
|
||||
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
|
||||
sortieStrategyCoverage: {
|
||||
total: sortieStrategyCoverage.total,
|
||||
|
||||
Reference in New Issue
Block a user