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

@@ -4,11 +4,13 @@ import {
adjustHpUseCase,
advanceTurnUseCase,
clearEncounterUseCase,
decrementConditionUseCase,
editCombatantUseCase,
redoUseCase,
removeCombatantUseCase,
retreatTurnUseCase,
setAcUseCase,
setConditionValueUseCase,
setCrUseCase,
setHpUseCase,
setInitiativeUseCase,
@@ -19,7 +21,6 @@ import {
undoUseCase,
} from "@initiative/application";
import type {
BestiaryIndexEntry,
CombatantId,
CombatantInit,
ConditionId,
@@ -40,6 +41,7 @@ import {
} from "@initiative/domain";
import { useCallback, useEffect, useReducer, useRef } from "react";
import { useAdapters } from "../contexts/adapter-context.js";
import type { SearchResult } from "./use-bestiary.js";
// -- Types --
@@ -61,14 +63,25 @@ type EncounterAction =
id: CombatantId;
conditionId: ConditionId;
}
| {
type: "set-condition-value";
id: CombatantId;
conditionId: ConditionId;
value: number;
}
| {
type: "decrement-condition";
id: CombatantId;
conditionId: ConditionId;
}
| { type: "toggle-concentration"; id: CombatantId }
| { type: "clear-encounter" }
| { type: "undo" }
| { type: "redo" }
| { type: "add-from-bestiary"; entry: BestiaryIndexEntry }
| { type: "add-from-bestiary"; entry: SearchResult }
| {
type: "add-multiple-from-bestiary";
entry: BestiaryIndexEntry;
entry: SearchResult;
count: number;
}
| { type: "add-from-player-character"; pc: PlayerCharacter }
@@ -156,7 +169,7 @@ function resolveAndRename(store: EncounterStore, name: string): string {
function addOneFromBestiary(
store: EncounterStore,
entry: BestiaryIndexEntry,
entry: SearchResult,
nextId: number,
): {
cId: CreatureId;
@@ -215,7 +228,7 @@ function handleUndoRedo(
function handleAddFromBestiary(
state: EncounterState,
entry: BestiaryIndexEntry,
entry: SearchResult,
count: number,
): EncounterState {
const { store, getEncounter } = makeStoreFromState(state);
@@ -325,6 +338,8 @@ function dispatchEncounterAction(
| { type: "set-cr" }
| { type: "set-side" }
| { type: "toggle-condition" }
| { type: "set-condition-value" }
| { type: "decrement-condition" }
| { type: "toggle-concentration" }
>,
): EncounterState {
@@ -373,6 +388,17 @@ function dispatchEncounterAction(
case "toggle-condition":
result = toggleConditionUseCase(store, action.id, action.conditionId);
break;
case "set-condition-value":
result = setConditionValueUseCase(
store,
action.id,
action.conditionId,
action.value,
);
break;
case "decrement-condition":
result = decrementConditionUseCase(store, action.id, action.conditionId);
break;
case "toggle-concentration":
result = toggleConcentrationUseCase(store, action.id);
break;
@@ -522,6 +548,16 @@ export function useEncounter() {
dispatch({ type: "toggle-condition", id, conditionId }),
[],
),
setConditionValue: useCallback(
(id: CombatantId, conditionId: ConditionId, value: number) =>
dispatch({ type: "set-condition-value", id, conditionId, value }),
[],
),
decrementCondition: useCallback(
(id: CombatantId, conditionId: ConditionId) =>
dispatch({ type: "decrement-condition", id, conditionId }),
[],
),
toggleConcentration: useCallback(
(id: CombatantId) => dispatch({ type: "toggle-concentration", id }),
[],
@@ -530,15 +566,12 @@ export function useEncounter() {
() => dispatch({ type: "clear-encounter" }),
[],
),
addFromBestiary: useCallback(
(entry: BestiaryIndexEntry): CreatureId | null => {
dispatch({ type: "add-from-bestiary", entry });
return null;
},
[],
),
addFromBestiary: useCallback((entry: SearchResult): CreatureId | null => {
dispatch({ type: "add-from-bestiary", entry });
return null;
}, []),
addMultipleFromBestiary: useCallback(
(entry: BestiaryIndexEntry, count: number): CreatureId | null => {
(entry: SearchResult, count: number): CreatureId | null => {
dispatch({
type: "add-multiple-from-bestiary",
entry,