# Data Model: On-Demand Bestiary with Pre-Indexed Search **Feature**: 029-on-demand-bestiary **Date**: 2026-03-10 ## Domain Types ### BestiaryIndexEntry (NEW) A lightweight creature record from the pre-shipped search index. Contains only mechanical facts — no copyrightable prose. | Field | Type | Description | |-------|------|-------------| | name | string | Creature name (e.g., "Goblin Warrior") | | source | string | Source code (e.g., "XMM") | | ac | number | Armor class | | hp | number | Average hit points | | dex | number | Dexterity ability score | | cr | string | Challenge rating (e.g., "1/4", "10") | | initiativeProficiency | number | Initiative proficiency multiplier (0, 1, or 2) | | size | string | Size code (e.g., "M", "L", "T") | | type | string | Creature type (e.g., "humanoid", "fiend") | **Uniqueness**: name + source (same creature name may appear in different sources) **Derivable fields** (not stored, calculated at use): - CreatureId: `{source.toLowerCase()}:{slugify(name)}` - Source display name: resolved from BestiaryIndex.sources map - Proficiency bonus: derived from CR via existing `proficiencyBonus(cr)` function - Initiative modifier: derived from DEX + proficiency calculation ### BestiaryIndex (NEW) The complete pre-shipped index loaded from `data/bestiary/index.json`. | Field | Type | Description | |-------|------|-------------| | sources | Record | Map of source code → display name (e.g., "XMM" → "Monster Manual (2025)") | | creatures | BestiaryIndexEntry[] | All indexed creatures (3,312 entries) | ### Creature (EXISTING, unchanged) Full stat block data — available only after source data is fetched and cached. See `packages/domain/src/creature-types.ts` for complete definition. Key fields: id, name, source, sourceDisplayName, size, type, alignment, ac, acSource, hp (average + formula), speed, abilities, cr, initiativeProficiency, proficiencyBonus, passive, traits, actions, bonusActions, reactions, legendaryActions, spellcasting. ### Combatant (EXISTING, unchanged) Encounter participant. Links to creature via `creatureId?: CreatureId`. All combatant data (name, HP, AC, initiative) is stored independently from the creature — clearing the cache does not affect in-encounter combatants. ## Adapter Types ### CachedSourceRecord (NEW, adapter-layer only) Stored in IndexedDB. One record per imported source. | Field | Type | Description | |-------|------|-------------| | sourceCode | string | Primary key (e.g., "XMM") | | displayName | string | Human-readable source name | | creatures | Creature[] | Full normalized creature array | | cachedAt | number | Unix timestamp of when source was cached | | creatureCount | number | Number of creatures in this source (for management UI) | ### SourceFetchState (NEW, adapter-layer only) UI state for the source fetch/upload prompt. | Field | Type | Description | |-------|------|-------------| | sourceCode | string | Source being fetched | | displayName | string | Display name for the prompt | | defaultUrl | string | Pre-filled URL for this source | | status | "idle" \| "fetching" \| "error" \| "success" | Current fetch state | | error | string \| undefined | Error message if fetch failed | ## State Transitions ### Source Cache Lifecycle ``` UNCACHED → FETCHING → CACHED ↘ ERROR → (retry) → FETCHING ↘ (change URL) → FETCHING ↘ (upload file) → CACHED CACHED → CLEARED → UNCACHED ``` ### Stat Block View Flow ``` 1. User clicks creature → stat block panel opens 2. Check: creatureId exists on combatant? NO → show "No stat block available" (custom combatant) YES → continue 3. Check: source cached in IndexedDB? YES → lookup creature by ID → render stat block NO → show SourceFetchPrompt for this source 4. After successful fetch → creature available → render stat block ``` ## Storage Layout ### IndexedDB Database: "initiative-bestiary" **Object Store: "sources"** - Key path: `sourceCode` - Records: `CachedSourceRecord` - Expected size: 1-3 MB per source, ~150 MB if all 102 sources cached (unlikely) ### localStorage (unchanged) - Key: `"initiative:encounter"` — encounter state with combatant creatureId links ### Shipped Static Asset - `data/bestiary/index.json` — imported at build time, included in JS bundle (~52 KB gzipped) ## Relationships ``` BestiaryIndex (shipped, static) ├── sources: {sourceCode → displayName} └── creatures: BestiaryIndexEntry[] │ ├──[search]──→ BestiarySearch UI (displays name + source) ├──[add]──→ Combatant (name, HP, AC, creatureId) └──[view stat block]──→ CachedSourceRecord? │ ├── YES → Creature (full stat block) └── NO → SourceFetchPrompt │ ├── fetch URL → normalizeBestiary() → CachedSourceRecord └── upload file → normalizeBestiary() → CachedSourceRecord ```