740cf20b93
- 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>
147 lines
4.9 KiB
TypeScript
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);
|
|
}
|