# 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 |