Add Pathfinder 2e game system mode
Implements PF2e as an alternative game system alongside D&D 5e/5.5e. Settings modal "Game System" selector switches conditions, bestiary, stat block layout, and initiative calculation between systems. - Valued conditions with increment/decrement UX (Clumsy 2, Frightened 3) - 2,502 PF2e creatures from bundled search index (77 sources) - PF2e stat block: level, traits, Perception, Fort/Ref/Will, ability mods - Perception-based initiative rolling - System-scoped source cache (D&D and PF2e sources don't collide) - Backwards-compatible condition rehydration (ConditionId[] → ConditionEntry[]) - Difficulty indicator hidden in PF2e mode (excluded from MVP) Closes dostulata/initiative#19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ConditionId } from "../conditions.js";
|
||||
import type { ConditionEntry, ConditionId } from "../conditions.js";
|
||||
import { CONDITION_DEFINITIONS } from "../conditions.js";
|
||||
import { toggleCondition } from "../toggle-condition.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 ConditionId[],
|
||||
conditions?: readonly ConditionEntry[],
|
||||
): Combatant {
|
||||
return conditions
|
||||
? { id: combatantId(name), name, conditions }
|
||||
@@ -32,7 +36,7 @@ describe("toggleCondition", () => {
|
||||
const e = enc([makeCombatant("A")]);
|
||||
const { encounter, events } = success(e, "A", "blinded");
|
||||
|
||||
expect(encounter.combatants[0].conditions).toEqual(["blinded"]);
|
||||
expect(encounter.combatants[0].conditions).toEqual([{ id: "blinded" }]);
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "ConditionAdded",
|
||||
@@ -43,7 +47,7 @@ describe("toggleCondition", () => {
|
||||
});
|
||||
|
||||
it("removes a condition when already present", () => {
|
||||
const e = enc([makeCombatant("A", ["blinded"])]);
|
||||
const e = enc([makeCombatant("A", [{ id: "blinded" }])]);
|
||||
const { encounter, events } = success(e, "A", "blinded");
|
||||
|
||||
expect(encounter.combatants[0].conditions).toBeUndefined();
|
||||
@@ -57,14 +61,17 @@ describe("toggleCondition", () => {
|
||||
});
|
||||
|
||||
it("maintains definition order when adding conditions", () => {
|
||||
const e = enc([makeCombatant("A", ["poisoned"])]);
|
||||
const e = enc([makeCombatant("A", [{ id: "poisoned" }])]);
|
||||
const { encounter } = success(e, "A", "blinded");
|
||||
|
||||
expect(encounter.combatants[0].conditions).toEqual(["blinded", "poisoned"]);
|
||||
expect(encounter.combatants[0].conditions).toEqual([
|
||||
{ id: "blinded" },
|
||||
{ id: "poisoned" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("prevents duplicate conditions", () => {
|
||||
const e = enc([makeCombatant("A", ["blinded"])]);
|
||||
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();
|
||||
@@ -96,7 +103,7 @@ describe("toggleCondition", () => {
|
||||
});
|
||||
|
||||
it("normalizes empty array to undefined on removal", () => {
|
||||
const e = enc([makeCombatant("A", ["charmed"])]);
|
||||
const e = enc([makeCombatant("A", [{ id: "charmed" }])]);
|
||||
const { encounter } = success(e, "A", "charmed");
|
||||
|
||||
expect(encounter.combatants[0].conditions).toBeUndefined();
|
||||
@@ -110,6 +117,91 @@ describe("toggleCondition", () => {
|
||||
const result = success(e, "A", cond);
|
||||
e = result.encounter;
|
||||
}
|
||||
expect(e.combatants[0].conditions).toEqual(order);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user