Files
initiative/packages/domain/src/__tests__/toggle-condition.test.ts
Lukas 553e09f280 Enforce maximum values for PF2e numbered conditions
Cap dying (4), doomed (3), wounded (3), and slowed (3) at their
rule-defined maximums. The domain clamps values in setConditionValue
and the condition picker disables the [+] button at the cap.

Closes #31

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:04:47 +02:00

262 lines
8.2 KiB
TypeScript

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");
});
});