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>
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
import type { Creature } from "@initiative/domain";
|
|
import { creatureId } from "@initiative/domain";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// Mock idb to reject — simulates IndexedDB unavailable.
|
|
// This must be a separate file from bestiary-cache.test.ts because the
|
|
// module caches the db connection in a singleton; once openDB succeeds
|
|
// in one test, the fallback path is unreachable.
|
|
vi.mock("idb", () => ({
|
|
openDB: vi.fn().mockRejectedValue(new Error("IndexedDB unavailable")),
|
|
}));
|
|
|
|
const {
|
|
cacheSource,
|
|
isSourceCached,
|
|
getCachedSources,
|
|
clearSource,
|
|
clearAll,
|
|
loadAllCachedCreatures,
|
|
} = await import("../bestiary-cache.js");
|
|
|
|
function makeCreature(id: string, name: string): Creature {
|
|
return {
|
|
id: creatureId(id),
|
|
name,
|
|
source: "MM",
|
|
sourceDisplayName: "Monster Manual",
|
|
size: "Small",
|
|
type: "humanoid",
|
|
alignment: "neutral evil",
|
|
ac: 15,
|
|
hp: { average: 7, formula: "2d6" },
|
|
speed: "30 ft.",
|
|
abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 },
|
|
cr: "1/4",
|
|
initiativeProficiency: 0,
|
|
proficiencyBonus: 2,
|
|
passive: 9,
|
|
};
|
|
}
|
|
|
|
describe("bestiary-cache fallback (IndexedDB unavailable)", () => {
|
|
beforeEach(async () => {
|
|
await clearAll();
|
|
});
|
|
|
|
it("cacheSource falls back to in-memory store", async () => {
|
|
const creatures = [makeCreature("mm:goblin", "Goblin")];
|
|
await cacheSource("dnd", "MM", "Monster Manual", creatures);
|
|
|
|
expect(await isSourceCached("dnd", "MM")).toBe(true);
|
|
});
|
|
|
|
it("isSourceCached returns false for uncached source", async () => {
|
|
expect(await isSourceCached("dnd", "XGE")).toBe(false);
|
|
});
|
|
|
|
it("getCachedSources returns sources from in-memory store", async () => {
|
|
await cacheSource("dnd", "MM", "Monster Manual", [
|
|
makeCreature("mm:goblin", "Goblin"),
|
|
]);
|
|
|
|
const sources = await getCachedSources();
|
|
expect(sources).toHaveLength(1);
|
|
expect(sources[0].sourceCode).toBe("MM");
|
|
expect(sources[0].creatureCount).toBe(1);
|
|
});
|
|
|
|
it("loadAllCachedCreatures assembles creatures from in-memory store", async () => {
|
|
const goblin = makeCreature("mm:goblin", "Goblin");
|
|
await cacheSource("dnd", "MM", "Monster Manual", [goblin]);
|
|
|
|
const map = await loadAllCachedCreatures();
|
|
expect(map.size).toBe(1);
|
|
expect(map.get(creatureId("mm:goblin"))?.name).toBe("Goblin");
|
|
});
|
|
|
|
it("clearSource removes a single source from in-memory store", async () => {
|
|
await cacheSource("dnd", "MM", "Monster Manual", []);
|
|
await cacheSource("dnd", "VGM", "Volo's Guide", []);
|
|
|
|
await clearSource("dnd", "MM");
|
|
|
|
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("dnd", "MM", "Monster Manual", []);
|
|
await clearAll();
|
|
|
|
const sources = await getCachedSources();
|
|
expect(sources).toEqual([]);
|
|
});
|
|
});
|