Add manual CR assignment and difficulty breakdown panel
All checks were successful
CI / check (push) Successful in 2m20s
CI / build-image (push) Successful in 17s

Implement issue #21: custom combatants can now have a challenge rating
assigned via a new breakdown panel, opened by tapping the difficulty
indicator. Bestiary-linked combatants show read-only CR with source name;
custom combatants get a CR picker with all standard 5e values. CR persists
across reloads and round-trips through JSON export/import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-04-02 17:03:33 +02:00
parent 2c643cc98b
commit 1ae9e12cff
26 changed files with 1461 additions and 17 deletions

View File

@@ -47,7 +47,7 @@ The difficulty indicator only appears when meaningful calculation is possible. I
**Acceptance Scenarios**:
1. **Given** an encounter with only custom combatants (no `creatureId`), **When** the top bar renders, **Then** no difficulty indicator is shown.
1. **Given** an encounter with only custom combatants that have no `cr` assigned, **When** the top bar renders, **Then** no difficulty indicator is shown.
2. **Given** an encounter with bestiary-linked monsters but no PC combatants, **When** the top bar renders, **Then** no difficulty indicator is shown.
@@ -55,7 +55,7 @@ The difficulty indicator only appears when meaningful calculation is possible. I
4. **Given** an encounter with one leveled PC combatant and one bestiary-linked monster, **When** the last leveled PC is removed, **Then** the indicator disappears.
5. **Given** an encounter with one leveled PC combatant and one bestiary-linked monster, **When** the last bestiary-linked monster is removed (only custom combatants remain), **Then** the indicator disappears.
5. **Given** an encounter with one leveled PC combatant and one bestiary-linked monster, **When** the last bestiary-linked monster is removed and the remaining custom combatants have no `cr` assigned, **Then** the indicator disappears.
---
@@ -101,12 +101,82 @@ The difficulty calculation uses the 2024 5.5e XP Budget per Character table and
3. **Given** a party with PCs at different levels (e.g., three level 3 and one level 2), **When** the budget is calculated, **Then** each PC's budget is looked up individually by level and summed (not averaged).
4. **Given** an encounter with both bestiary-linked and custom combatants, **When** the XP total is calculated, **Then** only bestiary-linked combatants contribute XP (custom combatants are excluded).
4. **Given** an encounter with bestiary-linked combatants, custom combatants with CR assigned, and custom combatants without CR, **When** the XP total is calculated, **Then** bestiary-linked combatants contribute XP from their creature CR and custom combatants with CR contribute XP from their assigned CR. Custom combatants without CR are excluded.
5. **Given** a PC combatant whose player character has no level, **When** the budget is calculated, **Then** that PC is excluded from the budget (as if they are not in the party).
---
### Difficulty Breakdown
**Story ED-5 — View difficulty breakdown details (Priority: P2)**
The game master taps the difficulty indicator to open a breakdown panel. The panel shows the party XP budget (sum of per-PC budgets with the tier thresholds), a list of all combatants that contribute XP (each showing name, CR, and XP value), and the total monster XP. This gives the GM visibility into how the difficulty tier was calculated.
**Why this priority**: The indicator alone shows the tier but not the reasoning. The breakdown panel turns the indicator from a black box into a transparent tool the GM can act on.
**Independent Test**: Can be tested by creating an encounter with leveled PCs and monsters, tapping the indicator, and verifying the panel displays correct budget and per-monster XP values.
**Acceptance Scenarios**:
1. **Given** the difficulty indicator is visible, **When** the user taps the indicator, **Then** a breakdown panel opens showing party budget, per-combatant XP contributions, and total monster XP.
2. **Given** the breakdown panel is open, **When** the user taps outside the panel or taps a close control, **Then** the panel closes.
3. **Given** an encounter with three leveled PCs at levels 1, 3, and 5, **When** the breakdown panel is open, **Then** the party budget section shows the summed Low, Moderate, and High thresholds for those levels.
4. **Given** an encounter with two bestiary-linked monsters and one custom combatant with CR assigned, **When** the breakdown panel is open, **Then** all three appear in the combatant list with their name, CR, and XP value.
5. **Given** an encounter with a custom combatant that has no CR assigned, **When** the breakdown panel is open, **Then** that combatant appears in the list as "unassigned" (no XP contribution shown).
6. **Given** the breakdown panel is open, **When** a combatant is added or removed from the encounter, **Then** the panel content updates immediately.
---
### Manual CR Assignment
**Story ED-6 — Assign CR to a custom combatant (Priority: P2)**
From the difficulty breakdown panel, the game master can assign a challenge rating to any custom (non-bestiary) combatant. A CR picker offers all standard 5e CR values (0, 1/8, 1/4, 1/2, 130). Assigning a CR immediately updates that combatant's XP contribution, the total monster XP, and the difficulty tier.
**Why this priority**: Without CR assignment, custom combatants are invisible to the difficulty calculation. This closes the gap for GMs who don't use the bestiary.
**Independent Test**: Can be tested by adding a custom combatant, opening the breakdown panel, assigning a CR, and verifying the XP total and difficulty tier update.
**Acceptance Scenarios**:
1. **Given** the breakdown panel is open and a custom combatant has no CR, **When** the user taps the "unassigned" CR area for that combatant, **Then** a CR picker appears offering values: 0, 1/8, 1/4, 1/2, 130.
2. **Given** the CR picker is open for a custom combatant, **When** the user selects CR 5, **Then** the combatant's XP updates to 1,800 and the difficulty tier recalculates immediately.
3. **Given** a custom combatant has CR 2 assigned, **When** the user taps the CR value in the breakdown panel, **Then** the CR picker opens with CR 2 pre-selected, allowing the user to change it.
4. **Given** a custom combatant has CR 3 assigned, **When** the user selects a different CR from the picker, **Then** the XP contribution updates immediately to match the new CR.
5. **Given** a custom combatant has CR assigned, **When** the encounter is saved and the page is reloaded, **Then** the CR assignment is restored and the difficulty calculation reflects it.
6. **Given** a custom combatant has CR assigned, **When** the encounter is exported to JSON and re-imported, **Then** the CR assignment is preserved.
---
**Story ED-7 — Bestiary CR takes precedence over manual CR (Priority: P2)**
Bestiary-linked combatants derive their CR from the creature data. The breakdown panel shows their CR as read-only with the bestiary source name visible, making the precedence clear. The manual `cr` field on `Combatant` is ignored when `creatureId` is present.
**Why this priority**: Without clear precedence rules, a combatant could show conflicting CRs from bestiary data and manual assignment, confusing the GM.
**Independent Test**: Can be tested by adding a bestiary-linked combatant and verifying its CR is read-only in the breakdown panel.
**Acceptance Scenarios**:
1. **Given** a bestiary-linked combatant with CR 3 from creature data, **When** the breakdown panel is open, **Then** the combatant shows CR 3 as read-only with the bestiary source name visible.
2. **Given** a bestiary-linked combatant, **When** the user views it in the breakdown panel, **Then** no CR picker is available — the CR cannot be manually overridden.
3. **Given** a combatant that was custom but is later linked to a bestiary creature, **When** the breakdown panel is open, **Then** the CR derives from the creature data and any previously assigned manual CR is ignored.
---
### Edge Cases
- **All bars empty (trivial)**: When total monster XP is greater than 0 but below the Low threshold, the indicator shows three empty bars. This communicates "we can calculate, but it's trivial."
@@ -114,7 +184,9 @@ The difficulty calculation uses the 2024 5.5e XP Budget per Character table and
- **Mixed party levels**: PCs at different levels each contribute their own budget — the system handles heterogeneous parties correctly.
- **Duplicate PC combatants**: If the same player character is added to the encounter multiple times, each copy contributes to the party budget independently (each counts as a party member).
- **CR fractions**: Bestiary creatures can have fractional CRs (e.g., "1/4", "1/2"). The CR-to-XP lookup must handle these string formats.
- **Custom combatants silently excluded**: Custom combatants without `creatureId` do not appear in the XP total and are not flagged as warnings or errors.
- **Custom combatants without CR silently excluded**: Custom combatants without `creatureId` and without a manually assigned `cr` do not appear in the XP total and are not flagged as warnings or errors. They appear in the breakdown panel as "unassigned."
- **Bestiary CR overrides manual CR**: If a combatant has both `creatureId` and a manual `cr` value, the bestiary CR is used and the manual value is ignored. The breakdown panel makes this visible by showing the CR as read-only.
- **CR assignment on combatant later linked to bestiary**: If a custom combatant with a manual CR is subsequently linked to a bestiary creature, the manual CR becomes irrelevant — the creature's CR takes over.
- **PCs without level silently excluded**: PC combatants whose player character has no level do not contribute to the budget and are not flagged.
- **Indicator with empty encounter**: When the encounter has no combatants, the indicator is hidden (the top bar may not even render per existing behavior).
- **Level field on existing player characters**: Existing player characters created before this feature will have no level. They are treated as "no level assigned" — no migration or default is needed.
@@ -135,7 +207,7 @@ The system MUST contain a CR-to-XP lookup table mapping all standard 5e challeng
The system MUST calculate the party's XP budget by summing the per-character budget for each PC combatant whose player character has a level assigned. PCs without a level are excluded from the sum.
#### FR-004 — Monster XP total calculation
The system MUST calculate the total monster XP by summing the XP value (derived from CR) for each combatant that has a `creatureId`. Combatants without `creatureId` are excluded.
The system MUST calculate the total monster XP by summing the XP value (derived from CR) for each combatant that has a CR. For bestiary-linked combatants, CR is derived from the creature data via `creatureId`. For custom combatants, CR comes from the optional `cr` field. Combatants with neither `creatureId` nor `cr` are excluded.
#### FR-005 — Difficulty tier determination
The system MUST determine the encounter difficulty tier by comparing total monster XP against the party's Low, Moderate, and High thresholds. The tier is the highest threshold that the total XP meets or exceeds. If below Low, the encounter is trivial (no tier label).
@@ -153,7 +225,7 @@ The indicator MUST show a tooltip on hover displaying the difficulty label (e.g.
The indicator MUST update immediately when combatants are added to or removed from the encounter.
#### FR-010 — Hidden when data insufficient
The indicator MUST be hidden when the encounter has no PC combatants with levels OR no bestiary-linked combatants.
The indicator MUST be hidden when the encounter has no PC combatants with levels OR no combatants with CR (neither bestiary-linked nor custom combatants with `cr` assigned).
#### FR-011 — Optional level field on PlayerCharacter
The `PlayerCharacter` entity MUST support an optional `level` field accepting integer values 1-20.
@@ -167,6 +239,24 @@ The player character level MUST be persisted and restored across sessions, consi
#### FR-014 — High is the cap
When total monster XP exceeds the High threshold, the indicator MUST display the High state (three red bars). There is no tier above High.
#### FR-015 — Optional CR field on Combatant
The `Combatant` entity MUST support an optional `cr` field accepting standard 5e challenge rating strings ("0", "1/8", "1/4", "1/2", "1""30").
#### FR-016 — Tappable difficulty indicator
The difficulty indicator MUST be tappable, opening a difficulty breakdown panel.
#### FR-017 — Breakdown panel content
The breakdown panel MUST display: the party XP budget (with Low, Moderate, High thresholds), a list of combatants showing name, CR, and XP contribution, and the total monster XP.
#### FR-018 — CR picker for custom combatants
The breakdown panel MUST provide a CR picker for custom combatants (those without `creatureId`) offering all standard 5e CR values: 0, 1/8, 1/4, 1/2, 130.
#### FR-019 — Bestiary CR precedence
When a combatant has a `creatureId`, the system MUST derive CR from the linked creature data. The manual `cr` field MUST be ignored. The breakdown panel MUST display bestiary-linked CRs as read-only with the source name visible.
#### FR-020 — CR persistence
The `cr` field on `Combatant` MUST persist within the encounter across page reloads (via encounter storage) and MUST round-trip through JSON export/import.
### Key Entities
- **XP Budget Table**: A lookup mapping character level (1-20) to three XP thresholds (Low, Moderate, High), sourced from the 2024 5.5e DMG.
@@ -174,6 +264,7 @@ When total monster XP exceeds the High threshold, the indicator MUST display the
- **DifficultyTier**: An enumeration of difficulty categories: Trivial, Low, Moderate, High.
- **DifficultyResult**: The output of the calculation containing the tier, total monster XP, and per-tier budget thresholds.
- **PlayerCharacter.level**: An optional integer (1-20) added to the existing `PlayerCharacter` entity defined in spec 005.
- **Combatant.cr**: An optional string field on the existing `Combatant` entity, accepting standard 5e CR values. Used for manual CR assignment on custom combatants. Ignored when the combatant has a `creatureId`.
---
@@ -188,6 +279,8 @@ When total monster XP exceeds the High threshold, the indicator MUST display the
- **SC-005**: The difficulty calculation is a pure domain function with no I/O, consistent with the project's deterministic domain core.
- **SC-006**: The domain module for difficulty calculation has zero imports from application, adapter, or UI layers.
- **SC-007**: The optional level field integrates seamlessly into the existing player character create/edit workflow without disrupting existing functionality.
- **SC-008**: The difficulty breakdown panel correctly displays per-combatant XP contributions and party budget that sum to the values used for tier determination.
- **SC-009**: Custom combatants with manually assigned CR contribute correctly to the difficulty calculation, matching the same CR-to-XP mapping used for bestiary creatures.
---
@@ -199,7 +292,7 @@ When total monster XP exceeds the High threshold, the indicator MUST display the
- The `level` field is added to the existing `PlayerCharacter` type from spec 005. No new entity or storage mechanism is needed.
- Existing player characters without a level are treated as "no level assigned" with no migration.
- The difficulty indicator occupies minimal horizontal space in the top bar and does not interfere with the combatant name truncation or other controls.
- MVP baseline does not include CR assignment for custom (non-bestiary) combatants.
- The breakdown panel is the sole UI surface for manual CR assignment — there is no CR field in the combatant create/edit forms.
- MVP baseline does not include assigning combatants to party/enemy sides — all combatants with CR are counted as enemies.
- MVP baseline does not include the 2014 DMG encounter multiplier mechanic or the four-tier (Easy/Medium/Hard/Deadly) system.
- MVP baseline does not include showing XP totals or budget numbers in the indicator — only the visual bars and tooltip label.
- MVP baseline does not include per-combatant level overrides — level is always derived from the player character template.