Persistent damage displayed as compact tags with damage type icon and formula (e.g., Flame + "2d6"). Supports fire, bleed, acid, cold, electricity, poison, and mental types. One instance per type, added via sub-picker in the condition picker. PF2e only, persists across reload. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
238 lines
6.2 KiB
TypeScript
238 lines
6.2 KiB
TypeScript
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> = {}): 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");
|
|
});
|
|
});
|