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

@@ -46,17 +46,17 @@ describe("bestiary-cache fallback (IndexedDB unavailable)", () => {
it("cacheSource falls back to in-memory store", async () => {
const creatures = [makeCreature("mm:goblin", "Goblin")];
await cacheSource("MM", "Monster Manual", creatures);
await cacheSource("dnd", "MM", "Monster Manual", creatures);
expect(await isSourceCached("MM")).toBe(true);
expect(await isSourceCached("dnd", "MM")).toBe(true);
});
it("isSourceCached returns false for uncached source", async () => {
expect(await isSourceCached("XGE")).toBe(false);
expect(await isSourceCached("dnd", "XGE")).toBe(false);
});
it("getCachedSources returns sources from in-memory store", async () => {
await cacheSource("MM", "Monster Manual", [
await cacheSource("dnd", "MM", "Monster Manual", [
makeCreature("mm:goblin", "Goblin"),
]);
@@ -68,7 +68,7 @@ describe("bestiary-cache fallback (IndexedDB unavailable)", () => {
it("loadAllCachedCreatures assembles creatures from in-memory store", async () => {
const goblin = makeCreature("mm:goblin", "Goblin");
await cacheSource("MM", "Monster Manual", [goblin]);
await cacheSource("dnd", "MM", "Monster Manual", [goblin]);
const map = await loadAllCachedCreatures();
expect(map.size).toBe(1);
@@ -76,17 +76,17 @@ describe("bestiary-cache fallback (IndexedDB unavailable)", () => {
});
it("clearSource removes a single source from in-memory store", async () => {
await cacheSource("MM", "Monster Manual", []);
await cacheSource("VGM", "Volo's Guide", []);
await cacheSource("dnd", "MM", "Monster Manual", []);
await cacheSource("dnd", "VGM", "Volo's Guide", []);
await clearSource("MM");
await clearSource("dnd", "MM");
expect(await isSourceCached("MM")).toBe(false);
expect(await isSourceCached("VGM")).toBe(true);
expect(await isSourceCached("dnd", "MM")).toBe(false);
expect(await isSourceCached("dnd", "VGM")).toBe(true);
});
it("clearAll removes all data from in-memory store", async () => {
await cacheSource("MM", "Monster Manual", []);
await cacheSource("dnd", "MM", "Monster Manual", []);
await clearAll();
const sources = await getCachedSources();

View File

@@ -69,17 +69,17 @@ describe("bestiary-cache", () => {
describe("cacheSource", () => {
it("stores creatures and metadata", async () => {
const creatures = [makeCreature("mm:goblin", "Goblin")];
await cacheSource("MM", "Monster Manual", creatures);
await cacheSource("dnd", "MM", "Monster Manual", creatures);
expect(fakeStore.has("MM")).toBe(true);
const record = fakeStore.get("MM") as {
expect(fakeStore.has("dnd:MM")).toBe(true);
const record = fakeStore.get("dnd:MM") as {
sourceCode: string;
displayName: string;
creatures: Creature[];
creatureCount: number;
cachedAt: number;
};
expect(record.sourceCode).toBe("MM");
expect(record.sourceCode).toBe("dnd:MM");
expect(record.displayName).toBe("Monster Manual");
expect(record.creatures).toHaveLength(1);
expect(record.creatureCount).toBe(1);
@@ -89,12 +89,12 @@ describe("bestiary-cache", () => {
describe("isSourceCached", () => {
it("returns false for uncached source", async () => {
expect(await isSourceCached("XGE")).toBe(false);
expect(await isSourceCached("dnd", "XGE")).toBe(false);
});
it("returns true after caching", async () => {
await cacheSource("MM", "Monster Manual", []);
expect(await isSourceCached("MM")).toBe(true);
await cacheSource("dnd", "MM", "Monster Manual", []);
expect(await isSourceCached("dnd", "MM")).toBe(true);
});
});
@@ -105,11 +105,11 @@ describe("bestiary-cache", () => {
});
it("returns source info with creature counts", async () => {
await cacheSource("MM", "Monster Manual", [
await cacheSource("dnd", "MM", "Monster Manual", [
makeCreature("mm:goblin", "Goblin"),
makeCreature("mm:orc", "Orc"),
]);
await cacheSource("VGM", "Volo's Guide", [
await cacheSource("dnd", "VGM", "Volo's Guide", [
makeCreature("vgm:flind", "Flind"),
]);
@@ -137,8 +137,8 @@ describe("bestiary-cache", () => {
const orc = makeCreature("mm:orc", "Orc");
const flind = makeCreature("vgm:flind", "Flind");
await cacheSource("MM", "Monster Manual", [goblin, orc]);
await cacheSource("VGM", "Volo's Guide", [flind]);
await cacheSource("dnd", "MM", "Monster Manual", [goblin, orc]);
await cacheSource("dnd", "VGM", "Volo's Guide", [flind]);
const map = await loadAllCachedCreatures();
expect(map.size).toBe(3);
@@ -150,20 +150,20 @@ describe("bestiary-cache", () => {
describe("clearSource", () => {
it("removes a single source", async () => {
await cacheSource("MM", "Monster Manual", []);
await cacheSource("VGM", "Volo's Guide", []);
await cacheSource("dnd", "MM", "Monster Manual", []);
await cacheSource("dnd", "VGM", "Volo's Guide", []);
await clearSource("MM");
await clearSource("dnd", "MM");
expect(await isSourceCached("MM")).toBe(false);
expect(await isSourceCached("VGM")).toBe(true);
expect(await isSourceCached("dnd", "MM")).toBe(false);
expect(await isSourceCached("dnd", "VGM")).toBe(true);
});
});
describe("clearAll", () => {
it("removes all cached data", async () => {
await cacheSource("MM", "Monster Manual", []);
await cacheSource("VGM", "Volo's Guide", []);
await cacheSource("dnd", "MM", "Monster Manual", []);
await cacheSource("dnd", "VGM", "Volo's Guide", []);
await clearAll();

View File

@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import {
getAllPf2eSourceCodes,
getDefaultPf2eFetchUrl,
getPf2eSourceDisplayName,
loadPf2eBestiaryIndex,
} from "../pf2e-bestiary-index-adapter.js";
describe("loadPf2eBestiaryIndex", () => {
it("returns an object with sources and creatures", () => {
const index = loadPf2eBestiaryIndex();
expect(index.sources).toBeDefined();
expect(index.creatures).toBeDefined();
expect(Array.isArray(index.creatures)).toBe(true);
});
it("creatures have the expected PF2e shape", () => {
const index = loadPf2eBestiaryIndex();
expect(index.creatures.length).toBeGreaterThan(0);
const first = index.creatures[0];
expect(first).toHaveProperty("name");
expect(first).toHaveProperty("source");
expect(first).toHaveProperty("level");
expect(first).toHaveProperty("ac");
expect(first).toHaveProperty("hp");
expect(first).toHaveProperty("perception");
expect(first).toHaveProperty("size");
expect(first).toHaveProperty("type");
});
it("contains a substantial number of creatures", () => {
const index = loadPf2eBestiaryIndex();
expect(index.creatures.length).toBeGreaterThan(2000);
});
it("returns the same cached instance on subsequent calls", () => {
const a = loadPf2eBestiaryIndex();
const b = loadPf2eBestiaryIndex();
expect(a).toBe(b);
});
});
describe("getAllPf2eSourceCodes", () => {
it("returns all keys from the index sources", () => {
const codes = getAllPf2eSourceCodes();
const index = loadPf2eBestiaryIndex();
expect(codes).toEqual(Object.keys(index.sources));
});
});
describe("getDefaultPf2eFetchUrl", () => {
it("returns Pf2eTools GitHub URL with lowercase source code", () => {
const url = getDefaultPf2eFetchUrl("B1");
expect(url).toBe(
"https://raw.githubusercontent.com/Pf2eToolsOrg/Pf2eTools/dev/data/bestiary/creatures-b1.json",
);
});
});
describe("getPf2eSourceDisplayName", () => {
it("returns display name for a known source", () => {
expect(getPf2eSourceDisplayName("B1")).toBe("Bestiary");
});
it("falls back to source code for unknown source", () => {
expect(getPf2eSourceDisplayName("UNKNOWN")).toBe("UNKNOWN");
});
});

View File

@@ -1,23 +1,25 @@
import type { Creature, CreatureId } from "@initiative/domain";
import type { AnyCreature, CreatureId } from "@initiative/domain";
import { type IDBPDatabase, openDB } from "idb";
const DB_NAME = "initiative-bestiary";
const STORE_NAME = "sources";
const DB_VERSION = 3;
const DB_VERSION = 4;
interface CachedSourceInfo {
readonly sourceCode: string;
readonly displayName: string;
readonly creatureCount: number;
readonly cachedAt: number;
readonly system?: string;
}
interface CachedSourceRecord {
sourceCode: string;
displayName: string;
creatures: Creature[];
creatures: AnyCreature[];
cachedAt: number;
creatureCount: number;
system?: string;
}
let db: IDBPDatabase | null = null;
@@ -26,6 +28,10 @@ let dbFailed = false;
// In-memory fallback when IndexedDB is unavailable
const memoryStore = new Map<string, CachedSourceRecord>();
function scopedKey(system: string, sourceCode: string): string {
return `${system}:${sourceCode}`;
}
async function getDb(): Promise<IDBPDatabase | null> {
if (db) return db;
if (dbFailed) return null;
@@ -58,60 +64,77 @@ async function getDb(): Promise<IDBPDatabase | null> {
}
export async function cacheSource(
system: string,
sourceCode: string,
displayName: string,
creatures: Creature[],
creatures: AnyCreature[],
): Promise<void> {
const key = scopedKey(system, sourceCode);
const record: CachedSourceRecord = {
sourceCode,
sourceCode: key,
displayName,
creatures,
cachedAt: Date.now(),
creatureCount: creatures.length,
system,
};
const database = await getDb();
if (database) {
await database.put(STORE_NAME, record);
} else {
memoryStore.set(sourceCode, record);
memoryStore.set(key, record);
}
}
export async function isSourceCached(sourceCode: string): Promise<boolean> {
export async function isSourceCached(
system: string,
sourceCode: string,
): Promise<boolean> {
const key = scopedKey(system, sourceCode);
const database = await getDb();
if (database) {
const record = await database.get(STORE_NAME, sourceCode);
const record = await database.get(STORE_NAME, key);
return record !== undefined;
}
return memoryStore.has(sourceCode);
return memoryStore.has(key);
}
export async function getCachedSources(): Promise<CachedSourceInfo[]> {
export async function getCachedSources(
system?: string,
): Promise<CachedSourceInfo[]> {
const database = await getDb();
let records: CachedSourceRecord[];
if (database) {
const all: CachedSourceRecord[] = await database.getAll(STORE_NAME);
return all.map((r) => ({
sourceCode: r.sourceCode,
displayName: r.displayName,
creatureCount: r.creatureCount,
cachedAt: r.cachedAt,
}));
records = await database.getAll(STORE_NAME);
} else {
records = [...memoryStore.values()];
}
return [...memoryStore.values()].map((r) => ({
sourceCode: r.sourceCode,
const filtered = system
? records.filter((r) => r.system === system)
: records;
return filtered.map((r) => ({
sourceCode: r.system
? r.sourceCode.slice(r.system.length + 1)
: r.sourceCode,
displayName: r.displayName,
creatureCount: r.creatureCount,
cachedAt: r.cachedAt,
system: r.system,
}));
}
export async function clearSource(sourceCode: string): Promise<void> {
export async function clearSource(
system: string,
sourceCode: string,
): Promise<void> {
const key = scopedKey(system, sourceCode);
const database = await getDb();
if (database) {
await database.delete(STORE_NAME, sourceCode);
await database.delete(STORE_NAME, key);
} else {
memoryStore.delete(sourceCode);
memoryStore.delete(key);
}
}
@@ -125,9 +148,9 @@ export async function clearAll(): Promise<void> {
}
export async function loadAllCachedCreatures(): Promise<
Map<CreatureId, Creature>
Map<CreatureId, AnyCreature>
> {
const map = new Map<CreatureId, Creature>();
const map = new Map<CreatureId, AnyCreature>();
const database = await getDb();
let records: CachedSourceRecord[];

View File

@@ -0,0 +1,334 @@
import type {
CreatureId,
Pf2eCreature,
TraitBlock,
TraitSegment,
} from "@initiative/domain";
import { creatureId } from "@initiative/domain";
import { stripTags } from "./strip-tags.js";
// -- Raw Pf2eTools types (minimal, for parsing) --
interface RawPf2eCreature {
name: string;
source: string;
level?: number;
traits?: string[];
perception?: { std?: number };
senses?: { name?: string; type?: string }[];
languages?: { languages?: string[] };
skills?: Record<string, { std?: number }>;
abilityMods?: Record<string, number>;
items?: string[];
defenses?: RawDefenses;
speed?: Record<string, number | { number: number }>;
attacks?: RawAttack[];
abilities?: {
top?: RawAbility[];
mid?: RawAbility[];
bot?: RawAbility[];
};
_copy?: unknown;
}
interface RawDefenses {
ac?: Record<string, unknown>;
savingThrows?: {
fort?: { std?: number };
ref?: { std?: number };
will?: { std?: number };
};
hp?: { hp?: number }[];
immunities?: (string | { name: string })[];
resistances?: { amount: number; name: string; note?: string }[];
weaknesses?: { amount: number; name: string; note?: string }[];
}
interface RawAbility {
name?: string;
entries?: RawEntry[];
}
interface RawAttack {
range?: string;
name: string;
attack?: number;
traits?: string[];
damage?: string;
}
type RawEntry = string | RawEntryObject;
interface RawEntryObject {
type?: string;
items?: (string | { name?: string; entry?: string })[];
entries?: RawEntry[];
}
// -- Module state --
let sourceDisplayNames: Record<string, string> = {};
export function setPf2eSourceDisplayNames(names: Record<string, string>): void {
sourceDisplayNames = names;
}
// -- Helpers --
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function makeCreatureId(source: string, name: string): CreatureId {
const slug = name
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, "-")
.replaceAll(/(^-|-$)/g, "");
return creatureId(`${source.toLowerCase()}:${slug}`);
}
function formatSpeed(
speed: Record<string, number | { number: number }> | undefined,
): string {
if (!speed) return "";
const parts: string[] = [];
for (const [mode, value] of Object.entries(speed)) {
if (typeof value === "number") {
parts.push(
mode === "walk" ? `${value} feet` : `${capitalize(mode)} ${value} feet`,
);
} else if (typeof value === "object" && "number" in value) {
parts.push(
mode === "walk"
? `${value.number} feet`
: `${capitalize(mode)} ${value.number} feet`,
);
}
}
return parts.join(", ");
}
function formatSkills(
skills: Record<string, { std?: number }> | undefined,
): string | undefined {
if (!skills) return undefined;
const parts = Object.entries(skills)
.map(([name, val]) => `${capitalize(name)} +${val.std ?? 0}`)
.sort();
return parts.length > 0 ? parts.join(", ") : undefined;
}
function formatSenses(
senses: readonly { name?: string; type?: string }[] | undefined,
): string | undefined {
if (!senses || senses.length === 0) return undefined;
return senses
.map((s) => {
const label = s.name ?? s.type ?? "";
return label ? capitalize(label) : "";
})
.filter(Boolean)
.join(", ");
}
function formatLanguages(
languages: { languages?: string[] } | undefined,
): string | undefined {
if (!languages?.languages || languages.languages.length === 0)
return undefined;
return languages.languages.map(capitalize).join(", ");
}
function formatImmunities(
immunities: readonly (string | { name: string })[] | undefined,
): string | undefined {
if (!immunities || immunities.length === 0) return undefined;
return immunities
.map((i) => capitalize(typeof i === "string" ? i : i.name))
.join(", ");
}
function formatResistances(
resistances:
| readonly { amount: number; name: string; note?: string }[]
| undefined,
): string | undefined {
if (!resistances || resistances.length === 0) return undefined;
return resistances
.map((r) =>
r.note
? `${capitalize(r.name)} ${r.amount} (${r.note})`
: `${capitalize(r.name)} ${r.amount}`,
)
.join(", ");
}
function formatWeaknesses(
weaknesses:
| readonly { amount: number; name: string; note?: string }[]
| undefined,
): string | undefined {
if (!weaknesses || weaknesses.length === 0) return undefined;
return weaknesses
.map((w) =>
w.note
? `${capitalize(w.name)} ${w.amount} (${w.note})`
: `${capitalize(w.name)} ${w.amount}`,
)
.join(", ");
}
// -- Entry parsing --
function segmentizeEntries(entries: unknown): TraitSegment[] {
if (!Array.isArray(entries)) return [];
const segments: TraitSegment[] = [];
for (const entry of entries) {
if (typeof entry === "string") {
segments.push({ type: "text", value: stripTags(entry) });
} else if (typeof entry === "object" && entry !== null) {
const obj = entry as RawEntryObject;
if (obj.type === "list" && Array.isArray(obj.items)) {
segments.push({
type: "list",
items: obj.items.map((item) => {
if (typeof item === "string") {
return { text: stripTags(item) };
}
return { label: item.name, text: stripTags(item.entry ?? "") };
}),
});
} else if (Array.isArray(obj.entries)) {
segments.push(...segmentizeEntries(obj.entries));
}
}
}
return segments;
}
function formatAffliction(a: Record<string, unknown>): TraitSegment[] {
const parts: string[] = [];
if (a.note) parts.push(stripTags(String(a.note)));
if (a.DC) parts.push(`DC ${a.DC}`);
if (a.savingThrow) parts.push(String(a.savingThrow));
const stages = a.stages as
| { stage: number; entry: string; duration: string }[]
| undefined;
if (stages) {
for (const s of stages) {
parts.push(`Stage ${s.stage}: ${stripTags(s.entry)} (${s.duration})`);
}
}
return parts.length > 0 ? [{ type: "text", value: parts.join("; ") }] : [];
}
function normalizeAbilities(
abilities: readonly RawAbility[] | undefined,
): TraitBlock[] | undefined {
if (!abilities || abilities.length === 0) return undefined;
return abilities
.filter((a) => a.name)
.map((a) => {
const raw = a as Record<string, unknown>;
return {
name: stripTags(a.name as string),
segments: Array.isArray(a.entries)
? segmentizeEntries(a.entries)
: formatAffliction(raw),
};
});
}
function normalizeAttacks(
attacks: readonly RawAttack[] | undefined,
): TraitBlock[] | undefined {
if (!attacks || attacks.length === 0) return undefined;
return attacks.map((a) => {
const parts: string[] = [];
if (a.range) parts.push(a.range);
const attackMod = a.attack == null ? "" : ` +${a.attack}`;
const traits =
a.traits && a.traits.length > 0
? ` (${a.traits.map((t) => stripTags(t)).join(", ")})`
: "";
const damage = a.damage ? `, ${stripTags(a.damage)}` : "";
return {
name: capitalize(stripTags(a.name)),
segments: [
{
type: "text" as const,
value: `${parts.join(" ")}${attackMod}${traits}${damage}`,
},
],
};
});
}
// -- Defenses extraction --
function extractDefenses(defenses: RawDefenses | undefined) {
const acRecord = defenses?.ac ?? {};
const acStd = (acRecord.std as number | undefined) ?? 0;
const acEntries = Object.entries(acRecord).filter(([k]) => k !== "std");
return {
ac: acStd,
acConditional:
acEntries.length > 0
? acEntries.map(([k, v]) => `${v} ${k}`).join(", ")
: undefined,
saveFort: defenses?.savingThrows?.fort?.std ?? 0,
saveRef: defenses?.savingThrows?.ref?.std ?? 0,
saveWill: defenses?.savingThrows?.will?.std ?? 0,
hp: defenses?.hp?.[0]?.hp ?? 0,
immunities: formatImmunities(defenses?.immunities),
resistances: formatResistances(defenses?.resistances),
weaknesses: formatWeaknesses(defenses?.weaknesses),
};
}
// -- Main normalization --
function normalizeCreature(raw: RawPf2eCreature): Pf2eCreature {
const source = raw.source ?? "";
const defenses = extractDefenses(raw.defenses);
const mods = raw.abilityMods ?? {};
return {
system: "pf2e",
id: makeCreatureId(source, raw.name),
name: raw.name,
source,
sourceDisplayName: sourceDisplayNames[source] ?? source,
level: raw.level ?? 0,
traits: raw.traits ?? [],
perception: raw.perception?.std ?? 0,
senses: formatSenses(raw.senses),
languages: formatLanguages(raw.languages),
skills: formatSkills(raw.skills),
abilityMods: {
str: mods.str ?? 0,
dex: mods.dex ?? 0,
con: mods.con ?? 0,
int: mods.int ?? 0,
wis: mods.wis ?? 0,
cha: mods.cha ?? 0,
},
...defenses,
speed: formatSpeed(raw.speed),
attacks: normalizeAttacks(raw.attacks),
abilitiesTop: normalizeAbilities(raw.abilities?.top),
abilitiesMid: normalizeAbilities(raw.abilities?.mid),
abilitiesBot: normalizeAbilities(raw.abilities?.bot),
};
}
export function normalizePf2eBestiary(raw: {
creature: unknown[];
}): Pf2eCreature[] {
return (raw.creature ?? [])
.filter((c: unknown) => {
const obj = c as { _copy?: unknown };
return !obj._copy;
})
.map((c) => normalizeCreature(c as RawPf2eCreature));
}

View File

@@ -0,0 +1,70 @@
import type {
Pf2eBestiaryIndex,
Pf2eBestiaryIndexEntry,
} from "@initiative/domain";
import rawIndex from "../../../../data/bestiary/pf2e-index.json";
interface CompactCreature {
readonly n: string;
readonly s: string;
readonly lv: number;
readonly ac: number;
readonly hp: number;
readonly pc: number;
readonly sz: string;
readonly tp: string;
}
interface CompactIndex {
readonly sources: Record<string, string>;
readonly creatures: readonly CompactCreature[];
}
function mapCreature(c: CompactCreature): Pf2eBestiaryIndexEntry {
return {
name: c.n,
source: c.s,
level: c.lv,
ac: c.ac,
hp: c.hp,
perception: c.pc,
size: c.sz,
type: c.tp,
};
}
let cachedIndex: Pf2eBestiaryIndex | undefined;
export function loadPf2eBestiaryIndex(): Pf2eBestiaryIndex {
if (cachedIndex) return cachedIndex;
const compact = rawIndex as unknown as CompactIndex;
cachedIndex = {
sources: compact.sources,
creatures: compact.creatures.map(mapCreature),
};
return cachedIndex;
}
export function getAllPf2eSourceCodes(): string[] {
const index = loadPf2eBestiaryIndex();
return Object.keys(index.sources);
}
export function getDefaultPf2eFetchUrl(
sourceCode: string,
baseUrl?: string,
): string {
const filename = `creatures-${sourceCode.toLowerCase()}.json`;
if (baseUrl !== undefined) {
const normalized = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
return `${normalized}${filename}`;
}
return `https://raw.githubusercontent.com/Pf2eToolsOrg/Pf2eTools/dev/data/bestiary/${filename}`;
}
export function getPf2eSourceDisplayName(sourceCode: string): string {
const index = loadPf2eBestiaryIndex();
return index.sources[sourceCode] ?? sourceCode;
}

View File

@@ -1,8 +1,9 @@
import type {
AnyCreature,
BestiaryIndex,
Creature,
CreatureId,
Encounter,
Pf2eBestiaryIndex,
PlayerCharacter,
UndoRedoState,
} from "@initiative/domain";
@@ -31,15 +32,16 @@ export interface CachedSourceInfo {
export interface BestiaryCachePort {
cacheSource(
system: string,
sourceCode: string,
displayName: string,
creatures: Creature[],
creatures: AnyCreature[],
): Promise<void>;
isSourceCached(sourceCode: string): Promise<boolean>;
getCachedSources(): Promise<CachedSourceInfo[]>;
clearSource(sourceCode: string): Promise<void>;
isSourceCached(system: string, sourceCode: string): Promise<boolean>;
getCachedSources(system?: string): Promise<CachedSourceInfo[]>;
clearSource(system: string, sourceCode: string): Promise<void>;
clearAll(): Promise<void>;
loadAllCachedCreatures(): Promise<Map<CreatureId, Creature>>;
loadAllCachedCreatures(): Promise<Map<CreatureId, AnyCreature>>;
}
export interface BestiaryIndexPort {
@@ -48,3 +50,10 @@ export interface BestiaryIndexPort {
getDefaultFetchUrl(sourceCode: string, baseUrl?: string): string;
getSourceDisplayName(sourceCode: string): string;
}
export interface Pf2eBestiaryIndexPort {
loadIndex(): Pf2eBestiaryIndex;
getAllSourceCodes(): string[];
getDefaultFetchUrl(sourceCode: string, baseUrl?: string): string;
getSourceDisplayName(sourceCode: string): string;
}

View File

@@ -13,6 +13,7 @@ import {
} from "../persistence/undo-redo-storage.js";
import * as bestiaryCache from "./bestiary-cache.js";
import * as bestiaryIndex from "./bestiary-index-adapter.js";
import * as pf2eBestiaryIndex from "./pf2e-bestiary-index-adapter.js";
export const productionAdapters: Adapters = {
encounterPersistence: {
@@ -41,4 +42,10 @@ export const productionAdapters: Adapters = {
getDefaultFetchUrl: bestiaryIndex.getDefaultFetchUrl,
getSourceDisplayName: bestiaryIndex.getSourceDisplayName,
},
pf2eBestiaryIndex: {
loadIndex: pf2eBestiaryIndex.loadPf2eBestiaryIndex,
getAllSourceCodes: pf2eBestiaryIndex.getAllPf2eSourceCodes,
getDefaultFetchUrl: pf2eBestiaryIndex.getDefaultPf2eFetchUrl,
getSourceDisplayName: pf2eBestiaryIndex.getPf2eSourceDisplayName,
},
};