Atomic addCombatant with optional CombatantInit bag
addCombatant now accepts an optional init parameter for pre-filled stats (HP, AC, initiative, creatureId, color, icon, playerCharacterId), making combatant creation a single atomic operation with domain validation. This eliminates the multi-step store.save() bypass in addFromBestiary and addFromPlayerCharacter, and removes the CombatantOpts/applyCombatantOpts helpers. Also extracts shared initiative sort logic into initiative-sort.ts used by both addCombatant and setInitiative. Closes #15 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { DomainEvent } from "./events.js";
|
||||
import { sortByInitiative } from "./initiative-sort.js";
|
||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
||||
|
||||
export interface SetInitiativeSuccess {
|
||||
@@ -44,45 +45,21 @@ export function setInitiative(
|
||||
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;
|
||||
// Record active combatant's id before reorder
|
||||
const activeCombatantId =
|
||||
encounter.combatants.length > 0
|
||||
? encounter.combatants[encounter.activeIndex].id
|
||||
: combatantId;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
const { sorted, activeIndex: newActiveIndex } = sortByInitiative(
|
||||
updated,
|
||||
activeCombatantId,
|
||||
);
|
||||
|
||||
return {
|
||||
encounter: {
|
||||
|
||||
Reference in New Issue
Block a user