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>
122 lines
3.1 KiB
TypeScript
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 }],
|
|
};
|
|
}
|