import type { DomainEvent } from "./events.js"; import type { CombatantId, DomainError, Encounter } from "./types.js"; export interface SetInitiativeSuccess { readonly encounter: Encounter; readonly events: DomainEvent[]; } /** * Pure function that sets, changes, or clears a combatant's initiative value. * * After updating the value, combatants are stable-sorted: * 1. Combatants with initiative — descending by value * 2. Combatants without initiative — preserve relative order * * The active combatant's turn is preserved through the reorder * by tracking identity (CombatantId) rather than position. * * roundNumber is never changed. */ export function setInitiative( encounter: Encounter, combatantId: CombatantId, value: number | undefined, ): SetInitiativeSuccess | DomainError { const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId); if (targetIdx === -1) { return { kind: "domain-error", code: "combatant-not-found", message: `No combatant found with ID "${combatantId}"`, }; } if (value !== undefined && !Number.isInteger(value)) { return { kind: "domain-error", code: "invalid-initiative", message: `Initiative must be an integer, got ${value}`, }; } const target = encounter.combatants[targetIdx]; const previousValue = target.initiative; // Record active combatant's id before reorder const activeCombatantId = encounter.combatants.length > 0 ? encounter.combatants[encounter.activeIndex].id : undefined; // Create new combatants array with updated initiative const updated = encounter.combatants.map((c) => c.id === combatantId ? { ...c, initiative: value } : c, ); // Stable sort: initiative descending, undefined last const indexed = updated.map((c, i) => ({ c, i })); indexed.sort((a, b) => { const aHas = a.c.initiative !== undefined; const bHas = b.c.initiative !== undefined; if (aHas && bHas) { const aInit = a.c.initiative as number; const bInit = b.c.initiative as number; const diff = bInit - aInit; return diff === 0 ? a.i - b.i : diff; } if (aHas && !bHas) return -1; if (!aHas && bHas) return 1; // Both undefined — preserve relative order return a.i - b.i; }); const sorted = indexed.map(({ c }) => c); // Find active combatant's new index let newActiveIndex = encounter.activeIndex; if (activeCombatantId !== undefined) { const idx = sorted.findIndex((c) => c.id === activeCombatantId); if (idx !== -1) { newActiveIndex = idx; } } return { encounter: { combatants: sorted, activeIndex: newActiveIndex, roundNumber: encounter.roundNumber, }, events: [ { type: "InitiativeSet", combatantId, previousValue, newValue: value, }, ], }; }