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,5 +1,5 @@
import {
type Creature,
type AnyCreature,
type CreatureId,
EMPTY_UNDO_REDO_STATE,
type Encounter,
@@ -12,10 +12,10 @@ export function createTestAdapters(options?: {
encounter?: Encounter | null;
undoRedoState?: UndoRedoState;
playerCharacters?: PlayerCharacter[];
creatures?: Map<CreatureId, Creature>;
creatures?: Map<CreatureId, AnyCreature>;
sources?: Map<
string,
{ displayName: string; creatures: Creature[]; cachedAt: number }
{ displayName: string; creatures: AnyCreature[]; cachedAt: number }
>;
}): Adapters {
let storedEncounter = options?.encounter ?? null;
@@ -25,7 +25,7 @@ export function createTestAdapters(options?: {
options?.sources ??
new Map<
string,
{ displayName: string; creatures: Creature[]; cachedAt: number }
{ displayName: string; creatures: AnyCreature[]; cachedAt: number }
>();
// Pre-populate sourceStore from creatures map if provided
@@ -33,7 +33,7 @@ export function createTestAdapters(options?: {
// No-op: creatures are accessed directly from the map
}
const creatureMap = options?.creatures ?? new Map<CreatureId, Creature>();
const creatureMap = options?.creatures ?? new Map<CreatureId, AnyCreature>();
return {
encounterPersistence: {
@@ -55,8 +55,9 @@ export function createTestAdapters(options?: {
},
},
bestiaryCache: {
cacheSource(sourceCode, displayName, creatures) {
sourceStore.set(sourceCode, {
cacheSource(system, sourceCode, displayName, creatures) {
const key = `${system}:${sourceCode}`;
sourceStore.set(key, {
displayName,
creatures,
cachedAt: Date.now(),
@@ -66,21 +67,25 @@ export function createTestAdapters(options?: {
}
return Promise.resolve();
},
isSourceCached(sourceCode) {
return Promise.resolve(sourceStore.has(sourceCode));
isSourceCached(system, sourceCode) {
return Promise.resolve(sourceStore.has(`${system}:${sourceCode}`));
},
getCachedSources() {
getCachedSources(system) {
return Promise.resolve(
[...sourceStore.entries()].map(([sourceCode, info]) => ({
sourceCode,
displayName: info.displayName,
creatureCount: info.creatures.length,
cachedAt: info.cachedAt,
})),
[...sourceStore.entries()]
.filter(([key]) => !system || key.startsWith(`${system}:`))
.map(([key, info]) => ({
sourceCode: key.includes(":")
? key.slice(key.indexOf(":") + 1)
: key,
displayName: info.displayName,
creatureCount: info.creatures.length,
cachedAt: info.cachedAt,
})),
);
},
clearSource(sourceCode) {
sourceStore.delete(sourceCode);
clearSource(system, sourceCode) {
sourceStore.delete(`${system}:${sourceCode}`);
return Promise.resolve();
},
clearAll() {
@@ -104,5 +109,12 @@ export function createTestAdapters(options?: {
},
getSourceDisplayName: (sourceCode) => sourceCode,
},
pf2eBestiaryIndex: {
loadIndex: () => ({ sources: {}, creatures: [] }),
getAllSourceCodes: () => [],
getDefaultFetchUrl: (sourceCode) =>
`https://example.com/creatures-${sourceCode.toLowerCase()}.json`,
getSourceDisplayName: (sourceCode) => sourceCode,
},
};
}