feat: surface officer strategies in sortie planning
This commit is contained in:
218
src/game/data/battleUsables.ts
Normal file
218
src/game/data/battleUsables.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
export type UsableCommand = 'strategy' | 'item';
|
||||
|
||||
export type UsableEffect = 'damage' | 'heal' | 'focus';
|
||||
|
||||
export type UsableTarget = 'enemy' | 'ally' | 'self';
|
||||
|
||||
export type BattleStatusKind = 'burn' | 'confusion';
|
||||
|
||||
export type BattleUsable = {
|
||||
id: string;
|
||||
command: UsableCommand;
|
||||
name: string;
|
||||
target: UsableTarget;
|
||||
effect: UsableEffect;
|
||||
range: number;
|
||||
power: number;
|
||||
accuracyBonus?: number;
|
||||
criticalBonus?: number;
|
||||
attackBonus?: number;
|
||||
hitBonus?: number;
|
||||
duration?: number;
|
||||
statusEffect?: BattleStatusKind;
|
||||
statusDuration?: number;
|
||||
statusPower?: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const usableCatalog: Record<string, BattleUsable> = {
|
||||
aid: {
|
||||
id: 'aid',
|
||||
command: 'strategy',
|
||||
name: '응급',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 2,
|
||||
power: 10,
|
||||
description: '사거리 2칸의 작은 책략 회복. 위험한 아군을 멀리서 살린다.'
|
||||
},
|
||||
encourage: {
|
||||
id: 'encourage',
|
||||
command: 'strategy',
|
||||
name: '격려',
|
||||
target: 'ally',
|
||||
effect: 'focus',
|
||||
range: 2,
|
||||
power: 0,
|
||||
attackBonus: 2,
|
||||
hitBonus: 10,
|
||||
criticalBonus: 4,
|
||||
duration: 2,
|
||||
description: '아군의 공격과 명중을 안정적으로 끌어올린다.'
|
||||
},
|
||||
fireTactic: {
|
||||
id: 'fireTactic',
|
||||
command: 'strategy',
|
||||
name: '화계',
|
||||
target: 'enemy',
|
||||
effect: 'damage',
|
||||
range: 2,
|
||||
power: 10,
|
||||
accuracyBonus: 4,
|
||||
criticalBonus: 0,
|
||||
statusEffect: 'burn',
|
||||
statusDuration: 2,
|
||||
statusPower: 3,
|
||||
description: '적 한 부대에 책략 피해와 짧은 화상 지속 피해를 준다.'
|
||||
},
|
||||
roar: {
|
||||
id: 'roar',
|
||||
command: 'strategy',
|
||||
name: '고함',
|
||||
target: 'enemy',
|
||||
effect: 'damage',
|
||||
range: 1,
|
||||
power: 7,
|
||||
accuracyBonus: 10,
|
||||
criticalBonus: 0,
|
||||
statusEffect: 'confusion',
|
||||
statusDuration: 2,
|
||||
statusPower: 1,
|
||||
description: '인접한 적을 위압해 다음 행동의 공격, 명중, 치명을 낮춘다.'
|
||||
},
|
||||
bean: {
|
||||
id: 'bean',
|
||||
command: 'item',
|
||||
name: '콩',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 1,
|
||||
power: 12,
|
||||
description: '인접 아군을 빠르게 소량 회복하는 기본 보급품.'
|
||||
},
|
||||
salve: {
|
||||
id: 'salve',
|
||||
command: 'item',
|
||||
name: '상처약',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 1,
|
||||
power: 22,
|
||||
description: '인접 아군의 큰 피해를 되돌리는 고가 회복품.'
|
||||
},
|
||||
wine: {
|
||||
id: 'wine',
|
||||
command: 'item',
|
||||
name: '술',
|
||||
target: 'self',
|
||||
effect: 'focus',
|
||||
range: 0,
|
||||
power: 0,
|
||||
attackBonus: 3,
|
||||
hitBonus: 4,
|
||||
criticalBonus: 12,
|
||||
duration: 2,
|
||||
description: '사용자 자신을 짧게 몰아붙이는 공격·치명 강화 도구.'
|
||||
}
|
||||
};
|
||||
|
||||
export const unitStrategyIds: Record<string, string[]> = {
|
||||
'liu-bei': ['aid', 'encourage'],
|
||||
'guan-yu': ['fireTactic'],
|
||||
'zhang-fei': ['roar'],
|
||||
'jian-yong': ['aid', 'encourage', 'fireTactic'],
|
||||
'mi-zhu': ['aid', 'encourage'],
|
||||
'sun-qian': ['aid', 'encourage', 'fireTactic'],
|
||||
'zhao-yun': ['encourage'],
|
||||
'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
|
||||
'ma-liang': ['aid', 'encourage', 'fireTactic'],
|
||||
'yi-ji': ['aid', 'encourage'],
|
||||
'gong-zhi': ['encourage'],
|
||||
'huang-zhong': ['encourage'],
|
||||
'wei-yan': ['roar'],
|
||||
'pang-tong': ['aid', 'encourage', 'fireTactic'],
|
||||
'fa-zheng': ['aid', 'encourage', 'fireTactic'],
|
||||
'wu-yi': ['encourage'],
|
||||
'yan-yan': ['encourage'],
|
||||
'li-yan': ['aid', 'encourage'],
|
||||
'huang-quan': ['aid', 'encourage'],
|
||||
'ma-chao': ['roar', 'encourage'],
|
||||
'ma-dai': ['encourage'],
|
||||
'wang-ping': ['aid', 'encourage'],
|
||||
'jiang-wei': ['aid', 'encourage', 'fireTactic']
|
||||
};
|
||||
|
||||
export const strategyCoverageDefinitions = [
|
||||
{ id: 'healing', label: '회복', usableId: 'aid' },
|
||||
{ id: 'buff', label: '강화', usableId: 'encourage' },
|
||||
{ id: 'fire', label: '화공', usableId: 'fireTactic' },
|
||||
{ id: 'control', label: '제어', usableId: 'roar' }
|
||||
] as const;
|
||||
|
||||
export type StrategyCoverageDefinition = (typeof strategyCoverageDefinitions)[number];
|
||||
|
||||
export type UnitStrategyCoverage = {
|
||||
total: number;
|
||||
count: number;
|
||||
covered: StrategyCoverageDefinition[];
|
||||
missing: StrategyCoverageDefinition[];
|
||||
};
|
||||
|
||||
export type UnitStrategyCoverageComparison = {
|
||||
current: UnitStrategyCoverage;
|
||||
projected: UnitStrategyCoverage;
|
||||
gained: StrategyCoverageDefinition[];
|
||||
lost: StrategyCoverageDefinition[];
|
||||
delta: number;
|
||||
trend: 'gain' | 'loss' | 'even';
|
||||
};
|
||||
|
||||
function uniqueUnitStrategyIds(unitIds: readonly string[]) {
|
||||
const strategyIds = new Set<string>();
|
||||
unitIds.forEach((unitId) => {
|
||||
(unitStrategyIds[unitId] ?? []).forEach((strategyId) => strategyIds.add(strategyId));
|
||||
});
|
||||
return strategyIds;
|
||||
}
|
||||
|
||||
export function getUnitStrategyCoverage(unitIds: readonly string[]): UnitStrategyCoverage {
|
||||
const strategyIds = uniqueUnitStrategyIds(unitIds);
|
||||
const covered = strategyCoverageDefinitions.filter((definition) => strategyIds.has(definition.usableId));
|
||||
const missing = strategyCoverageDefinitions.filter((definition) => !strategyIds.has(definition.usableId));
|
||||
|
||||
return {
|
||||
total: strategyCoverageDefinitions.length,
|
||||
count: covered.length,
|
||||
covered: [...covered],
|
||||
missing: [...missing]
|
||||
};
|
||||
}
|
||||
|
||||
export function getUnitStrategyNames(unitIds: readonly string[]) {
|
||||
return [...uniqueUnitStrategyIds(unitIds)]
|
||||
.map((strategyId) => usableCatalog[strategyId])
|
||||
.filter((usable): usable is BattleUsable => Boolean(usable))
|
||||
.map((usable) => usable.name);
|
||||
}
|
||||
|
||||
export function compareUnitStrategyCoverage(
|
||||
currentUnitIds: readonly string[],
|
||||
projectedUnitIds: readonly string[]
|
||||
): UnitStrategyCoverageComparison {
|
||||
const current = getUnitStrategyCoverage(currentUnitIds);
|
||||
const projected = getUnitStrategyCoverage(projectedUnitIds);
|
||||
const currentIds = new Set(current.covered.map((definition) => definition.id));
|
||||
const projectedIds = new Set(projected.covered.map((definition) => definition.id));
|
||||
const gained = projected.covered.filter((definition) => !currentIds.has(definition.id));
|
||||
const lost = current.covered.filter((definition) => !projectedIds.has(definition.id));
|
||||
const delta = projected.count - current.count;
|
||||
|
||||
return {
|
||||
current,
|
||||
projected,
|
||||
gained,
|
||||
lost,
|
||||
delta,
|
||||
trend: delta > 0 ? 'gain' : delta < 0 ? 'loss' : 'even'
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,13 @@ import {
|
||||
loadBattleUiIcons,
|
||||
type BattleUiIconKey
|
||||
} from '../data/battleUiIcons';
|
||||
import {
|
||||
usableCatalog,
|
||||
unitStrategyIds,
|
||||
type BattleStatusKind,
|
||||
type BattleUsable,
|
||||
type UsableCommand
|
||||
} from '../data/battleUsables';
|
||||
import {
|
||||
equipmentExpToNext,
|
||||
equipmentSlotLabels,
|
||||
@@ -1185,11 +1192,8 @@ type UnitActionPose = 'attack' | 'strategy' | 'item' | 'hurt' | 'celebrate';
|
||||
const levelUpCelebrationStepOffsets = [0, -7, -2, -9, 0, -6, -3, -8];
|
||||
|
||||
type BattlePhase = 'deployment' | 'idle' | 'moving' | 'command' | 'targeting' | 'animating' | 'resolved';
|
||||
type BattleCommand = 'attack' | 'strategy' | 'item' | 'wait';
|
||||
type BattleCommand = 'attack' | UsableCommand | 'wait';
|
||||
type DamageCommand = Exclude<BattleCommand, 'wait'>;
|
||||
type UsableCommand = Extract<BattleCommand, 'strategy' | 'item'>;
|
||||
type UsableEffect = 'damage' | 'heal' | 'focus';
|
||||
type UsableTarget = 'enemy' | 'ally' | 'self';
|
||||
type SpecialDamageMotionKind = 'item' | 'arcane' | 'fire' | 'roar';
|
||||
type RosterTab = 'ally' | 'enemy';
|
||||
type ActiveFaction = 'ally' | 'enemy';
|
||||
@@ -1614,25 +1618,6 @@ type CombatResult = CombatPreview & {
|
||||
counter?: CombatResult;
|
||||
};
|
||||
|
||||
type BattleUsable = {
|
||||
id: string;
|
||||
command: UsableCommand;
|
||||
name: string;
|
||||
target: UsableTarget;
|
||||
effect: UsableEffect;
|
||||
range: number;
|
||||
power: number;
|
||||
accuracyBonus?: number;
|
||||
criticalBonus?: number;
|
||||
attackBonus?: number;
|
||||
hitBonus?: number;
|
||||
duration?: number;
|
||||
statusEffect?: BattleStatusKind;
|
||||
statusDuration?: number;
|
||||
statusPower?: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type SupportPreview = {
|
||||
usable: BattleUsable;
|
||||
user: UnitData;
|
||||
@@ -1803,8 +1788,6 @@ type TacticalInitiativeCombatEffect = {
|
||||
labels: string[];
|
||||
};
|
||||
|
||||
type BattleStatusKind = 'burn' | 'confusion';
|
||||
|
||||
type BattleStatusState = {
|
||||
unitId: string;
|
||||
kind: BattleStatusKind;
|
||||
@@ -1879,123 +1862,6 @@ let battleSaveStorageKey = `heros-web:battle:${battleScenario.id}`;
|
||||
let initialBattleUnits = battleUnits.map(cloneUnitData);
|
||||
let initialBattleBonds = battleBonds.map(cloneBattleBond);
|
||||
|
||||
const usableCatalog: Record<string, BattleUsable> = {
|
||||
aid: {
|
||||
id: 'aid',
|
||||
command: 'strategy',
|
||||
name: '응급',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 2,
|
||||
power: 10,
|
||||
description: '사거리 2칸의 작은 책략 회복. 위험한 아군을 멀리서 살린다.'
|
||||
},
|
||||
encourage: {
|
||||
id: 'encourage',
|
||||
command: 'strategy',
|
||||
name: '격려',
|
||||
target: 'ally',
|
||||
effect: 'focus',
|
||||
range: 2,
|
||||
power: 0,
|
||||
attackBonus: 2,
|
||||
hitBonus: 10,
|
||||
criticalBonus: 4,
|
||||
duration: 2,
|
||||
description: '아군의 공격과 명중을 안정적으로 끌어올린다.'
|
||||
},
|
||||
fireTactic: {
|
||||
id: 'fireTactic',
|
||||
command: 'strategy',
|
||||
name: '화계',
|
||||
target: 'enemy',
|
||||
effect: 'damage',
|
||||
range: 2,
|
||||
power: 10,
|
||||
accuracyBonus: 4,
|
||||
criticalBonus: 0,
|
||||
statusEffect: 'burn',
|
||||
statusDuration: 2,
|
||||
statusPower: 3,
|
||||
description: '적 한 부대에 책략 피해와 짧은 화상 지속 피해를 준다.'
|
||||
},
|
||||
roar: {
|
||||
id: 'roar',
|
||||
command: 'strategy',
|
||||
name: '고함',
|
||||
target: 'enemy',
|
||||
effect: 'damage',
|
||||
range: 1,
|
||||
power: 7,
|
||||
accuracyBonus: 10,
|
||||
criticalBonus: 0,
|
||||
statusEffect: 'confusion',
|
||||
statusDuration: 2,
|
||||
statusPower: 1,
|
||||
description: '인접한 적을 위압해 다음 행동의 공격, 명중, 치명을 낮춘다.'
|
||||
},
|
||||
bean: {
|
||||
id: 'bean',
|
||||
command: 'item',
|
||||
name: '콩',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 1,
|
||||
power: 12,
|
||||
description: '인접 아군을 빠르게 소량 회복하는 기본 보급품.'
|
||||
},
|
||||
salve: {
|
||||
id: 'salve',
|
||||
command: 'item',
|
||||
name: '상처약',
|
||||
target: 'ally',
|
||||
effect: 'heal',
|
||||
range: 1,
|
||||
power: 22,
|
||||
description: '인접 아군의 큰 피해를 되돌리는 고가 회복품.'
|
||||
},
|
||||
wine: {
|
||||
id: 'wine',
|
||||
command: 'item',
|
||||
name: '술',
|
||||
target: 'self',
|
||||
effect: 'focus',
|
||||
range: 0,
|
||||
power: 0,
|
||||
attackBonus: 3,
|
||||
hitBonus: 4,
|
||||
criticalBonus: 12,
|
||||
duration: 2,
|
||||
description: '사용자 자신을 짧게 몰아붙이는 공격·치명 강화 도구.'
|
||||
}
|
||||
};
|
||||
|
||||
const unitStrategyIds: Record<string, string[]> = {
|
||||
'liu-bei': ['aid', 'encourage'],
|
||||
'guan-yu': ['fireTactic'],
|
||||
'zhang-fei': ['roar'],
|
||||
'jian-yong': ['aid', 'encourage', 'fireTactic'],
|
||||
'mi-zhu': ['aid', 'encourage'],
|
||||
'sun-qian': ['aid', 'encourage', 'fireTactic'],
|
||||
'zhao-yun': ['encourage'],
|
||||
'zhuge-liang': ['aid', 'encourage', 'fireTactic'],
|
||||
'ma-liang': ['aid', 'encourage', 'fireTactic'],
|
||||
'yi-ji': ['aid', 'encourage'],
|
||||
'gong-zhi': ['encourage'],
|
||||
'huang-zhong': ['encourage'],
|
||||
'wei-yan': ['roar'],
|
||||
'pang-tong': ['aid', 'encourage', 'fireTactic'],
|
||||
'fa-zheng': ['aid', 'encourage', 'fireTactic'],
|
||||
'wu-yi': ['encourage'],
|
||||
'yan-yan': ['encourage'],
|
||||
'li-yan': ['aid', 'encourage'],
|
||||
'huang-quan': ['aid', 'encourage'],
|
||||
'ma-chao': ['roar', 'encourage'],
|
||||
'ma-dai': ['encourage'],
|
||||
'wang-ping': ['aid', 'encourage'],
|
||||
'jiang-wei': ['aid', 'encourage', 'fireTactic']
|
||||
};
|
||||
|
||||
const initialItemStocks: Record<string, Record<string, number>> = {
|
||||
'liu-bei': { bean: 2, salve: 1 },
|
||||
'guan-yu': { bean: 1 },
|
||||
|
||||
@@ -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