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:
@@ -6,6 +6,7 @@ import { createRng } from '@/lib/rng';
|
||||
import { rollDice } from '@/lib/dice/notation';
|
||||
import { getSystem, getConditions, type SystemId } from '@/lib/rules';
|
||||
import { useCharacters } from '@/features/characters/hooks';
|
||||
import { useConditionGlossary } from './useConditionGlossary';
|
||||
import {
|
||||
addCombatant,
|
||||
applyDamage,
|
||||
@@ -25,6 +26,7 @@ import { NumberField } from '@/components/ui/NumberField';
|
||||
|
||||
export function EncounterTracker({ encounter, campaign }: { encounter: Encounter; campaign: Campaign }) {
|
||||
const characters = useCharacters(campaign.id);
|
||||
const glossary = useConditionGlossary(campaign.system);
|
||||
|
||||
const mutate = (fn: (e: Encounter) => Encounter) => {
|
||||
void encountersRepo.save(fn(encounter));
|
||||
@@ -87,6 +89,7 @@ export function EncounterTracker({ encounter, campaign }: { encounter: Encounter
|
||||
key={c.id}
|
||||
combatant={c}
|
||||
system={campaign.system}
|
||||
glossary={glossary}
|
||||
isCurrent={isActive && idx === encounter.turnIndex}
|
||||
onChange={(patch) => mutate((e) => updateCombatant(e, c.id, patch))}
|
||||
onDamage={(amt) => mutate((e) => updateCombatant(e, c.id, { hp: applyDamage(c, amt).hp }))}
|
||||
@@ -206,6 +209,7 @@ function AddCombatantBar({
|
||||
function CombatantRow({
|
||||
combatant: c,
|
||||
system,
|
||||
glossary,
|
||||
isCurrent,
|
||||
onChange,
|
||||
onDamage,
|
||||
@@ -215,6 +219,7 @@ function CombatantRow({
|
||||
}: {
|
||||
combatant: Combatant;
|
||||
system: SystemId;
|
||||
glossary: Map<string, string>;
|
||||
isCurrent: boolean;
|
||||
onChange: (patch: Partial<Combatant>) => void;
|
||||
onDamage: (amt: number) => void;
|
||||
@@ -251,7 +256,7 @@ function CombatantRow({
|
||||
<button
|
||||
key={`${cond.name}-${i}`}
|
||||
className="rounded-full bg-warning/15 px-2 py-0.5 text-xs text-warning hover:line-through"
|
||||
title="Click to remove"
|
||||
title={glossary.get(cond.name.toLowerCase()) || 'Click to remove'}
|
||||
onClick={() => onChange({ conditions: c.conditions.filter((_, j) => j !== i) })}
|
||||
>
|
||||
{cond.name}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { SystemId } from '@/lib/rules';
|
||||
|
||||
/** name (lowercased) -> short description, per system. Cached across mounts. */
|
||||
const cache = new Map<SystemId, Map<string, string>>();
|
||||
|
||||
export function useConditionGlossary(system: SystemId): Map<string, string> {
|
||||
const [map, setMap] = useState<Map<string, string>>(() => cache.get(system) ?? new Map());
|
||||
|
||||
useEffect(() => {
|
||||
if (cache.has(system)) {
|
||||
setMap(cache.get(system)!);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const m = new Map<string, string>();
|
||||
try {
|
||||
if (system === '5e') {
|
||||
const mod = await import('@/data/srd/conditions.json');
|
||||
for (const c of mod.default as { name: string; description?: string }[]) {
|
||||
m.set(c.name.toLowerCase(), c.description ?? '');
|
||||
}
|
||||
} else {
|
||||
const res = await fetch(`${import.meta.env.BASE_URL}data/pf2e/conditions.json`);
|
||||
if (res.ok) {
|
||||
for (const c of (await res.json()) as { name: string; text?: string }[]) {
|
||||
m.set(c.name.toLowerCase(), (c.text ?? '').trim().slice(0, 400));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* tooltips are best-effort */
|
||||
}
|
||||
cache.set(system, m);
|
||||
if (!cancelled) setMap(m);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [system]);
|
||||
|
||||
return map;
|
||||
}
|
||||
Reference in New Issue
Block a user