Link sortie roles to battle deployment
This commit is contained in:
187
src/game/data/sortieDeployment.ts
Normal file
187
src/game/data/sortieDeployment.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user