# Data Model: Encounter Difficulty Indicator **Date**: 2026-03-27 | **Feature**: 008-encounter-difficulty ## Entities ### PlayerCharacter (modified) Existing entity from spec 005. Adding one optional field. | Field | Type | Required | Notes | |-------|------|----------|-------| | id | PlayerCharacterId | yes | Existing — branded string | | name | string | yes | Existing | | ac | number | yes | Existing | | maxHp | number | yes | Existing | | color | PlayerColor | no | Existing | | icon | PlayerIcon | no | Existing | | **level** | **number** | **no** | **NEW — integer 1-20. Used for XP budget calculation. PCs without level are excluded from difficulty calc.** | **Validation rules for `level`**: - If provided, must be an integer - If provided, must be >= 1 and <= 20 - If omitted/undefined, PC is excluded from difficulty budget ### DifficultyTier (new) Enumeration of encounter difficulty categories. | Value | Display Label | Visual | |-------|---------------|--------| | `"trivial"` | Trivial | 3 empty bars | | `"low"` | Low | 1 green bar | | `"moderate"` | Moderate | 2 yellow bars | | `"high"` | High | 3 red bars | ### DifficultyResult (new) Output of the difficulty calculation. Pure data object. | Field | Type | Notes | |-------|------|-------| | tier | DifficultyTier | The determined difficulty category | | totalMonsterXp | number | Sum of XP for all bestiary-linked combatants | | partyBudget | { low: number; moderate: number; high: number } | XP thresholds for the party | ### XP Budget per Character (static lookup) Maps character level to XP thresholds. Data from 2024 5.5e DMG. | Level | Low | Moderate | High | |-------|-----|----------|------| | 1 | 50 | 75 | 100 | | 2 | 100 | 150 | 200 | | 3 | 150 | 225 | 400 | | 4 | 250 | 375 | 500 | | 5 | 500 | 750 | 1,100 | | 6 | 600 | 1,000 | 1,400 | | 7 | 750 | 1,300 | 1,700 | | 8 | 1,000 | 1,700 | 2,100 | | 9 | 1,300 | 2,000 | 2,600 | | 10 | 1,600 | 2,300 | 3,100 | | 11 | 1,900 | 2,900 | 4,100 | | 12 | 2,200 | 3,700 | 4,700 | | 13 | 2,600 | 4,200 | 5,400 | | 14 | 2,900 | 4,900 | 6,200 | | 15 | 3,300 | 5,400 | 7,800 | | 16 | 3,800 | 6,100 | 9,800 | | 17 | 4,500 | 7,200 | 11,700 | | 18 | 5,000 | 8,700 | 14,200 | | 19 | 5,500 | 10,700 | 17,200 | | 20 | 6,400 | 13,200 | 22,000 | ### CR-to-XP (static lookup) Maps challenge rating strings to XP values. Standard 5e values. | CR | XP | |----|-----| | 0 | 0 | | 1/8 | 25 | | 1/4 | 50 | | 1/2 | 100 | | 1 | 200 | | 2 | 450 | | 3 | 700 | | 4 | 1,100 | | 5 | 1,800 | | 6 | 2,300 | | 7 | 2,900 | | 8 | 3,900 | | 9 | 5,000 | | 10 | 5,900 | | 11 | 7,200 | | 12 | 8,400 | | 13 | 10,000 | | 14 | 11,500 | | 15 | 13,000 | | 16 | 15,000 | | 17 | 18,000 | | 18 | 20,000 | | 19 | 22,000 | | 20 | 25,000 | | 21 | 33,000 | | 22 | 41,000 | | 23 | 50,000 | | 24 | 62,000 | | 25 | 75,000 | | 26 | 90,000 | | 27 | 105,000 | | 28 | 120,000 | | 29 | 135,000 | | 30 | 155,000 | ## Relationships ``` PlayerCharacter (has optional level) │ ▼ linked via playerCharacterId Combatant (in Encounter) │ ▼ linked via creatureId Creature (has cr string) │ ▼ lookup via CR_TO_XP table XP value (number) Party levels ──► XP_BUDGET_TABLE ──► { low, moderate, high } thresholds Monster XP total ──► compare against thresholds ──► DifficultyTier ``` ## State Transitions The difficulty calculation is stateless — it's a pure derivation from current encounter state. No state machine or transitions to model. **Input derivation** (at adapter layer): 1. For each combatant with `playerCharacterId` → look up `PlayerCharacter.level` → collect non-undefined levels 2. For each combatant with `creatureId` → look up `Creature.cr` → collect CR strings 3. Pass `(levels[], crs[])` to domain function **Pure calculation** (domain layer): 1. Sum XP budget per level → `partyBudget.{low, moderate, high}` 2. Convert each CR to XP → sum → `totalMonsterXp` 3. Compare `totalMonsterXp` against thresholds → `DifficultyTier`