Phase 2: full compendium across all categories + PF2e data

- Scraped PF2e reference from Archives of Nethys ES into public/data/pf2e/
  (spells, creatures, feats, equipment, weapons, armor, ancestries, heritages,
  backgrounds, archetypes, actions, conditions, deities) via scripts/fetch_pf2e.ts
- Registry-driven compendium: system toggle + per-category nav + generic filters
- 5e expanded: weapons, armor, feats, conditions added alongside spells/monsters/items
- PF2e data served as static assets (fetched, not bundled) for fast native parse
- Cross-links: add spell->spellbook / item->inventory to a campaign character;
  add monster/creature -> open encounter
- Condition tooltips in combat sourced from the conditions glossary
- Tests: registry filter unit tests, compendium e2e (browse + cross-link)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 01:00:40 +02:00
parent 0b356adf82
commit 379b79e6c4
28 changed files with 997 additions and 267 deletions
+58 -1
View File
@@ -1,4 +1,13 @@
import type { Monster, Spell, MagicItem } from './types';
import type {
Monster,
Spell,
MagicItem,
Weapon5e,
Armor5e,
Feat5e,
Condition5e,
CompendiumEntry,
} from './types';
/**
* Lazy, cached loaders for the SRD datasets. Dynamic `import()` keeps the
@@ -33,6 +42,54 @@ export async function loadItems(): Promise<MagicItem[]> {
return itemsCache;
}
// ---- Additional 5e categories (bundled — these files are small) ----
const cache = new Map<string, unknown[]>();
export async function loadWeapons5e(): Promise<Weapon5e[]> {
if (!cache.has('w5e')) {
const mod = await import('@/data/srd/weapons-full.json');
cache.set('w5e', mod.default as unknown as Weapon5e[]);
}
return cache.get('w5e') as Weapon5e[];
}
export async function loadArmor5e(): Promise<Armor5e[]> {
if (!cache.has('a5e')) {
const mod = await import('@/data/srd/equipment.json');
cache.set('a5e', (mod.default as { armor: Armor5e[] }).armor);
}
return cache.get('a5e') as Armor5e[];
}
export async function loadFeats5e(): Promise<Feat5e[]> {
if (!cache.has('f5e')) {
const mod = await import('@/data/srd/mpmb-feats.json');
const obj = mod.default as Record<string, Feat5e>;
cache.set('f5e', Object.values(obj).sort((a, b) => a.name.localeCompare(b.name)));
}
return cache.get('f5e') as Feat5e[];
}
export async function loadConditions5e(): Promise<Condition5e[]> {
if (!cache.has('c5e')) {
const mod = await import('@/data/srd/conditions.json');
cache.set('c5e', mod.default as unknown as Condition5e[]);
}
return cache.get('c5e') as Condition5e[];
}
// ---- PF2e categories (static assets fetched at runtime; large files) ----
const pf2eCache = new Map<string, CompendiumEntry[]>();
export async function loadPf2e(file: string): Promise<CompendiumEntry[]> {
if (!pf2eCache.has(file)) {
const res = await fetch(`${import.meta.env.BASE_URL}data/pf2e/${file}.json`);
if (!res.ok) throw new Error(`Failed to load PF2e ${file} (${res.status})`);
pf2eCache.set(file, (await res.json()) as CompendiumEntry[]);
}
return pf2eCache.get(file)!;
}
/** Build a combat-ready stat block summary from a monster. */
export function monsterToCombatant(m: Monster): {
name: string;
+36
View File
@@ -73,3 +73,39 @@ export interface MagicItem {
}
export type CompendiumKind = 'monsters' | 'spells' | 'items';
/** A loosely-typed entry for the generic, registry-driven categories. */
export type CompendiumEntry = Record<string, unknown> & { name: string; slug?: string };
export interface Weapon5e {
name: string;
slug: string;
category?: string;
cost?: string;
damage_dice?: string;
damage_type?: string;
weight?: string | number;
properties?: string[];
}
export interface Armor5e {
name: string;
type?: string;
ac?: string;
dexCap?: number | null;
stealthPenalty?: boolean;
weight?: number;
cost?: string;
}
export interface Feat5e {
name: string;
description?: string;
source?: { source: string; page: number }[];
}
export interface Condition5e {
name: string;
description?: string;
effects?: string[];
}