Files
initiative/specs/029-on-demand-bestiary/data-model.md

5.1 KiB

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<string, string> 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