import { describe, expect, it } from "vitest"; import { addPersistentDamage, type PersistentDamageType, removePersistentDamage, } from "../persistent-damage.js"; import type { Encounter } from "../types.js"; import { combatantId } from "../types.js"; const goblinId = combatantId("goblin-1"); function buildEncounter(overrides: Partial = {}): Encounter { return { combatants: [ { id: goblinId, name: "Goblin", ...overrides.combatants?.[0], }, ], activeIndex: overrides.activeIndex ?? 0, roundNumber: overrides.roundNumber ?? 1, }; } describe("addPersistentDamage", () => { it("adds persistent fire damage to combatant", () => { const encounter = buildEncounter(); const result = addPersistentDamage(encounter, goblinId, "fire", "2d6"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; const target = result.encounter.combatants[0]; expect(target.persistentDamage).toEqual([{ type: "fire", formula: "2d6" }]); expect(result.events).toEqual([ { type: "PersistentDamageAdded", combatantId: goblinId, damageType: "fire", formula: "2d6", }, ]); }); it("replaces existing entry of same type with new formula", () => { const encounter = buildEncounter({ combatants: [ { id: goblinId, name: "Goblin", persistentDamage: [{ type: "fire", formula: "2d6" }], }, ], }); const result = addPersistentDamage(encounter, goblinId, "fire", "3d6"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; expect(result.encounter.combatants[0].persistentDamage).toEqual([ { type: "fire", formula: "3d6" }, ]); }); it("allows multiple different damage types", () => { const encounter = buildEncounter({ combatants: [ { id: goblinId, name: "Goblin", persistentDamage: [{ type: "fire", formula: "2d6" }], }, ], }); const result = addPersistentDamage(encounter, goblinId, "bleed", "1d4"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; expect(result.encounter.combatants[0].persistentDamage).toEqual([ { type: "fire", formula: "2d6" }, { type: "bleed", formula: "1d4" }, ]); }); it("sorts entries by definition order", () => { const encounter = buildEncounter({ combatants: [ { id: goblinId, name: "Goblin", persistentDamage: [{ type: "cold", formula: "1d6" }], }, ], }); const result = addPersistentDamage(encounter, goblinId, "fire", "2d6"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; const types = result.encounter.combatants[0].persistentDamage?.map( (e) => e.type, ); expect(types).toEqual(["fire", "cold"]); }); it("returns domain error for empty formula", () => { const encounter = buildEncounter(); const result = addPersistentDamage(encounter, goblinId, "fire", " "); expect(result).toHaveProperty("kind", "domain-error"); if (!("kind" in result)) return; expect(result.code).toBe("empty-formula"); }); it("returns domain error for unknown damage type", () => { const encounter = buildEncounter(); const result = addPersistentDamage( encounter, goblinId, "radiant" as PersistentDamageType, "2d6", ); expect(result).toHaveProperty("kind", "domain-error"); if (!("kind" in result)) return; expect(result.code).toBe("unknown-damage-type"); }); it("returns domain error for unknown combatant", () => { const encounter = buildEncounter(); const result = addPersistentDamage( encounter, combatantId("nonexistent"), "fire", "2d6", ); expect(result).toHaveProperty("kind", "domain-error"); if (!("kind" in result)) return; expect(result.code).toBe("combatant-not-found"); }); it("trims formula whitespace", () => { const encounter = buildEncounter(); const result = addPersistentDamage(encounter, goblinId, "fire", " 2d6 "); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; expect(result.encounter.combatants[0].persistentDamage?.[0].formula).toBe( "2d6", ); }); it("does not mutate input encounter", () => { const encounter = buildEncounter(); const originalCombatants = encounter.combatants; addPersistentDamage(encounter, goblinId, "fire", "2d6"); expect(encounter.combatants).toBe(originalCombatants); expect(encounter.combatants[0].persistentDamage).toBeUndefined(); }); }); describe("removePersistentDamage", () => { it("removes existing persistent damage entry", () => { const encounter = buildEncounter({ combatants: [ { id: goblinId, name: "Goblin", persistentDamage: [ { type: "fire", formula: "2d6" }, { type: "bleed", formula: "1d4" }, ], }, ], }); const result = removePersistentDamage(encounter, goblinId, "fire"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; expect(result.encounter.combatants[0].persistentDamage).toEqual([ { type: "bleed", formula: "1d4" }, ]); expect(result.events).toEqual([ { type: "PersistentDamageRemoved", combatantId: goblinId, damageType: "fire", }, ]); }); it("sets persistentDamage to undefined when last entry removed", () => { const encounter = buildEncounter({ combatants: [ { id: goblinId, name: "Goblin", persistentDamage: [{ type: "fire", formula: "2d6" }], }, ], }); const result = removePersistentDamage(encounter, goblinId, "fire"); expect(result).not.toHaveProperty("kind"); if ("kind" in result) return; expect(result.encounter.combatants[0].persistentDamage).toBeUndefined(); }); it("returns domain error when damage type not active", () => { const encounter = buildEncounter(); const result = removePersistentDamage(encounter, goblinId, "fire"); expect(result).toHaveProperty("kind", "domain-error"); if (!("kind" in result)) return; expect(result.code).toBe("persistent-damage-not-active"); }); it("returns domain error for unknown combatant", () => { const encounter = buildEncounter(); const result = removePersistentDamage( encounter, combatantId("nonexistent"), "fire", ); expect(result).toHaveProperty("kind", "domain-error"); if (!("kind" in result)) return; expect(result.code).toBe("combatant-not-found"); }); });