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
+6 -1
View File
@@ -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}