Files
ttrpg_manager/src/lib/compendium/index.ts
T
NilsBriggen 740cf20b93 UI/UX "Living Codex" 4/n: assistant features + emoji purge
- Replace all emoji icons (~26 instances) with Lucide icons across the app
  (DashboardPage, SessionSidebar, PlayerBoards, MyCharacterPanel,
  PlayerViewPage, EncounterTracker, CampaignsPage, HandoutControl,
  SessionControl, CharacterSheet, EncounterTipCard, ActionGuide)
- Expose real 5e race data in wizard (ASI/vision/traits instead of stub desc);
  add vision field to loadRaces5e return type
- Surface subclass descriptions after picking a subclass in the wizard
- Add per-class tips, race synergy notes, and ability/skill guidance in wizard
- New ActionGuide component: collapsible "What can I do on my turn?" in player view
- New SessionPrepCard: AI + fallback session hook generator on assistant page
- New NpcGenCard: AI + fallback quick NPC generator with "Add to campaign" action
- Inline NPC detail generator (wand button) on each NPC card in NpcsPage
- Quest hook generator button in QuestsPage header
- Condition tooltips in player party view (useConditionGlossary)
- Pass campaign.system through session snapshot so player view is system-aware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:29:06 +02:00

147 lines
4.9 KiB
TypeScript

import type { SystemId } from '@/lib/rules';
import type { RulesetClass } from '@/lib/ruleset/normalize';
import type {
Monster,
Spell,
MagicItem,
Weapon5e,
Armor5e,
Feat5e,
Condition5e,
CompendiumEntry,
} from './types';
/**
* Lazy, cached loaders for the SRD datasets. Dynamic `import()` keeps the
* multi-hundred-KB JSON out of the main bundle — it loads only when the user
* opens the compendium (fixing the old app's eager MB-scale imports).
*/
let monstersCache: Monster[] | null = null;
let spellsCache: Spell[] | null = null;
let itemsCache: MagicItem[] | null = null;
export async function loadMonsters(): Promise<Monster[]> {
if (!monstersCache) {
const mod = await import('@/data/srd/monsters-srd.json');
monstersCache = mod.default as unknown as Monster[];
}
return monstersCache;
}
export async function loadSpells(): Promise<Spell[]> {
if (!spellsCache) {
const mod = await import('@/data/srd/spells-srd.json');
spellsCache = mod.default as unknown as Spell[];
}
return spellsCache;
}
export async function loadItems(): Promise<MagicItem[]> {
if (!itemsCache) {
const mod = await import('@/data/srd/magicitems-srd.json');
itemsCache = mod.default as unknown as 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[];
}
// ---- Structured ruleset classes (Open5e + Foundry pf2e; feed builder + compendium) ----
export async function loadClasses(system: SystemId): Promise<RulesetClass[]> {
const key = `classes:${system}`;
if (!cache.has(key)) {
if (system === '5e') {
const mod = await import('@/data/srd/classes.json');
cache.set(key, mod.default as unknown as RulesetClass[]);
} else {
const res = await fetch(`${import.meta.env.BASE_URL}data/pf2e/classes.json`);
cache.set(key, (res.ok ? await res.json() : []) as RulesetClass[]);
}
}
return cache.get(key) as RulesetClass[];
}
export async function loadRaces5e(): Promise<{ slug: string; name: string; desc: string; asi: string; speed: string; vision: string; traits: string }[]> {
if (!cache.has('races5e')) {
const mod = await import('@/data/srd/races.json');
cache.set('races5e', mod.default as unknown[]);
}
return cache.get('races5e') as { slug: string; name: string; desc: string; asi: string; speed: string; vision: string; traits: string }[];
}
export async function loadBackgrounds5e(): Promise<{ slug: string; name: string; desc: string; skills: string; feature: string }[]> {
if (!cache.has('bg5e')) {
const mod = await import('@/data/srd/backgrounds.json');
cache.set('bg5e', mod.default as unknown[]);
}
return cache.get('bg5e') as { slug: string; name: string; desc: string; skills: string; feature: string }[];
}
// ---- 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;
ac: number;
hp: number;
} {
return {
name: m.name,
ac: m.armor_class ?? 10,
hp: m.hit_points ?? 1,
};
}
export function crLabel(m: Monster): string {
if (m.challenge_rating) return m.challenge_rating;
if (m.cr === undefined) return '—';
if (m.cr === 0.125) return '1/8';
if (m.cr === 0.25) return '1/4';
if (m.cr === 0.5) return '1/2';
return String(m.cr);
}