134 lines
5.1 KiB
Markdown
134 lines
5.1 KiB
Markdown
# 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
|
|
```
|