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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user