Files
initiative/packages/domain/src/set-initiative.ts
Lukas 36768d3aa1 Upgrade Biome to 2.4.7 and enable 54 additional lint rules
Add rules covering bug prevention (noLeakedRender, noFloatingPromises,
noImportCycles, noReactForwardRef), security (noScriptUrl, noAlert),
performance (noAwaitInLoops, useTopLevelRegex), and code style
(noNestedTernary, useGlobalThis, useNullishCoalescing, useSortedClasses,
plus ~40 more). Fix all violations: extract top-level regex constants,
guard React && renders with boolean coercion, refactor nested ternaries,
replace window with globalThis, sort Tailwind classes, and introduce
expectDomainError test helper to eliminate conditional expects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:25:09 +01:00

103 lines
2.7 KiB
TypeScript

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,
},
],
};
}