Files
initiative/packages/domain/src/persistent-damage.ts
T
LukasandClaude Fable 5 78079bf1b2 Break import cycle in domain persistent-damage modules
Extract persistent damage types and definitions into a leaf module so
events.ts and types.ts no longer import back into persistent-damage.ts.
Enable oxlint import/no-cycle (whole workspace via root tsconfig) to
keep cycles out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:35:14 +02:00

122 lines
3.1 KiB
TypeScript

import type { DomainEvent } from "./events.js";
import {
PERSISTENT_DAMAGE_DEFINITIONS,
type PersistentDamageEntry,
type PersistentDamageType,
VALID_PERSISTENT_DAMAGE_TYPES,
} from "./persistent-damage-types.js";
import {
type CombatantId,
type DomainError,
type Encounter,
findCombatant,
isDomainError,
} from "./types.js";
export {
PERSISTENT_DAMAGE_DEFINITIONS,
PERSISTENT_DAMAGE_TYPES,
type PersistentDamageDefinition,
type PersistentDamageEntry,
type PersistentDamageType,
VALID_PERSISTENT_DAMAGE_TYPES,
} from "./persistent-damage-types.js";
export interface PersistentDamageSuccess {
readonly encounter: Encounter;
readonly events: DomainEvent[];
}
function applyPersistentDamage(
encounter: Encounter,
combatantId: CombatantId,
newEntries: readonly PersistentDamageEntry[] | undefined,
): Encounter {
return {
combatants: encounter.combatants.map((c) =>
c.id === combatantId ? { ...c, persistentDamage: newEntries } : c,
),
activeIndex: encounter.activeIndex,
roundNumber: encounter.roundNumber,
};
}
export function addPersistentDamage(
encounter: Encounter,
combatantId: CombatantId,
damageType: PersistentDamageType,
formula: string,
): PersistentDamageSuccess | DomainError {
if (!VALID_PERSISTENT_DAMAGE_TYPES.has(damageType)) {
return {
kind: "domain-error",
code: "unknown-damage-type",
message: `Unknown persistent damage type "${damageType}"`,
};
}
if (formula.trim().length === 0) {
return {
kind: "domain-error",
code: "empty-formula",
message: "Persistent damage formula must not be empty",
};
}
const found = findCombatant(encounter, combatantId);
if (isDomainError(found)) return found;
const { combatant: target } = found;
const current = target.persistentDamage ?? [];
// Replace existing entry of same type, or append
const filtered = current.filter((e) => e.type !== damageType);
const newEntries = [
...filtered,
{ type: damageType, formula: formula.trim() },
];
// Sort by definition order
const order = PERSISTENT_DAMAGE_DEFINITIONS.map((d) => d.type);
newEntries.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type));
return {
encounter: applyPersistentDamage(encounter, combatantId, newEntries),
events: [
{
type: "PersistentDamageAdded",
combatantId,
damageType,
formula: formula.trim(),
},
],
};
}
export function removePersistentDamage(
encounter: Encounter,
combatantId: CombatantId,
damageType: PersistentDamageType,
): PersistentDamageSuccess | DomainError {
const found = findCombatant(encounter, combatantId);
if (isDomainError(found)) return found;
const { combatant: target } = found;
const current = target.persistentDamage ?? [];
if (!current.some((e) => e.type === damageType)) {
return {
kind: "domain-error",
code: "persistent-damage-not-active",
message: `Persistent ${damageType} damage is not active`,
};
}
const filtered = current.filter((e) => e.type !== damageType);
return {
encounter: applyPersistentDamage(
encounter,
combatantId,
filtered.length > 0 ? filtered : undefined,
),
events: [{ type: "PersistentDamageRemoved", combatantId, damageType }],
};
}