Files
initiative/specs/008-encounter-difficulty/data-model.md
Lukas ef76b9c90b
All checks were successful
CI / check (push) Successful in 1m13s
CI / build-image (push) Successful in 16s
Add encounter difficulty indicator (5.5e XP budget)
Live 3-bar difficulty indicator in the top bar showing encounter
difficulty (Trivial/Low/Moderate/High) based on the 2024 5.5e XP
budget system. Automatically derived from PC levels and bestiary
creature CRs.

- Add optional level field (1-20) to PlayerCharacter
- Add CR-to-XP and XP Budget per Character lookup tables in domain
- Add calculateEncounterDifficulty pure function
- Add DifficultyIndicator component with color-coded bars and tooltip
- Add useDifficulty hook composing encounter, PC, and bestiary contexts
- Indicator hidden when no PCs with levels or no bestiary-linked monsters
- Level field in PC create/edit forms, persisted in storage

Closes #18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:55:48 +01:00

146 lines
3.9 KiB
Markdown

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