Implement the 021-bestiary-statblock feature that adds a searchable D&D 2024 Monster Manual creature library with inline autocomplete suggestions, full stat block display in a fixed side panel, auto-numbering of duplicate creature names, HP/AC pre-fill from bestiary data, and automatic stat block display on turn change for wide viewports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
146
specs/021-bestiary-statblock/data-model.md
Normal file
146
specs/021-bestiary-statblock/data-model.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Data Model: Bestiary Search & Stat Block Display
|
||||
|
||||
**Branch**: `021-bestiary-statblock` | **Date**: 2026-03-06
|
||||
|
||||
## Domain Entities
|
||||
|
||||
### CreatureId (branded type)
|
||||
|
||||
A branded string type for creature identity, following the same pattern as `CombatantId`.
|
||||
|
||||
```
|
||||
CreatureId = string & { __brand: "CreatureId" }
|
||||
```
|
||||
|
||||
Format: `{source}:{name-slug}` (e.g., `xmm:goblin`, `xmm:adult-red-dragon`)
|
||||
|
||||
### Creature
|
||||
|
||||
Represents a normalized bestiary entry. All fields are readonly.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | CreatureId | yes | Unique identifier |
|
||||
| name | string | yes | Display name |
|
||||
| source | string | yes | Source identifier (e.g., "XMM") |
|
||||
| sourceDisplayName | string | yes | Human-readable source (e.g., "MM 2024") |
|
||||
| size | string | yes | Size label(s) (e.g., "Medium", "Small or Medium") |
|
||||
| type | string | yes | Creature type (e.g., "Dragon (Chromatic)", "Humanoid") |
|
||||
| alignment | string | yes | Alignment text (e.g., "Chaotic Evil", "Unaligned") |
|
||||
| ac | number | yes | Armor class (numeric value) |
|
||||
| acSource | string or undefined | no | Armor source description (e.g., "natural armor", "plate armor") |
|
||||
| hp | object | yes | `{ average: number; formula: string }` |
|
||||
| speed | string | yes | Formatted speed string (e.g., "30 ft., fly 60 ft. (hover)") |
|
||||
| abilities | object | yes | `{ str, dex, con, int, wis, cha: number }` |
|
||||
| cr | string | yes | Challenge rating (e.g., "1/4", "17") |
|
||||
| proficiencyBonus | number | yes | Derived from CR |
|
||||
| passive | number | yes | Passive perception |
|
||||
| savingThrows | string or undefined | no | Formatted saves (e.g., "DEX +5, WIS +5") |
|
||||
| skills | string or undefined | no | Formatted skills (e.g., "Perception +7, Stealth +5") |
|
||||
| resist | string or undefined | no | Damage resistances |
|
||||
| immune | string or undefined | no | Damage immunities |
|
||||
| vulnerable | string or undefined | no | Damage vulnerabilities |
|
||||
| conditionImmune | string or undefined | no | Condition immunities |
|
||||
| senses | string or undefined | no | Senses (e.g., "Darkvision 60 ft.") |
|
||||
| languages | string or undefined | no | Languages |
|
||||
| traits | TraitBlock[] or undefined | no | Creature traits |
|
||||
| actions | TraitBlock[] or undefined | no | Actions |
|
||||
| bonusActions | TraitBlock[] or undefined | no | Bonus actions |
|
||||
| reactions | TraitBlock[] or undefined | no | Reactions |
|
||||
| legendaryActions | LegendaryBlock or undefined | no | Legendary actions with preamble |
|
||||
| spellcasting | SpellcastingBlock[] or undefined | no | Spellcasting entries |
|
||||
|
||||
### TraitBlock
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| name | string | yes | Trait/action name |
|
||||
| text | string | yes | Pre-rendered plain text (tags stripped) |
|
||||
|
||||
### LegendaryBlock
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| preamble | string | yes | Introductory text about legendary actions |
|
||||
| entries | TraitBlock[] | yes | Individual legendary actions |
|
||||
|
||||
### SpellcastingBlock
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| name | string | yes | "Spellcasting" or variant name |
|
||||
| headerText | string | yes | Pre-rendered header description |
|
||||
| atWill | string[] or undefined | no | At-will spells |
|
||||
| daily | DailySpells[] or undefined | no | Daily-use spells with uses count |
|
||||
| restLong | DailySpells[] or undefined | no | Per-long-rest spells |
|
||||
|
||||
### DailySpells
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| uses | number | yes | Number of uses |
|
||||
| each | boolean | yes | Whether "each" applies |
|
||||
| spells | string[] | yes | Spell names (plain text) |
|
||||
|
||||
### BestiarySource
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | string | yes | Source code (e.g., "XMM") |
|
||||
| displayName | string | yes | Human-readable name (e.g., "Monster Manual (2024)") |
|
||||
| tag | string or undefined | no | Optional tag (e.g., "legacy" for 2014 books) |
|
||||
|
||||
## Extended Existing Entities
|
||||
|
||||
### Combatant (extended)
|
||||
|
||||
Add one optional field:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| creatureId | CreatureId or undefined | no | Reference to bestiary creature for stat block lookup |
|
||||
|
||||
This field is:
|
||||
- Set when adding a combatant from the bestiary
|
||||
- Undefined when adding a plain-named combatant
|
||||
- Persisted to localStorage
|
||||
- Used by the web layer to look up creature data for stat block display
|
||||
|
||||
## State Transitions
|
||||
|
||||
### Adding a creature from bestiary
|
||||
1. User searches → selects creature
|
||||
2. System reads creature's name, hp.average, ac
|
||||
3. System checks existing combatants for name conflicts
|
||||
4. If conflicts: auto-number (rename first to "X 1" if needed, new one gets next number)
|
||||
5. Domain `addCombatant` called with resolved name
|
||||
6. `creatureId` stored on the new combatant
|
||||
|
||||
### Viewing a stat block
|
||||
1. User clicks search result or bestiary-linked combatant
|
||||
2. Web layer resolves `creatureId` to `Creature` from in-memory bestiary
|
||||
3. Stat block panel renders the `Creature` data
|
||||
4. No domain state change
|
||||
|
||||
## Validation Rules
|
||||
|
||||
- `CreatureId` must be non-empty branded string
|
||||
- `Creature.ac` must be a non-negative integer
|
||||
- `Creature.hp.average` must be a positive integer
|
||||
- `Creature.abilities` values must be positive integers (1-30 range)
|
||||
- `Creature.cr` must be a valid CR string
|
||||
- Auto-numbering suffix must be a positive integer
|
||||
- `Combatant.creatureId` when present must reference a valid creature in the loaded bestiary (graceful degradation if not found — stat block unavailable but combatant still functions)
|
||||
|
||||
## Proficiency Bonus Derivation
|
||||
|
||||
| CR | Proficiency Bonus |
|
||||
|----|-------------------|
|
||||
| 0 - 4 | +2 |
|
||||
| 5 - 8 | +3 |
|
||||
| 9 - 12 | +4 |
|
||||
| 13 - 16 | +5 |
|
||||
| 17 - 20 | +6 |
|
||||
| 21 - 24 | +7 |
|
||||
| 25 - 28 | +8 |
|
||||
| 29 - 30 | +9 |
|
||||
Reference in New Issue
Block a user