feat: surface officer strategies in sortie planning
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
type EquipmentSlot,
|
||||
type ItemDefinition
|
||||
} from '../data/battleItems';
|
||||
import { compareUnitStrategyCoverage, getUnitStrategyCoverage, getUnitStrategyNames } 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';
|
||||
@@ -294,6 +295,7 @@ type SortieUnitTacticalSummary = {
|
||||
equipmentLine: string;
|
||||
bondLine: string;
|
||||
terrainLine: string;
|
||||
strategyLine: string;
|
||||
};
|
||||
|
||||
type SortieFocusedUnitSummary = SortieUnitTacticalSummary & {
|
||||
@@ -13268,6 +13270,14 @@ export class CampScene extends Phaser.Scene {
|
||||
this.textStyle(10, availability.available ? previewColor : '#68727e', selected || Boolean(recommendation))
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
cardX + 92,
|
||||
cardY + 70,
|
||||
this.compactText(this.sortieStrategyLine(unit.id), 20),
|
||||
this.textStyle(8, availability.available ? '#9fc8e8' : '#68727e', true)
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
|
||||
const canToggle = availability.available && !(selected && required);
|
||||
this.renderFirstSortieInlineButton(
|
||||
@@ -13733,15 +13743,22 @@ export class CampScene extends Phaser.Scene {
|
||||
const showMetricTransition = !isUndoAction && Boolean(preview?.projected);
|
||||
const comparison = isUndoAction ? undefined : preview?.comparison;
|
||||
const pursuitChanges = this.changedSortiePursuitPairs(current, projected);
|
||||
const strategyCoverageComparison = compareUnitStrategyCoverage(current.selectedUnitIds, projected.selectedUnitIds);
|
||||
const currentStrategyCoverage = strategyCoverageComparison.current;
|
||||
const projectedStrategyCoverage = strategyCoverageComparison.projected;
|
||||
const gainedStrategies = strategyCoverageComparison.gained;
|
||||
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' ||
|
||||
preview?.source === 'hover-preset' || preview?.source === 'pinned-preset';
|
||||
const lostRole = Boolean(comparison?.lostRoles.length);
|
||||
const lostStrategyCoverage = strategyCoverageDelta < 0;
|
||||
const completeFormation = projected.coveredRoleCount === coreSortieSynergyRoles.length;
|
||||
view.background.setStrokeStyle(
|
||||
lostRole ? 2 : 1,
|
||||
lostRole ? palette.red : isHoverPreview ? palette.gold : completeFormation ? palette.green : palette.gold,
|
||||
lostRole ? 0.78 : isHoverPreview ? 0.72 : 0.58
|
||||
lostRole || lostStrategyCoverage ? 2 : 1,
|
||||
lostRole || lostStrategyCoverage ? palette.red : isHoverPreview ? palette.gold : completeFormation ? palette.green : palette.gold,
|
||||
lostRole || lostStrategyCoverage ? 0.78 : isHoverPreview ? 0.72 : 0.58
|
||||
);
|
||||
|
||||
if (this.sortieCoreResonanceSelectorOpen) {
|
||||
@@ -13786,14 +13803,18 @@ export class CampScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
view.title.setText('편성 변화');
|
||||
view.summary.setText(`추격 후보 ${pursuitChanges.currentPairs.length}조 · 역할 ${current.coveredRoleCount}/3 · 지형 ${current.terrainGrade}`);
|
||||
view.summary.setText(`전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조`);
|
||||
view.headline.setText('출전 무장을 클릭해 교체 기준으로 고정하십시오.').setColor('#9fb0bf');
|
||||
view.metrics.forEach((metric) => {
|
||||
metric.background.setFillStyle(0x111922, 0.9).setStrokeStyle(1, 0x53606c, 0.34);
|
||||
metric.value.setText('-').setColor('#77818c');
|
||||
});
|
||||
view.impact.setText('후보 초상에 마우스를 올리면 역할·추격 후보 변화가 표시됩니다.').setColor('#d4dce6');
|
||||
view.detail.setText('미리보기는 실제 출전 명단과 보급을 변경하지 않습니다.').setColor('#9fb0bf');
|
||||
view.impact
|
||||
.setText(`전법 ${currentStrategyCoverage.covered.map((definition) => definition.label).join('·') || '없음'} · 후보 초상에 마우스를 올려 변화를 비교하세요.`)
|
||||
.setColor(currentStrategyCoverage.count === currentStrategyCoverage.total ? '#a8ffd0' : '#d4dce6');
|
||||
view.detail
|
||||
.setText(currentStrategyCoverage.missing.length > 0 ? `빈 전법 ${currentStrategyCoverage.missing.map((definition) => definition.label).join('·')} · 미리보기는 실제 편성을 변경하지 않습니다.` : '전법 역할이 모두 갖춰졌습니다. 미리보기는 실제 편성을 변경하지 않습니다.')
|
||||
.setColor(currentStrategyCoverage.missing.length > 0 ? '#ffdf7b' : '#9fb0bf');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13813,7 +13834,7 @@ export class CampScene extends Phaser.Scene {
|
||||
view.title.setText(this.compactText(title, 18));
|
||||
view.summary.setText(
|
||||
this.compactText(
|
||||
`추격 후보 ${pursuitChanges.currentPairs.length}→${pursuitChanges.projectedPairs.length}조 · 역할 ${current.coveredRoleCount}/3→${projected.coveredRoleCount}/3 · 지형 ${projected.terrainGrade}`,
|
||||
`전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total}→${projectedStrategyCoverage.count}/${projectedStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3→${projected.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}→${pursuitChanges.projectedPairs.length}조`,
|
||||
26
|
||||
)
|
||||
);
|
||||
@@ -13856,7 +13877,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
if (action?.kind === 'undo-preset' && this.sortiePresetUndoState) {
|
||||
view.impact
|
||||
.setText(`현재 역할 ${current.coveredRoleCount}/3 · 추격 후보 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
.setText(`현재 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText('유지 가능한 보급만 보존했습니다. 직전 적용은 한 번 되돌릴 수 있습니다.')
|
||||
@@ -13866,7 +13887,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
if (isUndoAction && this.sortieSwapUndoState) {
|
||||
view.impact
|
||||
.setText(`현재 역할 ${current.coveredRoleCount}/3 · 추격 후보 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
.setText(`현재 전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total} · 역할 ${current.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}조 · 지형 ${current.terrainGrade}`)
|
||||
.setColor('#a8ffd0');
|
||||
view.detail
|
||||
.setText('장비·보급은 자동 이전하지 않았습니다. 직전 교체는 한 번 되돌릴 수 있습니다.')
|
||||
@@ -13893,14 +13914,15 @@ export class CampScene extends Phaser.Scene {
|
||||
const roleChange = roleChanges.join(' · ') || '역할 유지';
|
||||
const pursuitDelta = pursuitChanges.projectedPairs.length - pursuitChanges.currentPairs.length;
|
||||
const signedPursuitDelta = pursuitDelta > 0 ? `+${pursuitDelta}` : `${pursuitDelta}`;
|
||||
const strategyCoverageChange = `전법 ${currentStrategyCoverage.count}/${currentStrategyCoverage.total}→${projectedStrategyCoverage.count}/${projectedStrategyCoverage.total}`;
|
||||
view.impact
|
||||
.setText(
|
||||
this.compactText(
|
||||
`역할 ${current.coveredRoleCount}/3→${projected.coveredRoleCount}/3 · 추격 후보 ${pursuitChanges.currentPairs.length}→${pursuitChanges.projectedPairs.length}조 (${signedPursuitDelta}) · ${roleChange}`,
|
||||
`${strategyCoverageChange} · 역할 ${current.coveredRoleCount}/3→${projected.coveredRoleCount}/3 · 추격 ${pursuitChanges.currentPairs.length}→${pursuitChanges.projectedPairs.length}조 (${signedPursuitDelta}) · ${roleChange}`,
|
||||
68
|
||||
)
|
||||
)
|
||||
.setColor(lostRoleLabels.length > 0 || pursuitDelta < 0 ? '#ff9d7d' : gainedRoleLabels.length > 0 || pursuitDelta > 0 ? '#a8ffd0' : '#d4dce6');
|
||||
.setColor(lostRoleLabels.length > 0 || strategyCoverageDelta < 0 || pursuitDelta < 0 ? '#ff9d7d' : gainedRoleLabels.length > 0 || strategyCoverageDelta > 0 || pursuitDelta > 0 ? '#a8ffd0' : '#d4dce6');
|
||||
|
||||
const gainedPursuits = pursuitChanges.gainedPairs.map((pair) => `추격 개방 ${this.sortiePursuitPairLabel(pair)}`);
|
||||
const lostPursuits = pursuitChanges.lostPairs.map((pair) => `추격 해제 ${this.sortiePursuitPairLabel(pair)}`);
|
||||
@@ -13910,10 +13932,14 @@ export class CampScene extends Phaser.Scene {
|
||||
.join(' · ') || '추격 변화 없음';
|
||||
const terrainDelta = projected.terrainScore - current.terrainScore;
|
||||
const signedTerrainDelta = terrainDelta > 0 ? `+${terrainDelta}` : `${terrainDelta}`;
|
||||
const strategyChanges = [
|
||||
gainedStrategies.length > 0 ? `전법 보강 ${gainedStrategies.map((definition) => definition.label).join('·')}` : '',
|
||||
lostStrategies.length > 0 ? `전법 공백 ${lostStrategies.map((definition) => definition.label).join('·')}` : ''
|
||||
].filter(Boolean).join(' · ') || '전법 유지';
|
||||
view.detail
|
||||
.setText(
|
||||
this.compactText(
|
||||
`${pursuitLine} · 지형 ${current.terrainGrade} ${current.terrainScore}→${projected.terrainGrade} ${projected.terrainScore} (${signedTerrainDelta}) · ${roleChange} · 실제 편성 미변경`,
|
||||
`${strategyChanges} · ${pursuitLine} · 지형 ${current.terrainGrade} ${current.terrainScore}→${projected.terrainGrade} ${projected.terrainScore} (${signedTerrainDelta}) · ${roleChange} · 실제 편성 미변경`,
|
||||
82
|
||||
)
|
||||
)
|
||||
@@ -14115,6 +14141,7 @@ export class CampScene extends Phaser.Scene {
|
||||
})
|
||||
).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 14, cardY + 278, this.compactText(this.sortieBondLine(unit), 26), this.textStyle(10, selected ? '#a8ffd0' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 14, cardY + 294, this.compactText(this.sortieStrategyLine(unit.id), 24), this.textStyle(10, selected ? '#9fc8e8' : '#77818c', true))).setDepth(depth + 2);
|
||||
|
||||
this.renderFirstSortieInlineButton(
|
||||
required ? '필수 출전' : selected ? '출전 확정' : '대기',
|
||||
@@ -14807,6 +14834,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const hasBattle = Boolean(scenario);
|
||||
const pursuit = this.sortiePursuitPairs();
|
||||
const strategyCoverage = this.sortieStrategyCoverage();
|
||||
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';
|
||||
@@ -14847,8 +14875,8 @@ export class CampScene extends Phaser.Scene {
|
||||
x + 18,
|
||||
y + 38,
|
||||
hasBattle
|
||||
? this.compactText(`군령 ${selectedOrder?.label ?? '미선택'} · ${plan.selectedCount}/${plan.maxCount}명 · 추격 후보 ${pursuit.activePairs.length}조 · 지형 ${plan.terrainGrade} · ${this.sortieRecommendedClassLine()}`, 54)
|
||||
: `${plan.selectedCount}/${plan.maxCount}명 · 군영 의정 · ${this.sortieRecommendedClassLine()}`,
|
||||
? this.compactText(`군령 ${selectedOrder?.label ?? '미선택'} · ${plan.selectedCount}/${plan.maxCount}명 · 전법 ${strategyCoverage.count}/${strategyCoverage.total} · 추격 ${pursuit.activePairs.length}조 · 지형 ${plan.terrainGrade} · ${this.sortieRecommendedClassLine()}`, 54)
|
||||
: `${plan.selectedCount}/${plan.maxCount}명 · 전법 ${strategyCoverage.count}/${strategyCoverage.total} · 군영 의정 · ${this.sortieRecommendedClassLine()}`,
|
||||
this.textStyle(11, '#d8b15f', true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
@@ -15026,7 +15054,12 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderSortieStatChip('move', '이', unit.move, x + 252, statY, chipWidth, depth + 1);
|
||||
|
||||
this.trackSortie(this.add.text(x + 18, y + 137, `지형 ${terrainGrade} ${terrainScore} · ${this.sortieTerrainLine(unit)}`, this.textStyle(11, '#d8b15f', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 157, this.compactText(synergyPreview.headline, 38), this.textStyle(11, synergyColor, true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 157, this.compactText(this.sortieStrategyLine(unit.id), 18), this.textStyle(10, '#9fc8e8', true))
|
||||
).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 154, y + 157, this.compactText(synergyPreview.headline, 25), this.textStyle(10, synergyColor, true))
|
||||
).setDepth(depth + 1);
|
||||
|
||||
equipmentSlots.forEach((slot, index) => {
|
||||
const rowY = y + 174 + index * 20;
|
||||
@@ -15238,10 +15271,20 @@ export class CampScene extends Phaser.Scene {
|
||||
statLine: `무 ${unit.stats.might} 지 ${unit.stats.intelligence} 통 ${unit.stats.leadership} 민 ${unit.stats.agility} 운 ${unit.stats.luck}`,
|
||||
equipmentLine: this.sortieEquipmentLine(unit),
|
||||
bondLine: this.sortieBondLine(unit),
|
||||
terrainLine: this.sortieTerrainLine(unit)
|
||||
terrainLine: this.sortieTerrainLine(unit),
|
||||
strategyLine: this.sortieStrategyLine(unit.id)
|
||||
};
|
||||
}
|
||||
|
||||
private sortieStrategyLine(unitId: string) {
|
||||
const names = getUnitStrategyNames([unitId]);
|
||||
return names.length > 0 ? `전법 ${names.join('·')}` : '전법 없음';
|
||||
}
|
||||
|
||||
private sortieStrategyCoverage(unitIds = this.selectedSortieUnitIds) {
|
||||
return getUnitStrategyCoverage(unitIds);
|
||||
}
|
||||
|
||||
private sortieEquipmentLine(unit: UnitData) {
|
||||
return equipmentSlots
|
||||
.map((slot) => {
|
||||
@@ -16615,6 +16658,7 @@ export class CampScene extends Phaser.Scene {
|
||||
equipmentLine: summary.equipmentLine,
|
||||
bondLine: summary.bondLine,
|
||||
terrainLine: summary.terrainLine,
|
||||
strategyLine: summary.strategyLine,
|
||||
equipment: equipmentSlots.map((slot) => {
|
||||
const state = unit.equipment[slot];
|
||||
const item = getItem(state.itemId);
|
||||
@@ -20874,6 +20918,13 @@ export class CampScene extends Phaser.Scene {
|
||||
const portraitRosterMaxScroll = Math.max(0, (portraitRosterPageCount - 1) * sortiePortraitRosterPageSize);
|
||||
const sortieComparisonPreview = this.sortieFormationComparisonPreview();
|
||||
const sortieComparisonAction = this.sortieComparisonAction(sortieComparisonPreview);
|
||||
const sortieStrategyCoverage = this.sortieStrategyCoverage();
|
||||
const sortieComparisonStrategyCoverage = sortieComparisonPreview
|
||||
? compareUnitStrategyCoverage(
|
||||
sortieComparisonPreview.current.selectedUnitIds,
|
||||
sortieComparisonPreview.projected?.selectedUnitIds ?? sortieComparisonPreview.current.selectedUnitIds
|
||||
)
|
||||
: null;
|
||||
const sortiePursuit = this.sortiePursuitPairs();
|
||||
const sortieCoreResonanceCandidates = this.sortieCoreResonanceCandidates();
|
||||
const sortieCoreResonanceSelection = this.campaign?.sortieResonanceSelection;
|
||||
@@ -21246,6 +21297,8 @@ export class CampScene extends Phaser.Scene {
|
||||
available: availability.available,
|
||||
availabilityLabel: availability.label,
|
||||
availabilityReason: availability.reason,
|
||||
strategyNames: getUnitStrategyNames([unit.id]),
|
||||
strategyLine: this.sortieStrategyLine(unit.id),
|
||||
portraitTextureKey: unitPortraitTextureKey ?? null,
|
||||
portraitReady: Boolean(unitPortraitTextureKey && this.textures.exists(unitPortraitTextureKey))
|
||||
};
|
||||
@@ -21312,7 +21365,12 @@ export class CampScene extends Phaser.Scene {
|
||||
};
|
||||
})
|
||||
},
|
||||
sortieComparisonPreview: sortieComparisonPreview ?? null,
|
||||
sortieComparisonPreview: sortieComparisonPreview
|
||||
? {
|
||||
...sortieComparisonPreview,
|
||||
strategyCoverage: sortieComparisonStrategyCoverage
|
||||
}
|
||||
: null,
|
||||
sortieComparisonAction: sortieComparisonAction ? { ...sortieComparisonAction, enabled: true } : null,
|
||||
sortieSwapUndo: this.sortieSwapUndoState
|
||||
? {
|
||||
@@ -21328,6 +21386,12 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
: null,
|
||||
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
|
||||
sortieStrategyCoverage: {
|
||||
total: sortieStrategyCoverage.total,
|
||||
count: sortieStrategyCoverage.count,
|
||||
covered: sortieStrategyCoverage.covered.map((definition) => ({ ...definition })),
|
||||
missing: sortieStrategyCoverage.missing.map((definition) => ({ ...definition }))
|
||||
},
|
||||
sortiePlanFeedback: this.sortiePlanFeedback,
|
||||
sortieChecklist,
|
||||
sortieChecklistSummary: this.sortieChecklistSummary(sortieChecklist),
|
||||
@@ -21369,7 +21433,8 @@ export class CampScene extends Phaser.Scene {
|
||||
statLine: summary.statLine,
|
||||
equipmentLine: summary.equipmentLine,
|
||||
bondLine: summary.bondLine,
|
||||
terrainLine: summary.terrainLine
|
||||
terrainLine: summary.terrainLine,
|
||||
strategyLine: summary.strategyLine
|
||||
};
|
||||
}),
|
||||
equipmentInventory: this.equipmentInventoryEntries().map((entry) => ({
|
||||
|
||||
Reference in New Issue
Block a user