Add Pathfinder 2e game system mode
All checks were successful
CI / check (push) Successful in 2m21s
CI / build-image (push) Successful in 24s

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:
Lukas
2026-04-07 01:26:22 +02:00
parent 8f6eebc43b
commit e62c49434c
67 changed files with 27758 additions and 527 deletions

View File

@@ -1,8 +1,4 @@
import type {
BestiaryIndexEntry,
ConditionId,
PlayerCharacter,
} from "@initiative/domain";
import type { ConditionId, PlayerCharacter } from "@initiative/domain";
import {
combatantId,
createEncounter,
@@ -11,6 +7,7 @@ import {
playerCharacterId,
} from "@initiative/domain";
import { describe, expect, it } from "vitest";
import type { SearchResult } from "../use-bestiary.js";
import { type EncounterState, encounterReducer } from "../use-encounter.js";
function emptyState(): EncounterState {
@@ -45,9 +42,11 @@ function stateWithHp(name: string, maxHp: number): EncounterState {
});
}
const BESTIARY_ENTRY: BestiaryIndexEntry = {
const BESTIARY_ENTRY: SearchResult = {
system: "dnd",
name: "Goblin",
source: "MM",
sourceDisplayName: "Monster Manual",
ac: 15,
hp: 7,
dex: 14,
@@ -57,6 +56,19 @@ const BESTIARY_ENTRY: BestiaryIndexEntry = {
type: "humanoid",
};
const PF2E_BESTIARY_ENTRY: SearchResult = {
system: "pf2e",
name: "Goblin Warrior",
source: "B1",
sourceDisplayName: "Bestiary",
level: -1,
ac: 16,
hp: 6,
perception: 5,
size: "small",
type: "humanoid",
};
describe("encounterReducer", () => {
describe("add-combatant", () => {
it("adds a combatant and pushes undo", () => {
@@ -236,7 +248,9 @@ describe("encounterReducer", () => {
conditionId: "blinded" as ConditionId,
});
expect(next.encounter.combatants[0].conditions).toContain("blinded");
expect(next.encounter.combatants[0].conditions).toContainEqual({
id: "blinded",
});
});
it("toggles concentration", () => {
@@ -327,6 +341,19 @@ describe("encounterReducer", () => {
expect(names).toContain("Goblin 1");
expect(names).toContain("Goblin 2");
});
it("adds PF2e creature with HP, AC, and creatureId", () => {
const next = encounterReducer(emptyState(), {
type: "add-from-bestiary",
entry: PF2E_BESTIARY_ENTRY,
});
const c = next.encounter.combatants[0];
expect(c.name).toBe("Goblin Warrior");
expect(c.maxHp).toBe(6);
expect(c.ac).toBe(16);
expect(c.creatureId).toBe("b1:goblin-warrior");
});
});
describe("add-multiple-from-bestiary", () => {