import type { ConditionEntry, ConditionId } from "./conditions.js"; import { CONDITION_DEFINITIONS, VALID_CONDITION_IDS } from "./conditions.js"; import type { DomainEvent } from "./events.js"; import { type CombatantId, type DomainError, type Encounter, findCombatant, isDomainError, } from "./types.js"; export interface ToggleConditionSuccess { readonly encounter: Encounter; readonly events: DomainEvent[]; } function sortByDefinitionOrder(entries: ConditionEntry[]): ConditionEntry[] { const order = CONDITION_DEFINITIONS.map((d) => d.id); entries.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id)); return entries; } function validateConditionId(conditionId: ConditionId): DomainError | null { if (!VALID_CONDITION_IDS.has(conditionId)) { return { kind: "domain-error", code: "unknown-condition", message: `Unknown condition "${conditionId}"`, }; } return null; } function applyConditions( encounter: Encounter, combatantId: CombatantId, newConditions: readonly ConditionEntry[] | undefined, ): Encounter { return { combatants: encounter.combatants.map((c) => c.id === combatantId ? { ...c, conditions: newConditions } : c, ), activeIndex: encounter.activeIndex, roundNumber: encounter.roundNumber, }; } export function toggleCondition( encounter: Encounter, combatantId: CombatantId, conditionId: ConditionId, ): ToggleConditionSuccess | DomainError { const err = validateConditionId(conditionId); if (err) return err; const found = findCombatant(encounter, combatantId); if (isDomainError(found)) return found; const { combatant: target } = found; const current = target.conditions ?? []; const isActive = current.some((c) => c.id === conditionId); let newConditions: readonly ConditionEntry[] | undefined; let event: DomainEvent; if (isActive) { const filtered = current.filter((c) => c.id !== conditionId); newConditions = filtered.length > 0 ? filtered : undefined; event = { type: "ConditionRemoved", combatantId, condition: conditionId }; } else { const added = sortByDefinitionOrder([...current, { id: conditionId }]); newConditions = added; event = { type: "ConditionAdded", combatantId, condition: conditionId }; } return { encounter: applyConditions(encounter, combatantId, newConditions), events: [event], }; } export function setConditionValue( encounter: Encounter, combatantId: CombatantId, conditionId: ConditionId, value: number, ): ToggleConditionSuccess | DomainError { const err = validateConditionId(conditionId); if (err) return err; const found = findCombatant(encounter, combatantId); if (isDomainError(found)) return found; const { combatant: target } = found; const current = target.conditions ?? []; const def = CONDITION_DEFINITIONS.find((d) => d.id === conditionId); const clampedValue = def?.maxValue === undefined ? value : Math.min(value, def.maxValue); if (clampedValue <= 0) { const filtered = current.filter((c) => c.id !== conditionId); const newConditions = filtered.length > 0 ? filtered : undefined; return { encounter: applyConditions(encounter, combatantId, newConditions), events: [ { type: "ConditionRemoved", combatantId, condition: conditionId }, ], }; } const existing = current.find((c) => c.id === conditionId); if (existing) { const updated = current.map((c) => c.id === conditionId ? { ...c, value: clampedValue } : c, ); return { encounter: applyConditions(encounter, combatantId, updated), events: [ { type: "ConditionAdded", combatantId, condition: conditionId, value: clampedValue, }, ], }; } const added = sortByDefinitionOrder([ ...current, { id: conditionId, value: clampedValue }, ]); return { encounter: applyConditions(encounter, combatantId, added), events: [ { type: "ConditionAdded", combatantId, condition: conditionId, value: clampedValue, }, ], }; } export function decrementCondition( encounter: Encounter, combatantId: CombatantId, conditionId: ConditionId, ): ToggleConditionSuccess | DomainError { const err = validateConditionId(conditionId); if (err) return err; const found = findCombatant(encounter, combatantId); if (isDomainError(found)) return found; const { combatant: target } = found; const current = target.conditions ?? []; const existing = current.find((c) => c.id === conditionId); if (!existing) { return { kind: "domain-error", code: "condition-not-active", message: `Condition "${conditionId}" is not active`, }; } const newValue = (existing.value ?? 1) - 1; if (newValue <= 0) { const filtered = current.filter((c) => c.id !== conditionId); return { encounter: applyConditions( encounter, combatantId, filtered.length > 0 ? filtered : undefined, ), events: [ { type: "ConditionRemoved", combatantId, condition: conditionId }, ], }; } const updated = current.map((c) => c.id === conditionId ? { ...c, value: newValue } : c, ); return { encounter: applyConditions(encounter, combatantId, updated), events: [ { type: "ConditionAdded", combatantId, condition: conditionId, value: newValue, }, ], }; }