import { describe, expect, it } from "vitest"; import type { ConditionEntry, ConditionId } from "../conditions.js"; import { CONDITION_DEFINITIONS } from "../conditions.js"; import { decrementCondition, setConditionValue, toggleCondition, } from "../toggle-condition.js"; import type { Combatant, Encounter } from "../types.js"; import { combatantId, isDomainError } from "../types.js"; import { expectDomainError } from "./test-helpers.js"; function makeCombatant( name: string, conditions?: readonly ConditionEntry[], ): Combatant { return conditions ? { id: combatantId(name), name, conditions } : { id: combatantId(name), name }; } function enc(combatants: Combatant[]): Encounter { return { combatants, activeIndex: 0, roundNumber: 1 }; } function success(encounter: Encounter, id: string, condition: ConditionId) { const result = toggleCondition(encounter, combatantId(id), condition); if (isDomainError(result)) { throw new Error(`Expected success, got error: ${result.message}`); } return result; } describe("toggleCondition", () => { it("adds a condition when not present", () => { const e = enc([makeCombatant("A")]); const { encounter, events } = success(e, "A", "blinded"); expect(encounter.combatants[0].conditions).toEqual([{ id: "blinded" }]); expect(events).toEqual([ { type: "ConditionAdded", combatantId: combatantId("A"), condition: "blinded", }, ]); }); it("removes a condition when already present", () => { const e = enc([makeCombatant("A", [{ id: "blinded" }])]); const { encounter, events } = success(e, "A", "blinded"); expect(encounter.combatants[0].conditions).toBeUndefined(); expect(events).toEqual([ { type: "ConditionRemoved", combatantId: combatantId("A"), condition: "blinded", }, ]); }); it("maintains definition order when adding conditions", () => { const e = enc([makeCombatant("A", [{ id: "poisoned" }])]); const { encounter } = success(e, "A", "blinded"); expect(encounter.combatants[0].conditions).toEqual([ { id: "blinded" }, { id: "poisoned" }, ]); }); it("prevents duplicate conditions", () => { const e = enc([makeCombatant("A", [{ id: "blinded" }])]); // Toggling blinded again removes it, not duplicates const { encounter } = success(e, "A", "blinded"); expect(encounter.combatants[0].conditions).toBeUndefined(); }); it("rejects unknown condition", () => { const e = enc([makeCombatant("A")]); const result = toggleCondition( e, combatantId("A"), "flying" as ConditionId, ); expectDomainError(result, "unknown-condition"); }); it("returns error for nonexistent combatant", () => { const e = enc([makeCombatant("A")]); const result = toggleCondition(e, combatantId("missing"), "blinded"); expectDomainError(result, "combatant-not-found"); }); it("does not mutate input encounter", () => { const e = enc([makeCombatant("A")]); const original = JSON.parse(JSON.stringify(e)); toggleCondition(e, combatantId("A"), "blinded"); expect(e).toEqual(original); }); it("normalizes empty array to undefined on removal", () => { const e = enc([makeCombatant("A", [{ id: "charmed" }])]); const { encounter } = success(e, "A", "charmed"); expect(encounter.combatants[0].conditions).toBeUndefined(); }); it("preserves order across all conditions", () => { const order = CONDITION_DEFINITIONS.map((d) => d.id); // Add in reverse order let e = enc([makeCombatant("A")]); for (const cond of [...order].reverse()) { const result = success(e, "A", cond); e = result.encounter; } expect(e.combatants[0].conditions).toEqual(order.map((id) => ({ id }))); }); }); describe("setConditionValue", () => { it("adds a valued condition at the specified value", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue(e, combatantId("A"), "frightened", 2); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "frightened", value: 2 }, ]); expect(result.events).toEqual([ { type: "ConditionAdded", combatantId: combatantId("A"), condition: "frightened", value: 2, }, ]); }); it("updates the value of an existing condition", () => { const e = enc([makeCombatant("A", [{ id: "frightened", value: 1 }])]); const result = setConditionValue(e, combatantId("A"), "frightened", 3); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "frightened", value: 3 }, ]); }); it("removes condition when value is 0", () => { const e = enc([makeCombatant("A", [{ id: "frightened", value: 2 }])]); const result = setConditionValue(e, combatantId("A"), "frightened", 0); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toBeUndefined(); expect(result.events[0].type).toBe("ConditionRemoved"); }); it("rejects unknown condition", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue( e, combatantId("A"), "flying" as ConditionId, 1, ); expectDomainError(result, "unknown-condition"); }); it("clamps value to maxValue for capped conditions", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue(e, combatantId("A"), "dying", 6); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "dying", value: 4 }, ]); expect(result.events[0]).toMatchObject({ type: "ConditionAdded", value: 4, }); }); it("allows value at exactly the max", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue(e, combatantId("A"), "doomed", 3); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "doomed", value: 3 }, ]); }); it("allows value below the max", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue(e, combatantId("A"), "wounded", 2); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "wounded", value: 2 }, ]); }); it("does not cap conditions without a maxValue", () => { const e = enc([makeCombatant("A")]); const result = setConditionValue(e, combatantId("A"), "frightened", 10); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "frightened", value: 10 }, ]); }); it("clamps when updating an existing capped condition", () => { const e = enc([makeCombatant("A", [{ id: "slowed-pf2e", value: 2 }])]); const result = setConditionValue(e, combatantId("A"), "slowed-pf2e", 5); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "slowed-pf2e", value: 3 }, ]); }); }); describe("decrementCondition", () => { it("decrements value by 1", () => { const e = enc([makeCombatant("A", [{ id: "frightened", value: 3 }])]); const result = decrementCondition(e, combatantId("A"), "frightened"); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toEqual([ { id: "frightened", value: 2 }, ]); }); it("removes condition when value reaches 0", () => { const e = enc([makeCombatant("A", [{ id: "frightened", value: 1 }])]); const result = decrementCondition(e, combatantId("A"), "frightened"); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toBeUndefined(); expect(result.events[0].type).toBe("ConditionRemoved"); }); it("removes non-valued condition (value undefined treated as 1)", () => { const e = enc([makeCombatant("A", [{ id: "blinded" }])]); const result = decrementCondition(e, combatantId("A"), "blinded"); if (isDomainError(result)) throw new Error(result.message); expect(result.encounter.combatants[0].conditions).toBeUndefined(); }); it("returns error for inactive condition", () => { const e = enc([makeCombatant("A")]); const result = decrementCondition(e, combatantId("A"), "frightened"); expectDomainError(result, "condition-not-active"); }); });