Link sortie roles to battle deployment

This commit is contained in:
2026-07-03 00:31:20 +09:00
parent 40769348ad
commit a3858db201
5 changed files with 356 additions and 17 deletions

View File

@@ -0,0 +1,187 @@
import type { UnitData } from './scenario';
export type SortieFormationRole = 'front' | 'flank' | 'support' | 'reserve';
export type SortieFormationAssignments = Partial<Record<string, SortieFormationRole>>;
export const sortieFormationRoles: SortieFormationRole[] = ['front', 'flank', 'support', 'reserve'];
export type SortieDeploymentPlanEntry = {
unitId: string;
role: SortieFormationRole;
x: number;
y: number;
};
type ScoredTile = {
x: number;
y: number;
forward: number;
lateral: number;
enemyDistance: number;
};
export function isSortieFormationRole(value: unknown): value is SortieFormationRole {
return sortieFormationRoles.includes(value as SortieFormationRole);
}
export function normalizeSortieFormationAssignments(
assignments?: Record<string, unknown>,
allowedUnitIds?: Set<string>
): SortieFormationAssignments {
return Object.fromEntries(
Object.entries(assignments ?? {}).filter(([unitId, role]) => {
return (!allowedUnitIds || allowedUnitIds.has(unitId)) && isSortieFormationRole(role);
})
) as SortieFormationAssignments;
}
export function createSortieDeploymentPlan(
deployedUnits: UnitData[],
sourceUnits: UnitData[],
selectedUnitIds: string[],
assignments: SortieFormationAssignments,
fallbackRoleForUnit: (unit: UnitData) => SortieFormationRole
) {
const selectedOrder = new Map(selectedUnitIds.map((unitId, index) => [unitId, index]));
const deployedAllies = deployedUnits
.filter((unit) => unit.faction === 'ally')
.sort((a, b) => (selectedOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (selectedOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
const sourceAllyTiles = uniqueTiles(sourceUnits.filter((unit) => unit.faction === 'ally').map((unit) => ({ x: unit.x, y: unit.y })));
if (deployedAllies.length === 0 || sourceAllyTiles.length === 0) {
return new Map<string, SortieDeploymentPlanEntry>();
}
const enemyTiles = sourceUnits.filter((unit) => unit.faction === 'enemy').map((unit) => ({ x: unit.x, y: unit.y }));
const allyCenter = averagePoint(sourceAllyTiles);
const enemyCenter = enemyTiles.length ? averagePoint(enemyTiles) : { x: allyCenter.x + 1, y: allyCenter.y };
const direction = normalizePoint({ x: enemyCenter.x - allyCenter.x, y: enemyCenter.y - allyCenter.y });
const scoredTiles = sourceAllyTiles.map((tile) => scoreTile(tile, allyCenter, enemyCenter, direction));
const plan = new Map<string, SortieDeploymentPlanEntry>();
const usedTiles = new Set<string>();
sortieFormationRoles.forEach((role) => {
const roleUnits = deployedAllies.filter((unit) => roleForUnit(unit, assignments, fallbackRoleForUnit) === role);
roleUnits.forEach((unit) => {
const tile = bestTileForRole(role, scoredTiles, usedTiles);
if (!tile) {
return;
}
usedTiles.add(tileKey(tile));
plan.set(unit.id, {
unitId: unit.id,
role,
x: tile.x,
y: tile.y
});
});
});
deployedAllies.forEach((unit) => {
if (plan.has(unit.id)) {
return;
}
const tile = bestTileForRole('reserve', scoredTiles, usedTiles);
if (!tile) {
return;
}
usedTiles.add(tileKey(tile));
plan.set(unit.id, {
unitId: unit.id,
role: roleForUnit(unit, assignments, fallbackRoleForUnit),
x: tile.x,
y: tile.y
});
});
return plan;
}
export function applySortieDeploymentPlan(units: UnitData[], plan: Map<string, SortieDeploymentPlanEntry>) {
return units.map((unit) => {
const entry = plan.get(unit.id);
if (!entry || unit.faction !== 'ally') {
return unit;
}
return {
...unit,
x: entry.x,
y: entry.y
};
});
}
function roleForUnit(
unit: UnitData,
assignments: SortieFormationAssignments,
fallbackRoleForUnit: (unit: UnitData) => SortieFormationRole
) {
return assignments[unit.id] ?? fallbackRoleForUnit(unit);
}
function bestTileForRole(role: SortieFormationRole, tiles: ScoredTile[], usedTiles: Set<string>) {
return [...tiles]
.filter((tile) => !usedTiles.has(tileKey(tile)))
.sort((a, b) => compareTilesForRole(role, a, b))[0];
}
function compareTilesForRole(role: SortieFormationRole, a: ScoredTile, b: ScoredTile) {
if (role === 'front') {
return b.forward - a.forward || a.enemyDistance - b.enemyDistance || a.lateral - b.lateral;
}
if (role === 'flank') {
return b.lateral - a.lateral || b.forward - a.forward || a.enemyDistance - b.enemyDistance;
}
if (role === 'support') {
return a.forward - b.forward || b.enemyDistance - a.enemyDistance || a.lateral - b.lateral;
}
return a.lateral - b.lateral || a.forward - b.forward || b.enemyDistance - a.enemyDistance;
}
function scoreTile(
tile: { x: number; y: number },
allyCenter: { x: number; y: number },
enemyCenter: { x: number; y: number },
direction: { x: number; y: number }
): ScoredTile {
const relative = { x: tile.x - allyCenter.x, y: tile.y - allyCenter.y };
return {
x: tile.x,
y: tile.y,
forward: relative.x * direction.x + relative.y * direction.y,
lateral: Math.abs(relative.x * -direction.y + relative.y * direction.x),
enemyDistance: Math.hypot(tile.x - enemyCenter.x, tile.y - enemyCenter.y)
};
}
function uniqueTiles(tiles: { x: number; y: number }[]) {
const seen = new Set<string>();
return tiles.filter((tile) => {
const key = tileKey(tile);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function averagePoint(points: { x: number; y: number }[]) {
const total = points.reduce((sum, point) => ({ x: sum.x + point.x, y: sum.y + point.y }), { x: 0, y: 0 });
return {
x: total.x / points.length,
y: total.y / points.length
};
}
function normalizePoint(point: { x: number; y: number }) {
const length = Math.hypot(point.x, point.y) || 1;
return {
x: point.x / length,
y: point.y / length
};
}
function tileKey(tile: { x: number; y: number }) {
return `${tile.x},${tile.y}`;
}

View File

@@ -26,6 +26,13 @@ import {
type EquipmentSlot,
type ItemDefinition
} from '../data/battleItems';
import {
applySortieDeploymentPlan,
createSortieDeploymentPlan,
normalizeSortieFormationAssignments,
type SortieFormationAssignments,
type SortieFormationRole
} from '../data/sortieDeployment';
import {
campaignSaveSlotCount,
getCampaignState,
@@ -1133,6 +1140,7 @@ type TurnEndPromptOptions = {
type BattleSceneData = {
battleId?: string;
selectedSortieUnitIds?: string[];
sortieFormationAssignments?: SortieFormationAssignments;
};
type CommandButton = {
@@ -2973,6 +2981,7 @@ export class BattleScene extends Phaser.Scene {
private suppressNextLeftClick = false;
private approachPathCostCache = new Map<string, Map<string, number>>();
private launchSortieUnitIds: string[] = [];
private launchSortieFormationAssignments: SortieFormationAssignments = {};
constructor() {
super('BattleScene');
@@ -2981,6 +2990,7 @@ export class BattleScene extends Phaser.Scene {
init(data?: BattleSceneData) {
configureBattleScenario(getBattleScenario(data?.battleId));
this.launchSortieUnitIds = this.normalizeLaunchSortieUnitIds(data?.selectedSortieUnitIds);
this.launchSortieFormationAssignments = normalizeSortieFormationAssignments(data?.sortieFormationAssignments);
}
create() {
@@ -3164,7 +3174,9 @@ export class BattleScene extends Phaser.Scene {
}
private resetBattleData(campaign?: CampaignState) {
const selected = new Set(this.effectiveSortieUnitIds(campaign));
const effectiveSortieUnitIds = this.effectiveSortieUnitIds(campaign);
const selected = new Set(effectiveSortieUnitIds);
const assignments = this.effectiveSortieFormationAssignments(campaign);
const forcedAllyIds = new Set(
battleScenario.defeatConditions
.filter((condition) => condition.kind === 'unit-defeated')
@@ -3174,7 +3186,14 @@ export class BattleScene extends Phaser.Scene {
const nextUnits = initialBattleUnits
.filter((unit) => unit.faction === 'enemy' || !hasSelection || selected.has(unit.id) || forcedAllyIds.has(unit.id))
.map((unit) => applyScenarioAllyReadiness(this.applyCampaignUnitProgress(unit, campaign)));
battleUnits.splice(0, battleUnits.length, ...nextUnits);
const deploymentPlan = createSortieDeploymentPlan(
nextUnits.filter((unit) => unit.faction === 'ally'),
initialBattleUnits,
effectiveSortieUnitIds,
assignments,
(unit) => this.defaultSortieFormationRole(unit)
);
battleUnits.splice(0, battleUnits.length, ...applySortieDeploymentPlan(nextUnits, deploymentPlan));
battleBonds.splice(0, battleBonds.length, ...initialBattleBonds.map((bond) => this.applyCampaignBondProgress(bond, campaign)));
initialBattleUnits = battleUnits.map(cloneUnitData);
initialBattleBonds = battleBonds.map(cloneBattleBond);
@@ -3188,6 +3207,25 @@ export class BattleScene extends Phaser.Scene {
return this.launchSortieUnitIds.length > 0 ? this.launchSortieUnitIds : campaign?.selectedSortieUnitIds ?? [];
}
private effectiveSortieFormationAssignments(campaign?: CampaignState) {
return Object.keys(this.launchSortieFormationAssignments).length > 0
? this.launchSortieFormationAssignments
: campaign?.sortieFormationAssignments ?? {};
}
private defaultSortieFormationRole(unit: UnitData): SortieFormationRole {
if (unit.classKey === 'cavalry') {
return 'flank';
}
if (unit.classKey === 'archer' || unit.classKey === 'strategist' || unit.classKey === 'quartermaster') {
return 'support';
}
if (unit.classKey === 'lord' || unit.classKey === 'infantry' || unit.classKey === 'spearman') {
return 'front';
}
return 'reserve';
}
private applyCampaignUnitProgress(unit: UnitData, campaign?: CampaignState) {
const cloned = cloneUnitData(unit);
const progress = campaign?.roster.find((candidate) => candidate.id === unit.id);
@@ -12489,7 +12527,9 @@ export class BattleScene extends Phaser.Scene {
}
getDebugState() {
const effectiveSortieUnitIds = this.effectiveSortieUnitIds(getCampaignState());
const campaign = getCampaignState();
const effectiveSortieUnitIds = this.effectiveSortieUnitIds(campaign);
const sortieFormationAssignments = this.effectiveSortieFormationAssignments(campaign);
const deployedAllyIds = battleUnits.filter((unit) => unit.faction === 'ally').map((unit) => unit.id);
return {
@@ -12499,7 +12539,17 @@ export class BattleScene extends Phaser.Scene {
victoryConditionLabel: battleScenario.victoryConditionLabel,
defeatConditionLabel: battleScenario.defeatConditionLabel,
selectedSortieUnitIds: effectiveSortieUnitIds,
sortieFormationAssignments: { ...sortieFormationAssignments },
deployedAllyIds,
deployedAllyPositions: battleUnits
.filter((unit) => unit.faction === 'ally')
.map((unit) => ({
id: unit.id,
name: unit.name,
x: unit.x,
y: unit.y,
formationRole: sortieFormationAssignments[unit.id] ?? this.defaultSortieFormationRole(unit)
})),
turnNumber: this.turnNumber,
activeFaction: this.activeFaction,
phase: this.phase,

View File

@@ -7,6 +7,12 @@ import { getUnitClass, terrainRules, type TerrainType } from '../data/battleRule
import { defaultBattleScenario, getBattleScenario, type BattleScenarioDefinition, type BattleScenarioId } from '../data/battles';
import { getSortieFlow } from '../data/campaignFlow';
import { portraitAssetEntriesForKey, portraitTextureKey, type PortraitAssetEntry } from '../data/portraitAssets';
import {
createSortieDeploymentPlan,
normalizeSortieFormationAssignments,
type SortieFormationAssignments,
type SortieFormationRole
} from '../data/sortieDeployment';
import {
caoBreakRecruitBonds,
caoBreakRecruitUnits,
@@ -214,8 +220,6 @@ type SortieRuleDefinition = {
note: string;
};
type SortieFormationRole = 'front' | 'flank' | 'support' | 'reserve';
type SortieFormationSlot = {
role: SortieFormationRole;
label: string;
@@ -10000,6 +10004,7 @@ export class CampScene extends Phaser.Scene {
private tabButtons: CampTabButtonView[] = [];
private visitedTabs = new Set<CampTab>();
private selectedSortieUnitIds: string[] = [];
private sortieFormationAssignments: SortieFormationAssignments = {};
private sortieFocusedUnitId = 'liu-bei';
private sortieRosterScroll = 0;
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
@@ -10019,6 +10024,7 @@ export class CampScene extends Phaser.Scene {
this.ensureCurrentCampRecruitment();
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
this.selectedVisitId = this.availableCampVisits()[0]?.id ?? campVisits[0].id;
soundDirector.playMusic('militia-theme');
@@ -11200,6 +11206,7 @@ export class CampScene extends Phaser.Scene {
this.campaign = getCampaignState();
this.report = this.campaign.firstBattleReport ?? this.report;
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
this.ensureSortieFocus();
const depth = 40;
@@ -11511,16 +11518,29 @@ export class CampScene extends Phaser.Scene {
const gap = 8;
const slotWidth = Math.floor((width - gap) / 2);
const slotHeight = Math.floor((height - 6) / 2);
const focusedUnit = this.sortieFocusedUnit();
const focusedUnitSelected = Boolean(focusedUnit && this.isSortieSelected(focusedUnit.id));
plan.formationSlots.forEach((slot, index) => {
const column = index % 2;
const row = Math.floor(index / 2);
const slotX = x + column * (slotWidth + gap);
const slotY = y + row * (slotHeight + 6);
const filled = slot.unitNames.length > 0;
const focusedRole = focusedUnitSelected && focusedUnit ? this.sortieFormationRole(focusedUnit) === slot.role : false;
const bg = this.trackSortie(this.add.rectangle(slotX, slotY, slotWidth, slotHeight, filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.78));
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, filled ? palette.green : 0x53606c, filled ? 0.58 : 0.34);
bg.setStrokeStyle(focusedRole ? 2 : 1, focusedRole ? palette.gold : filled ? palette.green : 0x53606c, focusedRole ? 0.92 : filled ? 0.58 : 0.34);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => this.assignFocusedSortieRole(slot.role));
bg.on('pointerover', () => {
if (focusedUnitSelected) {
bg.setFillStyle(filled ? 0x22342b : 0x182330, 0.96);
}
});
bg.on('pointerout', () => {
bg.setFillStyle(filled ? 0x172a22 : 0x111922, filled ? 0.94 : 0.78);
});
this.renderSortieBattleUiIcon(this.formationRoleIconKey(slot.role), slotX + 13, slotY + slotHeight / 2, 18, depth + 1, filled ? 0.95 : 0.5);
this.trackSortie(this.add.text(slotX + 28, slotY + 3, `${slot.label} ${slot.unitNames.length}`, this.textStyle(10, filled ? '#f2e3bf' : '#9fb0bf', true))).setDepth(depth + 1);
this.trackSortie(this.add.text(slotX + 76, slotY + 3, this.compactText(slot.unitNames.join(', ') || slot.description, 12), this.textStyle(10, filled ? '#c8d2dd' : '#68727e'))).setDepth(depth + 1);
@@ -11859,23 +11879,51 @@ export class CampScene extends Phaser.Scene {
});
});
const selected = new Set(this.selectedSortieUnitIds);
const deploymentPlan = this.sortieDeploymentPlanForScenario(scenario);
scenario.units.forEach((unit) => {
if (unit.faction !== 'enemy' && !selected.has(unit.id)) {
if (unit.faction !== 'enemy') {
return;
}
const dotX = originX + (unit.x + 0.5) * cell;
const dotY = originY + (unit.y + 0.5) * cell;
const color = unit.faction === 'enemy' ? 0xd35f4a : unit.id === 'liu-bei' ? palette.gold : palette.green;
const alpha = unit.faction === 'enemy' ? 0.72 : 0.96;
const dot = this.trackSortie(this.add.circle(dotX, dotY, unit.faction === 'enemy' ? 2.2 : 3.7, color, alpha));
const dot = this.trackSortie(this.add.circle(dotX, dotY, 2.2, 0xd35f4a, 0.72));
dot.setDepth(depth + 2);
if (unit.faction !== 'enemy') {
dot.setStrokeStyle(1, 0x05070a, 0.8);
}
});
this.trackSortie(this.add.text(x + 10, y + height - 16, '녹색: 출전 아군 · 붉은색: 적 배치', this.textStyle(10, '#9fb0bf'))).setDepth(depth + 2);
deploymentPlan.forEach((entry) => {
const dotX = originX + (entry.x + 0.5) * cell;
const dotY = originY + (entry.y + 0.5) * cell;
const dot = this.trackSortie(this.add.circle(dotX, dotY, entry.unitId === 'liu-bei' ? 4.2 : 3.7, this.sortieRoleDotColor(entry.role), 0.96));
dot.setDepth(depth + 3);
dot.setStrokeStyle(1, 0x05070a, 0.82);
});
this.trackSortie(this.add.text(x + 10, y + height - 16, '전열/돌파/후원 배치점 · 붉은색: 적', this.textStyle(10, '#9fb0bf'))).setDepth(depth + 2);
}
private sortieDeploymentPlanForScenario(scenario: BattleScenarioDefinition) {
const selected = new Set(this.selectedSortieUnitIds);
const deployedUnits = scenario.units.filter((unit) => unit.faction === 'ally' && selected.has(unit.id));
return createSortieDeploymentPlan(
deployedUnits,
scenario.units,
this.selectedSortieUnitIds,
this.sortieFormationAssignments,
(unit) => this.defaultSortieFormationRole(unit)
);
}
private sortieRoleDotColor(role: SortieFormationRole) {
if (role === 'front') {
return palette.green;
}
if (role === 'flank') {
return palette.gold;
}
if (role === 'support') {
return palette.blue;
}
return 0x9fb0bf;
}
private sortiePlanSummary(): SortiePlanSummary {
@@ -11970,6 +12018,10 @@ export class CampScene extends Phaser.Scene {
}
private sortieFormationRole(unit: UnitData): SortieFormationRole {
return this.sortieFormationAssignments[unit.id] ?? this.defaultSortieFormationRole(unit);
}
private defaultSortieFormationRole(unit: UnitData): SortieFormationRole {
if (unit.classKey === 'cavalry') {
return 'flank';
}
@@ -11982,6 +12034,27 @@ export class CampScene extends Phaser.Scene {
return 'reserve';
}
private assignFocusedSortieRole(role: SortieFormationRole) {
const unit = this.sortieFocusedUnit();
if (!unit) {
return;
}
this.sortieFocusedUnitId = unit.id;
if (!this.isSortieSelected(unit.id)) {
this.showCampNotice(`${unit.name}을 먼저 출전 편성에 포함해야 역할을 바꿀 수 있습니다.`);
return;
}
this.sortieFormationAssignments = {
...this.sortieFormationAssignments,
[unit.id]: role
};
this.persistSortieSelection();
soundDirector.playSelect();
this.showSortiePrep();
}
private sortieTerrainScore(unit: UnitData, scenario: BattleScenarioDefinition | undefined) {
if (!scenario) {
return 100;
@@ -12230,10 +12303,11 @@ export class CampScene extends Phaser.Scene {
markCampaignStep(flow.campaignStep);
const selectedSortieUnitIds = [...this.selectedSortieUnitIds];
const sortieFormationAssignments = { ...this.sortieFormationAssignments };
void startLazyScene(this, 'StoryScene', {
pages: flow.pages,
nextScene: 'BattleScene',
nextSceneData: { battleId: flow.nextBattleId, selectedSortieUnitIds }
nextSceneData: { battleId: flow.nextBattleId, selectedSortieUnitIds, sortieFormationAssignments }
});
}
@@ -13338,6 +13412,10 @@ export class CampScene extends Phaser.Scene {
return [...new Set([...required, ...optional])].slice(0, maxUnits);
}
private normalizedSortieFormationAssignments(assignments?: Record<string, unknown>) {
return normalizeSortieFormationAssignments(assignments, new Set(this.sortieAllies().map((unit) => unit.id)));
}
private isSortieSelected(unitId: string) {
return this.selectedSortieUnitIds.includes(unitId);
}
@@ -13356,6 +13434,9 @@ export class CampScene extends Phaser.Scene {
const selected = new Set(this.selectedSortieUnitIds);
if (selected.has(unitId)) {
selected.delete(unitId);
const nextAssignments = { ...this.sortieFormationAssignments };
delete nextAssignments[unitId];
this.sortieFormationAssignments = nextAssignments;
} else if (selected.size >= this.sortieMaxUnits()) {
this.showCampNotice(`이번 전투는 최대 ${this.sortieMaxUnits()}명까지 출전할 수 있습니다.`);
return;
@@ -13382,7 +13463,9 @@ export class CampScene extends Phaser.Scene {
private persistSortieSelection() {
const campaign = this.campaign ?? getCampaignState();
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.sortieFormationAssignments);
campaign.selectedSortieUnitIds = [...this.selectedSortieUnitIds];
campaign.sortieFormationAssignments = { ...this.sortieFormationAssignments };
this.campaign = saveCampaignState(campaign);
}
@@ -13463,6 +13546,14 @@ export class CampScene extends Phaser.Scene {
this.contentObjects = [];
}
private sortieDeploymentPreviewDebug() {
const scenario = this.nextSortieScenario();
if (!scenario) {
return [];
}
return [...this.sortieDeploymentPlanForScenario(scenario).values()];
}
getDebugState() {
return {
scene: this.scene.key,
@@ -13472,6 +13563,8 @@ export class CampScene extends Phaser.Scene {
selectedDialogueId: this.selectedDialogueId,
selectedVisitId: this.selectedVisitId,
selectedSortieUnitIds: [...this.selectedSortieUnitIds],
sortieFormationAssignments: { ...this.sortieFormationAssignments },
sortieDeploymentPreview: this.sortieDeploymentPreviewDebug(),
sortieFocusedUnitId: this.sortieFocusedUnitId,
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
@@ -13512,6 +13605,7 @@ export class CampScene extends Phaser.Scene {
gold: this.campaign.gold,
inventory: this.campaign.inventory,
selectedSortieUnitIds: this.campaign.selectedSortieUnitIds,
sortieFormationAssignments: this.campaign.sortieFormationAssignments,
reserveTrainingFocus: this.campaign.reserveTrainingFocus,
roster: this.campaign.roster.map((unit) => ({
id: unit.id,

View File

@@ -319,7 +319,11 @@ export class TitleScene extends Phaser.Scene {
const battleId = battleIdForCampaignStep(campaign.step);
if (battleId) {
await this.navigateTo('BattleScene', { battleId, selectedSortieUnitIds: campaign.selectedSortieUnitIds });
await this.navigateTo('BattleScene', {
battleId,
selectedSortieUnitIds: campaign.selectedSortieUnitIds,
sortieFormationAssignments: campaign.sortieFormationAssignments
});
return;
}

View File

@@ -1,5 +1,6 @@
import { equipmentExpToNext, equipmentSlots } from '../data/battleItems';
import type { BattleBond, UnitData } from '../data/scenario';
import { normalizeSortieFormationAssignments, type SortieFormationAssignments } from '../data/sortieDeployment';
export type BattleObjectiveSnapshot = {
id: string;
@@ -279,6 +280,7 @@ export type CampaignState = {
bonds: CampBondSnapshot[];
inventory: Record<string, number>;
selectedSortieUnitIds: string[];
sortieFormationAssignments: SortieFormationAssignments;
reserveTrainingFocus: CampaignReserveTrainingFocusId;
completedCampDialogues: string[];
completedCampVisits: string[];
@@ -381,6 +383,7 @@ export function createInitialCampaignState(): CampaignState {
bonds: [],
inventory: {},
selectedSortieUnitIds: [],
sortieFormationAssignments: {},
reserveTrainingFocus: defaultCampaignReserveTrainingFocusId,
completedCampDialogues: [],
completedCampVisits: [],
@@ -666,6 +669,7 @@ function normalizeCampaignState(state: CampaignState): CampaignState {
normalized.completedCampVisits = [...new Set(normalized.completedCampVisits ?? [])];
normalized.inventory = { ...(normalized.inventory ?? {}) };
normalized.selectedSortieUnitIds = [...new Set(normalized.selectedSortieUnitIds ?? [])];
normalized.sortieFormationAssignments = normalizeSortieFormationAssignments(normalized.sortieFormationAssignments);
normalized.reserveTrainingFocus = normalizeReserveTrainingFocusId(normalized.reserveTrainingFocus);
normalized.battleHistory = normalized.battleHistory ?? {};
if (normalized.firstBattleReport) {