5.7 KiB
5.7 KiB
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
- User searches → selects creature
- System reads creature's name, hp.average, ac
- System checks existing combatants for name conflicts
- If conflicts: auto-number (rename first to "X 1" if needed, new one gets next number)
- Domain
addCombatantcalled with resolved name creatureIdstored on the new combatant
Viewing a stat block
- User clicks search result or bestiary-linked combatant
- Web layer resolves
creatureIdtoCreaturefrom in-memory bestiary - Stat block panel renders the
Creaturedata - No domain state change
Validation Rules
CreatureIdmust be non-empty branded stringCreature.acmust be a non-negative integerCreature.hp.averagemust be a positive integerCreature.abilitiesvalues must be positive integers (1-30 range)Creature.crmust be a valid CR string- Auto-numbering suffix must be a positive integer
Combatant.creatureIdwhen 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 |