feat: make sortie feedback actionable
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
} from '../data/battleUsables';
|
||||
import { getUnitClass, terrainRules, type TerrainType, type UnitClassKey } from '../data/battleRules';
|
||||
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
|
||||
import { getSortieFlow } from '../data/campaignFlow';
|
||||
import { getSortieFlow, type SortieFlow } from '../data/campaignFlow';
|
||||
import { getCampSoundscape, type CampSoundscape } from '../data/campSoundscapes';
|
||||
import {
|
||||
campaignPortraitKeysByUnitId,
|
||||
@@ -65,6 +65,10 @@ import {
|
||||
sortieRecommendationDisplayUnitIds,
|
||||
type SortieRecommendationOutcome
|
||||
} from '../data/sortieRecommendationOutcome';
|
||||
import {
|
||||
buildSortieRecommendationImprovement,
|
||||
type SortieRecommendationImprovement
|
||||
} from '../data/sortieRecommendationImprovement';
|
||||
import {
|
||||
sortieOrderDefinition,
|
||||
sortieOrderDefinitions,
|
||||
@@ -441,8 +445,8 @@ type SortieComparisonMetric = {
|
||||
};
|
||||
|
||||
type SortieFormationComparisonPreview = {
|
||||
source: 'focus' | 'hover-add' | 'hover-swap' | 'pinned-swap' | 'hover-blocked' | 'hover-recommendation' | 'pinned-recommendation' | 'hover-preset' | 'pinned-preset';
|
||||
mode: SortieUnitSynergyPreview['mode'] | 'swap' | 'recommendation' | 'preset';
|
||||
source: 'focus' | 'guided-improvement' | 'hover-add' | 'hover-swap' | 'pinned-swap' | 'hover-blocked' | 'hover-recommendation' | 'pinned-recommendation' | 'hover-preset' | 'pinned-preset';
|
||||
mode: SortieUnitSynergyPreview['mode'] | 'swap' | 'recommendation' | 'preset' | 'improvement';
|
||||
unitId: string;
|
||||
recommendationId?: SortieRecommendationPlanId;
|
||||
presetId?: CampaignSortiePresetId;
|
||||
@@ -561,6 +565,30 @@ type SortieRecommendationUndoState = {
|
||||
applied: SortieConfigurationSnapshot;
|
||||
};
|
||||
|
||||
type SortieGuidedImprovementState = {
|
||||
sourceBattleId: string;
|
||||
targetBattleId: BattleScenarioId;
|
||||
orderId?: CampaignSortieOrderId;
|
||||
coreBondId?: string;
|
||||
proposal: SortieRecommendationImprovement;
|
||||
};
|
||||
|
||||
type SortieGuidedImprovementProjection = {
|
||||
selectedUnitIds: string[];
|
||||
formationAssignments: SortieFormationAssignments;
|
||||
itemAssignments: CampaignSortieItemAssignments;
|
||||
};
|
||||
|
||||
type SortieGuidedImprovementUndoState = {
|
||||
sourceBattleId: string;
|
||||
targetBattleId: BattleScenarioId;
|
||||
proposal: SortieRecommendationImprovement;
|
||||
beforeCoreBondId?: string;
|
||||
beforeRecommendation?: CampaignSortieRecommendationSnapshot;
|
||||
before: SortieConfigurationSnapshot;
|
||||
applied: SortieConfigurationSnapshot;
|
||||
};
|
||||
|
||||
type SortiePresetUndoState = {
|
||||
presetId: CampaignSortiePresetId;
|
||||
before: SortieConfigurationSnapshot;
|
||||
@@ -627,6 +655,7 @@ type SortieOrderBrowserView = {
|
||||
|
||||
type ReportFormationReviewView = {
|
||||
closeButton: Phaser.GameObjects.Rectangle;
|
||||
improvementButton?: Phaser.GameObjects.Rectangle;
|
||||
presetButtons: Partial<Record<CampaignSortiePresetId, Phaser.GameObjects.Rectangle>>;
|
||||
recommendationPanel?: Phaser.GameObjects.Rectangle;
|
||||
recommendationRows: {
|
||||
@@ -696,7 +725,7 @@ type ReportFormationHistoryView = {
|
||||
};
|
||||
|
||||
type SortieComparisonAction = {
|
||||
kind: 'confirm-swap' | 'undo-swap' | 'confirm-recommendation' | 'undo-recommendation' | 'confirm-preset' | 'undo-preset';
|
||||
kind: 'confirm-improvement' | 'undo-improvement' | 'confirm-swap' | 'undo-swap' | 'confirm-recommendation' | 'undo-recommendation' | 'confirm-preset' | 'undo-preset';
|
||||
label: string;
|
||||
outgoingUnitId?: string;
|
||||
incomingUnitId?: string;
|
||||
@@ -715,6 +744,8 @@ type CampaignTimelineChapter = {
|
||||
|
||||
type CampSceneData = {
|
||||
openSortiePrep?: boolean;
|
||||
openSortieImprovement?: boolean;
|
||||
retryBattleId?: BattleScenarioId;
|
||||
skipIntroStory?: boolean;
|
||||
};
|
||||
|
||||
@@ -11222,6 +11253,8 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortieHoveredRecommendationId?: SortieRecommendationPlanId;
|
||||
private sortiePinnedRecommendationId?: SortieRecommendationPlanId;
|
||||
private sortieRecommendationUndoState?: SortieRecommendationUndoState;
|
||||
private sortieGuidedImprovement?: SortieGuidedImprovementState;
|
||||
private sortieGuidedImprovementUndoState?: SortieGuidedImprovementUndoState;
|
||||
private sortieRecommendationPlanCache?: { key: string; plans: SortieRecommendationPlan[] };
|
||||
private sortieRecommendationBrowserView?: SortieRecommendationBrowserView;
|
||||
private sortieRecommendationReasonPanelView?: SortieRecommendationReasonPanelView;
|
||||
@@ -11247,6 +11280,8 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortiePrepStep: SortiePrepStep = 'briefing';
|
||||
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
|
||||
private openSortiePrepOnCreate = false;
|
||||
private openSortieImprovementOnCreate = false;
|
||||
private retrySortieBattleId?: BattleScenarioId;
|
||||
private skipIntroStoryForSortie = false;
|
||||
|
||||
constructor() {
|
||||
@@ -11255,6 +11290,8 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
init(data?: CampSceneData) {
|
||||
this.openSortiePrepOnCreate = Boolean(data?.openSortiePrep);
|
||||
this.openSortieImprovementOnCreate = Boolean(data?.openSortieImprovement);
|
||||
this.retrySortieBattleId = data?.retryBattleId;
|
||||
this.skipIntroStoryForSortie = Boolean(data?.skipIntroStory);
|
||||
}
|
||||
|
||||
@@ -11279,6 +11316,8 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieHoveredRecommendationId = undefined;
|
||||
this.sortiePinnedRecommendationId = undefined;
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
this.sortieRecommendationPlanCache = undefined;
|
||||
this.sortieRecommendationBrowserView = undefined;
|
||||
this.sortieRecommendationReasonPanelView = undefined;
|
||||
@@ -11313,7 +11352,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.visitedTabs = new Set(['status']);
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
||||
this.ensureCurrentCampRecruitment();
|
||||
if (!this.retrySortieBattleId) {
|
||||
this.ensureCurrentCampRecruitment();
|
||||
}
|
||||
const sortieFlow = this.currentSortieFlow();
|
||||
this.campSkinSelection = selectCampSkin({
|
||||
campaignStep: this.campaign.step,
|
||||
@@ -11462,7 +11503,12 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderStaticButtons();
|
||||
this.render();
|
||||
if (this.openSortiePrepOnCreate) {
|
||||
this.time.delayedCall(0, () => this.showSortiePrep());
|
||||
this.time.delayedCall(0, () => {
|
||||
if (this.openSortieImprovementOnCreate) {
|
||||
this.prepareGuidedSortieImprovement();
|
||||
}
|
||||
this.showSortiePrep();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12999,7 +13045,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const isFirstSortieSlice = this.isFirstSortiePrepSlice(flow);
|
||||
const usesStagedPrep = this.usesStagedSortiePrep(flow);
|
||||
if (usesStagedPrep && !wasVisible) {
|
||||
this.sortiePrepStep = 'briefing';
|
||||
this.sortiePrepStep = this.sortieGuidedImprovement || this.sortieGuidedImprovementUndoState
|
||||
? 'formation'
|
||||
: 'briefing';
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
}
|
||||
const checklist = this.sortieChecklist();
|
||||
@@ -14009,7 +14057,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-recommendation' || action.kind === 'undo-preset';
|
||||
const undo = action.kind === 'undo-improvement' || 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);
|
||||
@@ -14125,7 +14173,7 @@ export class CampScene extends Phaser.Scene {
|
||||
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 isUndoAction = action?.kind === 'undo-improvement' || 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);
|
||||
@@ -14138,7 +14186,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const lostStrategies = strategyCoverageComparison.lost;
|
||||
const strategyCoverageDelta = strategyCoverageComparison.delta;
|
||||
this.setCampaignSortieCoreSelectorVisible(false);
|
||||
const isHoverPreview = preview?.source === 'hover-add' || preview?.source === 'hover-swap' || preview?.source === 'pinned-swap' ||
|
||||
const isHoverPreview = preview?.source === 'guided-improvement' || 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);
|
||||
@@ -14207,12 +14255,16 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState
|
||||
const title = action?.kind === 'undo-improvement' && this.sortieGuidedImprovementUndoState
|
||||
? `보완 완료 · ${this.sortieGuidedImprovementUndoState.proposal.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 === 'guided-improvement'
|
||||
? `전투 보고 보완 · ${preview.recommendationId ? this.sortieRecommendationDefinition(preview.recommendationId).label : '추천안'}`
|
||||
: preview.source === 'pinned-recommendation' || preview.source === 'hover-recommendation'
|
||||
? `${this.sortieRecommendationDefinition(preview.recommendationId ?? 'order').label} 비교`
|
||||
: preview.source === 'pinned-preset' || preview.source === 'hover-preset'
|
||||
@@ -14231,7 +14283,7 @@ export class CampScene extends Phaser.Scene {
|
||||
26
|
||||
)
|
||||
);
|
||||
const headlineColor = isUndoAction || preview.source === 'hover-swap' || preview.source === 'pinned-swap' ||
|
||||
const headlineColor = isUndoAction || preview.source === 'guided-improvement' || 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'
|
||||
@@ -14242,7 +14294,9 @@ export class CampScene extends Phaser.Scene {
|
||||
: preview.mode === 'blocked'
|
||||
? '#87919c'
|
||||
: '#d4dce6';
|
||||
const headline = action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState
|
||||
const headline = action?.kind === 'undo-improvement' && this.sortieGuidedImprovementUndoState
|
||||
? `${this.sortieGuidedImprovementUndoState.proposal.title} · 출진 전 점검`
|
||||
: action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState
|
||||
? `추천 명단 ${current.selectedUnitIds.length}명·역할 적용 · 장비·보급 재점검`
|
||||
: action?.kind === 'undo-preset' && this.sortiePresetUndoState
|
||||
? `명단 ${current.selectedUnitIds.length}명·역할 저장값 적용 · 장비·보급 재점검`
|
||||
@@ -14271,6 +14325,16 @@ export class CampScene extends Phaser.Scene {
|
||||
.setColor(color);
|
||||
});
|
||||
|
||||
if (action?.kind === 'undo-improvement' && this.sortieGuidedImprovementUndoState) {
|
||||
view.impact
|
||||
.setText(this.compactText(this.sortieGuidedImprovementUndoState.proposal.detail, 68))
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText('직전 전투의 보완을 적용했습니다. 되돌리기는 한 번만 가능합니다.')
|
||||
.setColor('#ffdf7b');
|
||||
return;
|
||||
}
|
||||
|
||||
if (action?.kind === 'undo-recommendation' && this.sortieRecommendationUndoState) {
|
||||
view.impact
|
||||
.setText(`현재 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
@@ -14311,6 +14375,28 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.mode === 'improvement' && this.sortieGuidedImprovement) {
|
||||
const proposal = this.sortieGuidedImprovement.proposal;
|
||||
view.impact
|
||||
.setText(this.compactText(proposal.detail, 52))
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText(
|
||||
this.compactText(
|
||||
proposal.kind === 'swap'
|
||||
? `한 명만 교체 · OUT ${proposal.outgoingUnitName} / IN ${proposal.incomingUnitName} · 신규 무장 보급 미배정 · 실제 편성 미변경`
|
||||
: proposal.kind === 'add'
|
||||
? `빈 자리에 한 명만 추가 · IN ${proposal.incomingUnitName} · 신규 무장 보급 미배정 · 실제 편성 미변경`
|
||||
: proposal.kind === 'role'
|
||||
? `한 명의 역할만 변경 · ${proposal.unitName} ${this.sortieFormationRoleLabel(proposal.fromRole, true)}→${this.sortieFormationRoleLabel(proposal.toRole, true)} · 실제 편성 미변경`
|
||||
: `${proposal.instruction} · 확인 전까지 추천 기록 미변경`,
|
||||
64
|
||||
)
|
||||
)
|
||||
.setColor('#ffdf7b');
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.mode === 'recommendation' && preview.recommendationId) {
|
||||
const recommendation = this.sortieRecommendationProjection(preview.recommendationId);
|
||||
const pursuitDelta = pursuitChanges.projectedPairs.length - pursuitChanges.currentPairs.length;
|
||||
@@ -14388,7 +14474,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
const undo = action.kind === 'undo-swap' || action.kind === 'undo-recommendation' || action.kind === 'undo-preset';
|
||||
const undo = action.kind === 'undo-improvement' || action.kind === 'undo-swap' || action.kind === 'undo-recommendation' || action.kind === 'undo-preset';
|
||||
view.actionButton
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setFillStyle(undo ? 0x243442 : 0x263a2d, 0.98)
|
||||
@@ -14399,6 +14485,18 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortieComparisonAction(
|
||||
preview = this.sortieFormationComparisonPreview()
|
||||
): SortieComparisonAction | undefined {
|
||||
if (
|
||||
preview?.source === 'guided-improvement' &&
|
||||
preview.mode === 'improvement' &&
|
||||
this.sortieGuidedImprovement &&
|
||||
this.sortieGuidedImprovementProjection(this.sortieGuidedImprovement)
|
||||
) {
|
||||
return {
|
||||
kind: 'confirm-improvement',
|
||||
label: this.sortieGuidedImprovement.proposal.kind === 'tactic' ? '행동 확인' : '보완 적용'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
preview?.source === 'pinned-recommendation' &&
|
||||
preview.mode === 'recommendation' &&
|
||||
@@ -14448,6 +14546,14 @@ export class CampScene extends Phaser.Scene {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const guidedUndo = this.sortieGuidedImprovementUndoState;
|
||||
if (guidedUndo && this.sortiePersistedConfigurationMatches(guidedUndo.applied)) {
|
||||
return {
|
||||
kind: 'undo-improvement',
|
||||
label: '되돌리기'
|
||||
};
|
||||
}
|
||||
|
||||
const recommendationUndo = this.sortieRecommendationUndoState;
|
||||
if (recommendationUndo && this.sortiePersistedConfigurationMatches(recommendationUndo.applied)) {
|
||||
return {
|
||||
@@ -14480,6 +14586,14 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private handleSortieComparisonAction() {
|
||||
const action = this.sortieComparisonAction();
|
||||
if (action?.kind === 'confirm-improvement') {
|
||||
this.confirmGuidedSortieImprovement();
|
||||
return;
|
||||
}
|
||||
if (action?.kind === 'undo-improvement') {
|
||||
this.undoGuidedSortieImprovement();
|
||||
return;
|
||||
}
|
||||
if (action?.kind === 'confirm-recommendation' && action.recommendationId) {
|
||||
this.confirmPinnedSortieRecommendation(action.recommendationId);
|
||||
return;
|
||||
@@ -16051,6 +16165,21 @@ export class CampScene extends Phaser.Scene {
|
||||
return scenario && selection?.battleId === scenario.id ? selection.bondId : undefined;
|
||||
}
|
||||
|
||||
private validCurrentSortieResonanceBondId(
|
||||
scenario = this.nextSortieScenario(),
|
||||
selectedUnitIds: readonly string[] = this.selectedSortieUnitIds
|
||||
) {
|
||||
const bondId = this.currentSortieResonanceBondId(scenario);
|
||||
if (!scenario || !bondId) {
|
||||
return undefined;
|
||||
}
|
||||
const selected = new Set(selectedUnitIds);
|
||||
const bond = this.sortieSynergyBonds(scenario).find((candidate) => candidate.id === bondId);
|
||||
return bond && bond.level >= coreSortieResonanceMinLevel && bond.unitIds.every((unitId) => selected.has(unitId))
|
||||
? bond.id
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private sortieCoreResonanceCandidates(
|
||||
snapshot = this.sortieSynergySnapshot(),
|
||||
scenario = this.nextSortieScenario()
|
||||
@@ -16107,10 +16236,14 @@ export class CampScene extends Phaser.Scene {
|
||||
this.persistSortieSelection();
|
||||
const clearing = this.currentSortieResonanceBondId(scenario) === candidate.id;
|
||||
this.campaign = setCampaignSortieResonanceSelection(scenario.id, clearing ? undefined : candidate.id);
|
||||
const guidedRecalculated = this.resetRecommendationAfterSortieStrategyChange();
|
||||
const pairLabel = `${candidate.unitNames[0]}↔${candidate.unitNames[1]}`;
|
||||
this.sortiePlanFeedback = clearing
|
||||
? `핵심 공명조 해제 · ${pairLabel}`
|
||||
: `핵심 공명조 지정 · ${pairLabel} · 추격 ${candidate.coreChainRate}%`;
|
||||
if (guidedRecalculated) {
|
||||
this.sortiePlanFeedback += ' · 보완안 재계산';
|
||||
}
|
||||
this.sortieCoreResonancePage = 0;
|
||||
soundDirector.playSelect();
|
||||
this.showCampNotice(this.sortiePlanFeedback);
|
||||
@@ -16321,6 +16454,150 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.sortieRecommendationProjections(scenario).find((projection) => projection.recommendationId === recommendationId);
|
||||
}
|
||||
|
||||
private prepareGuidedSortieImprovement() {
|
||||
const report = this.report;
|
||||
const review = this.reportFormationReview();
|
||||
const scenario = this.nextSortieScenario();
|
||||
const outcome = review ? this.reportSortieRecommendationOutcome(review, report) : undefined;
|
||||
if (!report || !review || !scenario || !outcome) {
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
const plans = this.sortieRecommendationPlans(scenario).filter((plan) => plan.applicable && plan.selectedUnitIds.length > 0);
|
||||
const plan = plans.find((candidate) => candidate.id === outcome.planId) ?? plans[0];
|
||||
if (!plan) {
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
return false;
|
||||
}
|
||||
|
||||
const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit]));
|
||||
const currentFormationAssignments = this.selectedSortieUnitIds.reduce<SortieFormationAssignments>((assignments, unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
if (unit) {
|
||||
assignments[unitId] = this.sortieFormationRole(unit, scenario);
|
||||
}
|
||||
return assignments;
|
||||
}, {});
|
||||
const unitNames = Object.fromEntries(this.sortieRosterUnits().map((unit) => [unit.id, unit.name]));
|
||||
const proposal = buildSortieRecommendationImprovement({
|
||||
plan,
|
||||
selectedUnitIds: this.selectedSortieUnitIds,
|
||||
formationAssignments: currentFormationAssignments,
|
||||
requiredUnitIds: [...this.requiredSortieUnitIdsFor(scenario)],
|
||||
unitNames,
|
||||
previousContributions: outcome.unitContributions
|
||||
});
|
||||
this.sortieGuidedImprovement = {
|
||||
sourceBattleId: report.battleId,
|
||||
targetBattleId: scenario.id,
|
||||
orderId: this.currentSortieOrderId(),
|
||||
coreBondId: this.validCurrentSortieResonanceBondId(scenario),
|
||||
proposal
|
||||
};
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
this.sortieRecommendationUndoState = undefined;
|
||||
this.sortiePresetUndoState = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.closeSortieFormationPanelBrowser();
|
||||
this.sortieCoreResonanceSelectorOpen = false;
|
||||
this.sortiePrepStep = 'formation';
|
||||
this.sortieFocusedUnitId = proposal.focusUnitId && this.selectedSortieUnitIds.includes(proposal.focusUnitId)
|
||||
? proposal.focusUnitId
|
||||
: this.selectedSortieUnitIds[0];
|
||||
this.sortiePlanFeedback = `${proposal.title} · 적용 전 미리보기`;
|
||||
return true;
|
||||
}
|
||||
|
||||
private sortieGuidedImprovementProjection(
|
||||
state = this.sortieGuidedImprovement
|
||||
): SortieGuidedImprovementProjection | undefined {
|
||||
const scenario = this.nextSortieScenario();
|
||||
if (!state || !scenario || scenario.id !== state.targetBattleId) {
|
||||
return undefined;
|
||||
}
|
||||
const proposal = state.proposal;
|
||||
const rosterById = new Map(this.sortieRosterUnits().map((unit) => [unit.id, unit]));
|
||||
if (
|
||||
state.orderId !== this.currentSortieOrderId() ||
|
||||
state.coreBondId !== this.validCurrentSortieResonanceBondId(scenario) ||
|
||||
JSON.stringify(proposal.baselineSelectedUnitIds) !== JSON.stringify(this.selectedSortieUnitIds) ||
|
||||
proposal.baselineSelectedUnitIds.some((unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
return !unit || proposal.baselineFormationAssignments[unitId] !== this.sortieFormationRole(unit, scenario);
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selectedUnitIds = this.normalizedSortieUnitIds([...proposal.projectedSelectedUnitIds]);
|
||||
const selectedIds = new Set(selectedUnitIds);
|
||||
const requiredIds = this.requiredSortieUnitIdsFor(scenario);
|
||||
if (
|
||||
selectedUnitIds.length !== proposal.projectedSelectedUnitIds.length ||
|
||||
proposal.projectedSelectedUnitIds.some((unitId) => !selectedIds.has(unitId)) ||
|
||||
[...requiredIds].some((unitId) => rosterById.has(unitId) && !selectedIds.has(unitId)) ||
|
||||
selectedUnitIds.some((unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
return !unit || !this.sortieUnitAvailability(unit, scenario).available;
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const formationAssignments = selectedUnitIds.reduce<SortieFormationAssignments>((assignments, unitId) => {
|
||||
const unit = rosterById.get(unitId);
|
||||
if (unit) {
|
||||
assignments[unitId] = proposal.projectedFormationAssignments[unitId] ?? this.sortieFormationRole(unit, scenario);
|
||||
}
|
||||
return assignments;
|
||||
}, {});
|
||||
const itemAssignments = Object.entries(this.cloneSortieItemAssignments(this.sortieItemAssignments))
|
||||
.reduce<CampaignSortieItemAssignments>((assignments, [unitId, stocks]) => {
|
||||
if (selectedIds.has(unitId)) {
|
||||
assignments[unitId] = { ...stocks };
|
||||
}
|
||||
return assignments;
|
||||
}, {});
|
||||
return { selectedUnitIds, formationAssignments, itemAssignments };
|
||||
}
|
||||
|
||||
private createGuidedSortieRecommendationSnapshot(
|
||||
state: Pick<SortieGuidedImprovementState, 'targetBattleId' | 'proposal'>
|
||||
): CampaignSortieRecommendationSnapshot | undefined {
|
||||
const scenario = this.nextSortieScenario();
|
||||
if (!scenario || scenario.id !== state.targetBattleId) {
|
||||
return undefined;
|
||||
}
|
||||
const plan = this.sortieRecommendationPlans(scenario).find((candidate) => candidate.id === state.proposal.planId);
|
||||
if (!plan) {
|
||||
return undefined;
|
||||
}
|
||||
const sortieOrderId = this.currentSortieOrderId();
|
||||
const sortieResonanceBondId = this.validCurrentSortieResonanceBondId(scenario);
|
||||
const focusUnitId = state.proposal.focusUnitId;
|
||||
const unitReasons = Object.fromEntries(this.selectedSortieUnitIds.map((unitId) => [
|
||||
unitId,
|
||||
focusUnitId === unitId
|
||||
? state.proposal.detail
|
||||
: plan.unitReasons[unitId] ?? `${this.unitName(unitId)}은 보완 후 편성에서 기존 임무를 유지합니다.`
|
||||
]));
|
||||
return {
|
||||
version: 1,
|
||||
battleId: scenario.id,
|
||||
planId: state.proposal.planId,
|
||||
label: `${plan.label} · 보완안`,
|
||||
summary: state.proposal.title,
|
||||
...(sortieOrderId ? { sortieOrderId } : {}),
|
||||
...(sortieResonanceBondId ? { sortieResonanceBondId } : {}),
|
||||
selectedUnitIds: [...this.selectedSortieUnitIds],
|
||||
formationAssignments: { ...this.sortieFormationAssignments },
|
||||
unitReasons
|
||||
};
|
||||
}
|
||||
|
||||
private sortieRecommendationProjectionMatchesCurrent(projection: SortieRecommendationProjection) {
|
||||
if (JSON.stringify(projection.selectedUnitIds) !== JSON.stringify(this.selectedSortieUnitIds)) {
|
||||
return false;
|
||||
@@ -16487,6 +16764,60 @@ export class CampScene extends Phaser.Scene {
|
||||
if (this.sortieOrderBrowserOpen) {
|
||||
return undefined;
|
||||
}
|
||||
const guidedImprovement = this.sortieGuidedImprovement;
|
||||
if (guidedImprovement) {
|
||||
const projection = this.sortieGuidedImprovementProjection(guidedImprovement);
|
||||
if (projection) {
|
||||
const proposal = guidedImprovement.proposal;
|
||||
const projected = this.sortieSynergySnapshot(
|
||||
projection.selectedUnitIds,
|
||||
scenario,
|
||||
projection.formationAssignments
|
||||
);
|
||||
return {
|
||||
source: 'guided-improvement',
|
||||
mode: 'improvement',
|
||||
unitId: proposal.focusUnitId ?? proposal.planId,
|
||||
recommendationId: proposal.planId,
|
||||
incomingUnitId: proposal.kind === 'swap' || proposal.kind === 'add' ? proposal.incomingUnitId : null,
|
||||
incomingUnitName: proposal.kind === 'swap' || proposal.kind === 'add' ? proposal.incomingUnitName : null,
|
||||
outgoingUnitId: proposal.kind === 'swap' ? proposal.outgoingUnitId : null,
|
||||
outgoingUnitName: proposal.kind === 'swap' ? proposal.outgoingUnitName : null,
|
||||
selectedUnitIds,
|
||||
projectedSelectedUnitIds: projection.selectedUnitIds,
|
||||
headline: proposal.title,
|
||||
detail: proposal.kind === 'tactic' ? proposal.instruction : proposal.detail,
|
||||
metrics: this.sortiePresetComparisonMetrics(projection, scenario),
|
||||
current,
|
||||
projected,
|
||||
comparison: compareSortieSynergy(current, projected)
|
||||
};
|
||||
}
|
||||
}
|
||||
const guidedUndo = this.sortieGuidedImprovementUndoState;
|
||||
if (guidedUndo && this.sortiePersistedConfigurationMatches(guidedUndo.applied)) {
|
||||
const proposal = guidedUndo.proposal;
|
||||
return {
|
||||
source: 'guided-improvement',
|
||||
mode: 'improvement',
|
||||
unitId: proposal.focusUnitId ?? proposal.planId,
|
||||
recommendationId: proposal.planId,
|
||||
incomingUnitId: proposal.kind === 'swap' || proposal.kind === 'add' ? proposal.incomingUnitId : null,
|
||||
incomingUnitName: proposal.kind === 'swap' || proposal.kind === 'add' ? proposal.incomingUnitName : null,
|
||||
outgoingUnitId: proposal.kind === 'swap' ? proposal.outgoingUnitId : null,
|
||||
outgoingUnitName: proposal.kind === 'swap' ? proposal.outgoingUnitName : null,
|
||||
selectedUnitIds,
|
||||
projectedSelectedUnitIds: selectedUnitIds,
|
||||
headline: `${proposal.title} 적용 완료`,
|
||||
detail: proposal.kind === 'tactic' ? proposal.instruction : proposal.detail,
|
||||
metrics: this.sortiePresetComparisonMetrics({
|
||||
selectedUnitIds,
|
||||
formationAssignments: this.sortieFormationAssignments
|
||||
}, scenario),
|
||||
current,
|
||||
projected: current
|
||||
};
|
||||
}
|
||||
const recommendationId = this.sortieHoveredRecommendationId ?? this.sortiePinnedRecommendationId;
|
||||
if (recommendationId) {
|
||||
const projection = this.sortieRecommendationProjection(recommendationId);
|
||||
@@ -16805,12 +17136,29 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
const definition = sortieOrderDefinition(orderId);
|
||||
const changed = this.currentSortieOrderId() !== orderId;
|
||||
this.campaign = setCampaignSortieOrderSelection(scenario.id, orderId);
|
||||
const guidedRecalculated = changed
|
||||
? this.resetRecommendationAfterSortieStrategyChange()
|
||||
: false;
|
||||
this.sortiePlanFeedback = `${definition.label} 군령 선택 · ${definition.condition}`;
|
||||
if (guidedRecalculated) {
|
||||
this.sortiePlanFeedback += ' · 보완안 재계산';
|
||||
}
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private resetRecommendationAfterSortieStrategyChange() {
|
||||
const hadGuidedImprovement = Boolean(
|
||||
this.sortieGuidedImprovement || this.sortieGuidedImprovementUndoState
|
||||
);
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
this.persistSortieSelection(null);
|
||||
return hadGuidedImprovement ? this.prepareGuidedSortieImprovement() : false;
|
||||
}
|
||||
|
||||
private toggleSortiePresetBrowser(forceOpen?: boolean) {
|
||||
const open = forceOpen ?? !this.sortiePresetBrowserOpen;
|
||||
if (!open) {
|
||||
@@ -16973,6 +17321,86 @@ export class CampScene extends Phaser.Scene {
|
||||
this.showCampNotice(`${definition.label} 편성책 저장 완료 · 보급은 포함하지 않았습니다.`);
|
||||
}
|
||||
|
||||
private confirmGuidedSortieImprovement() {
|
||||
const state = this.sortieGuidedImprovement;
|
||||
const projection = this.sortieGuidedImprovementProjection(state);
|
||||
if (!state || !projection) {
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice('편성 조건이 달라졌습니다. 전투 보고에서 보완안을 다시 여십시오.');
|
||||
return;
|
||||
}
|
||||
|
||||
const beforeCoreBondId = this.currentSortieResonanceBondId();
|
||||
const beforeRecommendation = this.currentSortieRecommendationSelection();
|
||||
const before = this.captureSortieConfiguration();
|
||||
const proposal = state.proposal;
|
||||
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 = proposal.focusUnitId && projection.selectedUnitIds.includes(proposal.focusUnitId)
|
||||
? proposal.focusUnitId
|
||||
: projection.selectedUnitIds[0];
|
||||
this.sortieRosterScroll = 0;
|
||||
this.sortiePortraitRosterPage = 0;
|
||||
this.sortiePlanFeedback = proposal.kind === 'tactic'
|
||||
? `전술 행동 확인 · ${proposal.instruction}`
|
||||
: `${proposal.title} 적용 완료 · 장비·보급 재점검`;
|
||||
this.sortieGuidedImprovement = undefined;
|
||||
this.sortieGuidedImprovementUndoState = {
|
||||
sourceBattleId: state.sourceBattleId,
|
||||
targetBattleId: state.targetBattleId,
|
||||
proposal,
|
||||
beforeCoreBondId,
|
||||
beforeRecommendation,
|
||||
before,
|
||||
applied: this.captureSortieConfiguration()
|
||||
};
|
||||
this.persistSortieSelection(this.createGuidedSortieRecommendationSnapshot(state));
|
||||
this.closeSortieFormationPanelBrowser();
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
this.showCampNotice(
|
||||
proposal.kind === 'tactic'
|
||||
? `${proposal.instruction} 출진 전 행동 지침으로 기록했습니다.`
|
||||
: `${proposal.title} 적용 완료 · 바뀐 무장의 장비와 보급을 확인하십시오.`
|
||||
);
|
||||
}
|
||||
|
||||
private undoGuidedSortieImprovement() {
|
||||
const undo = this.sortieGuidedImprovementUndoState;
|
||||
if (!undo || !this.sortiePersistedConfigurationMatches(undo.applied)) {
|
||||
this.sortieGuidedImprovementUndoState = undefined;
|
||||
this.refreshCampaignSortieComparisonPanel();
|
||||
this.showCampNotice('이후 편성이 변경되어 직전 보완을 되돌릴 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sortieGuidedImprovementUndoState = 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(null);
|
||||
const scenario = this.nextSortieScenario();
|
||||
if (scenario) {
|
||||
this.campaign = setCampaignSortieResonanceSelection(scenario.id, undo.beforeCoreBondId);
|
||||
}
|
||||
if (undo.beforeRecommendation) {
|
||||
this.persistSortieSelection(undo.beforeRecommendation);
|
||||
}
|
||||
this.closeSortieFormationPanelBrowser();
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
this.showCampNotice(`${undo.proposal.title} 적용을 되돌렸습니다.`);
|
||||
}
|
||||
|
||||
private confirmPinnedSortieRecommendation(recommendationId: SortieRecommendationPlanId) {
|
||||
const preview = this.sortieFormationComparisonPreview();
|
||||
if (preview?.source !== 'pinned-recommendation' || preview.recommendationId !== recommendationId) {
|
||||
@@ -17243,7 +17671,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return undefined;
|
||||
}
|
||||
const sortieOrderId = this.currentSortieOrderId();
|
||||
const sortieResonanceBondId = this.currentSortieResonanceBondId(scenario);
|
||||
const sortieResonanceBondId = this.validCurrentSortieResonanceBondId(scenario, projection.selectedUnitIds);
|
||||
return {
|
||||
version: 1,
|
||||
battleId: scenario.id,
|
||||
@@ -17268,7 +17696,7 @@ export class CampScene extends Phaser.Scene {
|
||||
selection.formationAssignments[unitId] === this.sortieFormationAssignments[unitId]
|
||||
)) &&
|
||||
selection.sortieOrderId === this.currentSortieOrderId() &&
|
||||
selection.sortieResonanceBondId === this.currentSortieResonanceBondId(scenario)
|
||||
selection.sortieResonanceBondId === this.validCurrentSortieResonanceBondId(scenario)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18321,7 +18749,20 @@ export class CampScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
private currentSortieFlow() {
|
||||
private currentSortieFlow(): SortieFlow {
|
||||
if (this.retrySortieBattleId) {
|
||||
const scenario = getBattleScenario(this.retrySortieBattleId);
|
||||
return {
|
||||
afterBattleId: this.retrySortieBattleId,
|
||||
eyebrow: '재도전 준비',
|
||||
title: scenario.title,
|
||||
description: '직전 전투의 추천 검증을 바탕으로 한 자리만 보완한 뒤 같은 전장에 다시 출진합니다.',
|
||||
rewardHint: `재도전 목표: ${scenario.victoryConditionLabel}`,
|
||||
nextBattleId: this.retrySortieBattleId,
|
||||
campaignStep: this.campaign?.step,
|
||||
pages: []
|
||||
};
|
||||
}
|
||||
return getSortieFlow(this.campaign?.latestBattleId, this.campaign?.step);
|
||||
}
|
||||
|
||||
@@ -18531,7 +18972,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flow.nextBattleId || !flow.campaignStep || flow.pages.length === 0) {
|
||||
if (!flow.nextBattleId || !flow.campaignStep || (!this.skipIntroStoryForSortie && flow.pages.length === 0)) {
|
||||
this.hideSortiePrep();
|
||||
if (!flow.nextBattleId && flow.campaignStep && flow.pages.length > 0) {
|
||||
if (this.campaign?.step === flow.campaignStep) {
|
||||
@@ -19360,6 +19801,36 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderReportFormationReview();
|
||||
}
|
||||
|
||||
private reportSortieImprovementTargetBattleId(report = this.report): BattleScenarioId | undefined {
|
||||
if (!report) {
|
||||
return undefined;
|
||||
}
|
||||
if (report.outcome === 'defeat') {
|
||||
return report.battleId as BattleScenarioId;
|
||||
}
|
||||
return this.currentSortieFlow().nextBattleId;
|
||||
}
|
||||
|
||||
private openReportGuidedSortieImprovement() {
|
||||
const report = this.report;
|
||||
const targetBattleId = this.reportSortieImprovementTargetBattleId(report);
|
||||
if (!report || !targetBattleId) {
|
||||
this.showCampNotice('보완안을 적용할 다음 전장이 없습니다.');
|
||||
return;
|
||||
}
|
||||
this.retrySortieBattleId = report.outcome === 'defeat' ? targetBattleId : undefined;
|
||||
this.skipIntroStoryForSortie = report.outcome === 'defeat';
|
||||
this.sortieRecommendationPlanCache = undefined;
|
||||
this.hideReportFormationReview();
|
||||
if (!this.prepareGuidedSortieImprovement()) {
|
||||
this.showCampNotice('현재 편성 조건으로 보완안을 계산할 수 없습니다. 출전 전략 추천을 확인하십시오.');
|
||||
this.showSortiePrep();
|
||||
return;
|
||||
}
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
|
||||
private renderReportFormationReview() {
|
||||
const review = this.reportFormationReview();
|
||||
const report = this.report;
|
||||
@@ -19373,6 +19844,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.reportFormationReviewView = undefined;
|
||||
const recommendationOutcome = this.reportSortieRecommendationOutcome(review, report);
|
||||
const recommendationSnapshot = this.reportSortieRecommendationSnapshot(report);
|
||||
const improvementTargetBattleId = recommendationOutcome
|
||||
? this.reportSortieImprovementTargetBattleId(report)
|
||||
: undefined;
|
||||
const depth = 52;
|
||||
const width = 980;
|
||||
const height = 420;
|
||||
@@ -19445,8 +19919,8 @@ export class CampScene extends Phaser.Scene {
|
||||
: '전술 판단',
|
||||
recommendationOutcome
|
||||
? [
|
||||
`${recommendationOutcome.statusLabel} ${recommendationOutcome.score}점 · ${recommendationOutcome.kpis.map((kpi) => `${kpi.label} ${kpi.status === 'hit' ? '적중' : kpi.status === 'partial' ? '부분' : '미달'}`).join(' · ')}`,
|
||||
`다음 조정 · ${this.compactText(recommendationOutcome.improvement, 58)}`
|
||||
this.compactText(`${recommendationOutcome.statusLabel} ${recommendationOutcome.score}점 · ${recommendationOutcome.kpis.map((kpi) => `${kpi.label} ${kpi.status === 'hit' ? '적중' : kpi.status === 'partial' ? '부분' : '미달'}`).join(' · ')}`, improvementTargetBattleId ? 40 : 72),
|
||||
`다음 조정 · ${this.compactText(recommendationOutcome.improvement, improvementTargetBattleId ? 32 : 58)}`
|
||||
]
|
||||
: [
|
||||
`강점 · ${review.strengths.join(' · ')}`,
|
||||
@@ -19459,6 +19933,20 @@ export class CampScene extends Phaser.Scene {
|
||||
review.gradeColor,
|
||||
depth + 2
|
||||
);
|
||||
const improvementButton = recommendationOutcome && improvementTargetBattleId
|
||||
? this.addReportFormationReviewButton(
|
||||
report.outcome === 'defeat' ? '재도전 보완' : '다음 출전 보완',
|
||||
x + 456,
|
||||
analysisY + 18,
|
||||
146,
|
||||
36,
|
||||
true,
|
||||
false,
|
||||
() => this.openReportGuidedSortieImprovement(),
|
||||
depth + 3,
|
||||
palette.gold
|
||||
)
|
||||
: undefined;
|
||||
this.renderReportFormationAnalysisPanel(
|
||||
'역할·공명 기여',
|
||||
[
|
||||
@@ -19577,6 +20065,7 @@ export class CampScene extends Phaser.Scene {
|
||||
.setDepth(depth + 2);
|
||||
this.reportFormationReviewView = {
|
||||
closeButton,
|
||||
improvementButton,
|
||||
presetButtons,
|
||||
recommendationPanel: recommendationOutcome ? recommendationPanel : undefined,
|
||||
recommendationRows
|
||||
@@ -21529,13 +22018,17 @@ export class CampScene extends Phaser.Scene {
|
||||
private sortieAllies() {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const rule = this.nextSortieRule(scenario);
|
||||
return this.currentUnits().filter((unit) => this.sortieUnitAvailability(unit, scenario, rule).available);
|
||||
return this.sortieRosterUnits().filter((unit) => this.sortieUnitAvailability(unit, scenario, rule).available);
|
||||
}
|
||||
|
||||
private sortieRosterUnits() {
|
||||
const campaignOrder = new Map(this.currentUnits().map((unit, index) => [unit.id, index]));
|
||||
const restoresFreshRetryReadiness = Boolean(this.retrySortieBattleId && !this.campaign?.roster.length);
|
||||
return this.currentUnits()
|
||||
.filter((unit) => unit.faction === 'ally')
|
||||
.map((unit) => restoresFreshRetryReadiness && unit.hp <= 0
|
||||
? { ...unit, hp: unit.maxHp }
|
||||
: unit)
|
||||
.sort((a, b) => (campaignOrder.get(a.id) ?? 0) - (campaignOrder.get(b.id) ?? 0));
|
||||
}
|
||||
|
||||
@@ -21731,7 +22224,7 @@ export class CampScene extends Phaser.Scene {
|
||||
return true;
|
||||
}
|
||||
|
||||
private persistSortieSelection(recommendationSelection?: CampaignSortieRecommendationSnapshot) {
|
||||
private persistSortieSelection(recommendationSelection?: CampaignSortieRecommendationSnapshot | null) {
|
||||
this.reportFormationHistoryUndoState = undefined;
|
||||
this.sortieSwapUndoState = undefined;
|
||||
this.sortiePinnedSwapCandidateUnitId = undefined;
|
||||
@@ -21748,7 +22241,13 @@ export class CampScene extends Phaser.Scene {
|
||||
campaign.selectedSortieUnitIds = [...this.selectedSortieUnitIds];
|
||||
campaign.sortieFormationAssignments = { ...this.sortieFormationAssignments };
|
||||
campaign.sortieItemAssignments = this.cloneSortieItemAssignments(this.sortieItemAssignments);
|
||||
const activeRecommendation = recommendationSelection ?? campaign.sortieRecommendationSelection;
|
||||
const guidedRecommendation = this.sortieGuidedImprovementUndoState &&
|
||||
this.sortiePersistedConfigurationMatches(this.sortieGuidedImprovementUndoState.applied)
|
||||
? this.createGuidedSortieRecommendationSnapshot(this.sortieGuidedImprovementUndoState)
|
||||
: undefined;
|
||||
const activeRecommendation = recommendationSelection === null
|
||||
? undefined
|
||||
: recommendationSelection ?? guidedRecommendation ?? campaign.sortieRecommendationSelection;
|
||||
if (this.sortieRecommendationSelectionMatchesCurrent(activeRecommendation)) {
|
||||
campaign.sortieRecommendationSelection = {
|
||||
...activeRecommendation!,
|
||||
@@ -22119,6 +22618,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const reportRecommendationOutcome = reportFormationReview
|
||||
? this.reportSortieRecommendationOutcome(reportFormationReview)
|
||||
: undefined;
|
||||
const reportImprovementTargetBattleId = reportRecommendationOutcome
|
||||
? this.reportSortieImprovementTargetBattleId()
|
||||
: undefined;
|
||||
const reportRecommendationSnapshot = this.reportSortieRecommendationSnapshot();
|
||||
const reportFormationMatchingPresetIds = reportFormationReview
|
||||
? this.reportFormationSourcePresetIds(reportFormationReview.snapshot)
|
||||
@@ -22308,6 +22810,22 @@ export class CampScene extends Phaser.Scene {
|
||||
})
|
||||
}
|
||||
: null,
|
||||
improvementAction: reportRecommendationOutcome
|
||||
? {
|
||||
available: Boolean(reportImprovementTargetBattleId),
|
||||
label: reportImprovementTargetBattleId
|
||||
? this.report?.outcome === 'defeat' ? '재도전 보완' : '다음 출전 보완'
|
||||
: null,
|
||||
sourceBattleId: this.report?.battleId ?? null,
|
||||
targetBattleId: reportImprovementTargetBattleId ?? null,
|
||||
retry: this.report?.outcome === 'defeat',
|
||||
planId: reportRecommendationOutcome.planId,
|
||||
weakUnitIds: reportRecommendationOutcome.unitContributions
|
||||
.filter((entry) => entry.status !== 'hit')
|
||||
.map((entry) => entry.unitId),
|
||||
buttonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewView?.improvementButton)
|
||||
}
|
||||
: null,
|
||||
toggleButtonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewToggleButton),
|
||||
closeButtonBounds: this.sortieObjectBoundsDebug(this.reportFormationReviewView?.closeButton),
|
||||
presetCards: sortiePresetDefinitions.map((definition) => ({
|
||||
@@ -22328,6 +22846,7 @@ export class CampScene extends Phaser.Scene {
|
||||
overwriteConfirmId: null,
|
||||
feedback: '',
|
||||
recommendationFeedback: null,
|
||||
improvementAction: null,
|
||||
toggleButtonBounds: null,
|
||||
closeButtonBounds: null,
|
||||
presetCards: []
|
||||
@@ -22655,6 +23174,33 @@ export class CampScene extends Phaser.Scene {
|
||||
};
|
||||
})
|
||||
},
|
||||
sortieGuidedImprovement: this.sortieGuidedImprovement
|
||||
? {
|
||||
stage: 'preview',
|
||||
sourceBattleId: this.sortieGuidedImprovement.sourceBattleId,
|
||||
targetBattleId: this.sortieGuidedImprovement.targetBattleId,
|
||||
proposal: {
|
||||
...this.sortieGuidedImprovement.proposal,
|
||||
baselineSelectedUnitIds: [...this.sortieGuidedImprovement.proposal.baselineSelectedUnitIds],
|
||||
baselineFormationAssignments: { ...this.sortieGuidedImprovement.proposal.baselineFormationAssignments },
|
||||
projectedSelectedUnitIds: [...this.sortieGuidedImprovement.proposal.projectedSelectedUnitIds],
|
||||
projectedFormationAssignments: { ...this.sortieGuidedImprovement.proposal.projectedFormationAssignments }
|
||||
}
|
||||
}
|
||||
: this.sortieGuidedImprovementUndoState
|
||||
? {
|
||||
stage: 'applied',
|
||||
sourceBattleId: this.sortieGuidedImprovementUndoState.sourceBattleId,
|
||||
targetBattleId: this.sortieGuidedImprovementUndoState.targetBattleId,
|
||||
proposal: {
|
||||
...this.sortieGuidedImprovementUndoState.proposal,
|
||||
baselineSelectedUnitIds: [...this.sortieGuidedImprovementUndoState.proposal.baselineSelectedUnitIds],
|
||||
baselineFormationAssignments: { ...this.sortieGuidedImprovementUndoState.proposal.baselineFormationAssignments },
|
||||
projectedSelectedUnitIds: [...this.sortieGuidedImprovementUndoState.proposal.projectedSelectedUnitIds],
|
||||
projectedFormationAssignments: { ...this.sortieGuidedImprovementUndoState.proposal.projectedFormationAssignments }
|
||||
}
|
||||
}
|
||||
: null,
|
||||
sortieComparisonPreview: sortieComparisonPreview
|
||||
? {
|
||||
...sortieComparisonPreview,
|
||||
@@ -22698,6 +23244,24 @@ export class CampScene extends Phaser.Scene {
|
||||
beforeCoreBondId: this.sortieRecommendationUndoState.beforeCoreBondId ?? null
|
||||
}
|
||||
: null,
|
||||
sortieGuidedImprovementUndo: this.sortieGuidedImprovementUndoState
|
||||
? {
|
||||
available: sortieComparisonAction?.kind === 'undo-improvement',
|
||||
sourceBattleId: this.sortieGuidedImprovementUndoState.sourceBattleId,
|
||||
targetBattleId: this.sortieGuidedImprovementUndoState.targetBattleId,
|
||||
kind: this.sortieGuidedImprovementUndoState.proposal.kind,
|
||||
before: {
|
||||
selectedUnitIds: [...this.sortieGuidedImprovementUndoState.before.selectedUnitIds],
|
||||
formationAssignments: { ...this.sortieGuidedImprovementUndoState.before.formationAssignments },
|
||||
itemAssignments: this.cloneSortieItemAssignments(this.sortieGuidedImprovementUndoState.before.itemAssignments)
|
||||
},
|
||||
applied: {
|
||||
selectedUnitIds: [...this.sortieGuidedImprovementUndoState.applied.selectedUnitIds],
|
||||
formationAssignments: { ...this.sortieGuidedImprovementUndoState.applied.formationAssignments },
|
||||
itemAssignments: this.cloneSortieItemAssignments(this.sortieGuidedImprovementUndoState.applied.itemAssignments)
|
||||
}
|
||||
}
|
||||
: null,
|
||||
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
|
||||
sortieStrategyCoverage: {
|
||||
total: sortieStrategyCoverage.total,
|
||||
|
||||
Reference in New Issue
Block a user