51 lines
1.3 KiB
TypeScript
51 lines
1.3 KiB
TypeScript
import type { DomainEvent } from "./events.js";
|
|
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
|
|
|
export interface AddCombatantSuccess {
|
|
readonly encounter: Encounter;
|
|
readonly events: DomainEvent[];
|
|
}
|
|
|
|
/**
|
|
* Pure function that adds a combatant to the end of an encounter's list.
|
|
*
|
|
* FR-001: Accepts an Encounter, CombatantId, and name; returns next state + events.
|
|
* FR-002: Appends new combatant to end of combatants list.
|
|
* FR-004: Rejects empty/whitespace-only names with DomainError.
|
|
* FR-005: Does not alter activeIndex or roundNumber.
|
|
* FR-006: Events returned as values, not dispatched via side effects.
|
|
*/
|
|
export function addCombatant(
|
|
encounter: Encounter,
|
|
id: CombatantId,
|
|
name: string,
|
|
): AddCombatantSuccess | DomainError {
|
|
const trimmed = name.trim();
|
|
|
|
if (trimmed === "") {
|
|
return {
|
|
kind: "domain-error",
|
|
code: "invalid-name",
|
|
message: "Combatant name must not be empty",
|
|
};
|
|
}
|
|
|
|
const position = encounter.combatants.length;
|
|
|
|
return {
|
|
encounter: {
|
|
combatants: [...encounter.combatants, { id, name: trimmed }],
|
|
activeIndex: encounter.activeIndex,
|
|
roundNumber: encounter.roundNumber,
|
|
},
|
|
events: [
|
|
{
|
|
type: "CombatantAdded",
|
|
combatantId: id,
|
|
name: trimmed,
|
|
position,
|
|
},
|
|
],
|
|
};
|
|
}
|