Implement the 029-on-demand-bestiary feature that replaces the bundled XMM bestiary JSON with a compact search index (~350KB) and on-demand source loading, where users explicitly provide a URL or upload a JSON file to fetch full stat block data per source, which is then normalized and cached in IndexedDB (with in-memory fallback) so creature stat blocks load instantly on subsequent visits while keeping the app bundle small and never auto-fetching copyrighted content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
133
specs/029-on-demand-bestiary/data-model.md
Normal file
133
specs/029-on-demand-bestiary/data-model.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user