Combatants can now be assigned to party or enemy side via a toggle in the difficulty breakdown panel. Party-side NPCs subtract their XP from the encounter total, letting allied NPCs reduce difficulty. PCs default to party, non-PCs to enemy — users who don't use sides see no change. Side persists across reload and export/import. Closes #22 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
27 KiB
Feature Specification: Encounter Difficulty Indicator
Feature Branch: 008-encounter-difficulty
Created: 2026-03-27
Status: Draft
Input: Gitea issue #18 — "Encounter difficulty indicator (5.5e XP budget)", Gitea issue #22 — "Combatant side assignment for encounter difficulty"
User Scenarios & Testing (mandatory)
Difficulty Calculation
Story ED-1 — See encounter difficulty at a glance (Priority: P1)
A game master is building an encounter by adding monsters and player characters. As they add bestiary-linked creatures alongside PC combatants that have levels assigned, a compact 3-bar difficulty indicator appears in the top bar next to the active combatant name. The bars fill and change color to reflect the current difficulty tier: one green bar for Low, two yellow bars for Moderate, three red bars for High. Hovering over the indicator shows a tooltip with the difficulty label (e.g., "Moderate encounter difficulty").
Why this priority: This is the entire feature — without the indicator there is nothing to show.
Independent Test: Can be fully tested by adding PC combatants (with levels) and bestiary-linked monsters to an encounter and verifying the indicator appears with the correct difficulty tier.
Acceptance Scenarios:
-
Given an encounter with at least one PC combatant whose player character has a level and at least one bestiary-linked combatant, When the total monster XP is below the Low threshold, Then the indicator shows three empty bars (trivial difficulty).
-
Given an encounter where total monster XP meets or exceeds the Low threshold but is below Moderate, When the indicator renders, Then it shows one filled green bar and the tooltip reads "Low encounter difficulty".
-
Given an encounter where total monster XP meets or exceeds the Moderate threshold but is below High, When the indicator renders, Then it shows two filled yellow bars and the tooltip reads "Moderate encounter difficulty".
-
Given an encounter where total monster XP meets or exceeds the High threshold, When the indicator renders, Then it shows three filled red bars and the tooltip reads "High encounter difficulty".
-
Given an encounter where total monster XP exceeds the High threshold by a large margin, When the indicator renders, Then it still shows three filled red bars (High is the cap — there is no "above High" tier).
-
Given the difficulty indicator is visible, When a bestiary-linked combatant is added or removed, Then the indicator updates immediately to reflect the new difficulty tier.
-
Given the difficulty indicator is visible, When a PC combatant is added or removed, Then the indicator updates immediately to reflect the new party budget.
Indicator Visibility
Story ED-2 — Indicator hidden when data is insufficient (Priority: P1)
The difficulty indicator only appears when meaningful calculation is possible. If the encounter lacks PC combatants with levels or lacks bestiary-linked monsters, the indicator is hidden entirely rather than showing a confusing empty or zero state.
Why this priority: Showing an indicator when it can't calculate anything is worse than showing nothing — it would confuse users who don't use bestiary creatures or don't assign levels.
Independent Test: Can be tested by creating encounters with various combatant combinations and verifying the indicator appears or hides correctly.
Acceptance Scenarios:
-
Given an encounter with only custom combatants that have no
crassigned, When the top bar renders, Then no difficulty indicator is shown. -
Given an encounter with bestiary-linked monsters but no PC combatants, When the top bar renders, Then no difficulty indicator is shown.
-
Given an encounter with PC combatants whose player characters have no level assigned, When the top bar renders, Then no difficulty indicator is shown (even if bestiary-linked monsters are present).
-
Given an encounter with one leveled PC combatant and one bestiary-linked monster, When the last leveled PC is removed, Then the indicator disappears.
-
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
crassigned, Then the indicator disappears.
Player Character Level
Story ED-3 — Assign a level to a player character (Priority: P1)
The game master can set an optional level (1-20) when creating or editing a player character. This level is used to determine the party's XP budget for the difficulty calculation. Player characters without a level are silently excluded from the budget.
Why this priority: Without levels on PCs, the XP budget cannot be calculated and the indicator cannot function.
Independent Test: Can be tested by creating a player character with a level, adding it to an encounter with a bestiary creature, and verifying the difficulty indicator appears.
Acceptance Scenarios:
-
Given the create player character form is open, When the user sets level to 5, Then the player character is created with level 5.
-
Given the create player character form is open, When the user leaves the level field empty, Then the player character is created without a level.
-
Given a player character with no level, When the user edits the player character and sets level to 10, Then the level is saved and used in future difficulty calculations.
-
Given the level field is shown, When the user enters a value outside 1-20 (e.g., 0, 21, -1), Then a validation error is shown and the value is not accepted.
-
Given a player character with level 5 exists, When the page is reloaded, Then the level is restored as part of the player character data.
XP Budget Calculation
Story ED-4 — Correct XP budget from 5.5e rules (Priority: P1)
The difficulty calculation uses the 2024 5.5e XP Budget per Character table and a standard CR-to-XP mapping. The party's XP budget is the sum of per-character budgets for each PC combatant that has a level. The total monster XP is the sum of XP values for each bestiary-linked combatant's CR. The difficulty tier is determined by comparing total monster XP against the Low, Moderate, and High budget thresholds.
Why this priority: Incorrect calculation would make the feature misleading — the math must match the published rules.
Independent Test: Can be tested with pure domain function unit tests using known party/monster combinations from the 2024 DMG examples.
Acceptance Scenarios:
-
Given a party of four level 1 PCs (Low budget: 50 each = 200 total), When facing a single Bugbear (CR 1, 200 XP), Then the difficulty is Low (200 XP meets the Low threshold of 200 but is below Moderate at 300).
-
Given a party of five level 3 PCs (Moderate budget: 225 each = 1,125 total), When facing monsters totaling 1,125 XP, Then the difficulty is Moderate.
-
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).
-
Given an encounter with bestiary-linked combatants, custom combatants with CR assigned, and custom combatants without CR, When the XP total is calculated, Then enemy-side combatants with CR add XP to the monster total, party-side combatants with CR subtract XP from the monster total, and custom combatants without CR are excluded. The net monster XP is floored at 0.
-
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:
-
Given the difficulty indicator is visible, When the user taps the indicator, Then a breakdown panel opens showing party budget, two columns (Party and Enemy) listing combatants with their XP contributions, a side toggle per combatant, and the net monster XP total.
-
Given the breakdown panel is open, When the user taps outside the panel or taps a close control, Then the panel closes.
-
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.
-
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.
-
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).
-
Given the breakdown panel is open, When a combatant is added or removed from the encounter, Then the panel content updates immediately.
-
Given the breakdown panel is open, When the user toggles a combatant's side, Then it moves to the other column and the difficulty tier, monster XP total, and party budget update 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, 1–30). 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:
-
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, 1–30.
-
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.
-
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.
-
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.
-
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.
-
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:
-
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.
-
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.
-
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.
Side Assignment
Story ED-8 — Assign combatants to party or enemy side (Priority: P2)
A game master has allied NPCs fighting alongside the party. From the difficulty breakdown panel, they toggle an NPC to the party side. The NPC's XP is subtracted from the monster total instead of added, and the difficulty tier drops accordingly. PC combatants default to the party side and non-PC combatants default to the enemy side, so users who don't care about sides never interact with this feature.
Why this priority: Extends the breakdown panel (ED-5) with side assignment. Without sides, allied NPCs inflate difficulty artificially.
Independent Test: Can be tested by adding a leveled PC and two monsters, toggling one monster to party side, and verifying its XP is subtracted from the total.
Acceptance Scenarios:
-
Given the breakdown panel is open, When a non-PC combatant's side is toggled to party, Then its CR-derived XP is subtracted from the monster total instead of added, and the difficulty tier recalculates immediately.
-
Given a combatant with both a level (from its player character) and a CR on the party side, When the difficulty is calculated, Then it contributes to the party budget via its level AND subtracts its CR XP from the monster total — both effects apply independently.
-
Given party-side combatants whose total CR XP exceeds the enemy-side total, When the difficulty is calculated, Then the net monster XP is floored at 0 (difficulty cannot go negative).
-
Given the breakdown panel is open, When the user views a PC combatant, Then it appears in the Party column by default. When the user views a non-PC combatant, Then it appears in the Enemy column by default. Both can be toggled.
-
Given a combatant's side has been toggled, When the encounter is saved and the page is reloaded, Then the side assignment is restored.
-
Given a combatant's side has been toggled, When the encounter is exported to JSON and re-imported, Then the side assignment is preserved.
-
Given the breakdown panel is open, Then above the two columns a brief rules-oriented explanation is shown: "Allied NPC XP is subtracted from encounter difficulty" (tone is mechanical/rules-focused).
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."
- Zero monster XP: If all combatants with
creatureIdhave CR 0 (0 XP), the indicator shows three empty bars (trivial). - 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 without CR silently excluded: Custom combatants without
creatureIdand without a manually assignedcrdo 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
creatureIdand a manualcrvalue, 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.
- Net monster XP floored at 0: If party-side combatant XP exceeds enemy-side combatant XP, the net monster XP is 0 (trivial), not negative.
- Dual contribution (level + CR on party side): A combatant with both a level and a CR on the party side contributes to the party budget via level and subtracts from monster XP via CR. These are independent effects.
- Side defaults preserve opt-in: Because PCs default to party and others default to enemy, users who never assign sides see identical behavior to the pre-side-assignment calculation.
Requirements (mandatory)
Functional Requirements
FR-001 — XP Budget per Character table
The system MUST contain the 2024 5.5e XP Budget per Character lookup table mapping character levels 1-20 to Low, Moderate, and High XP thresholds.
FR-002 — CR-to-XP lookup table
The system MUST contain a CR-to-XP lookup table mapping all standard 5e challenge ratings (0, 1/8, 1/4, 1/2, 1-30) to their XP values.
FR-003 — Party XP budget calculation
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 — Net monster XP calculation
The system MUST calculate the net monster XP by summing the XP value (derived from CR) for each enemy-side combatant that has a CR and subtracting the XP value for each party-side 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. The net monster XP MUST be floored at 0.
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).
FR-006 — Difficulty indicator in top bar
The system MUST display a 3-bar difficulty indicator in the top bar, positioned to the right of the active combatant name.
FR-007 — Bar visual states
The indicator MUST display: three empty bars for trivial, one green filled bar for Low, two yellow filled bars for Moderate, three red filled bars for High.
FR-008 — Tooltip on hover
The indicator MUST show a tooltip on hover displaying the difficulty label (e.g., "Moderate encounter difficulty"). For the trivial state, the tooltip MUST read "Trivial encounter difficulty".
FR-009 — Live updates
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 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.
FR-012 — Level in create/edit forms
The player character create and edit forms MUST include an optional level field with validation constraining values to the 1-20 range.
FR-013 — Level persistence
The player character level MUST be persisted and restored across sessions, consistent with existing player character persistence behavior.
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 and side fields 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") and an optional side field accepting "party" or "enemy".
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), two stacked sections (Party and Enemy) using a columnar grid layout (name, toggle button, CR, XP) for aligned readability, the net monster XP total, and a brief rules-oriented explanation (e.g., "Allied NPC XP is subtracted from encounter difficulty"). Source names are omitted from the panel to conserve horizontal space.
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, 1–30.
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.
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.
FR-021 — Side defaults
When side is undefined, PC combatants MUST default to party side and all other combatants MUST default to enemy side. The useDifficulty hook resolves defaults before calling the domain function.
FR-022 — Party-side CR subtraction
Party-side combatants with CR MUST have their XP subtracted from the monster total. Party-side combatants with level MUST contribute to the party budget. These effects are independent — a combatant with both level and CR on party side contributes to budget AND subtracts from monster XP.
FR-023 — Side toggle in breakdown panel
The breakdown panel MUST provide a side toggle button per non-PC combatant to switch between party and enemy side. PC combatants are fixed to the party side and do not show a toggle. Toggling MUST immediately update the difficulty calculation. The toggle button uses an arrow icon with a hover background effect for discoverability.
FR-024 — Side persistence
The side field on Combatant MUST persist within the encounter across page reloads (via encounter storage) and MUST round-trip through JSON export/import.
FR-025 — Domain function signature
The calculateEncounterDifficulty domain function MUST accept combatant descriptors with { level?, cr?, side } so it can partition combatants internally, replacing the current partyLevels[] / monsterCrs[] signature.
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.
- CR-to-XP Table: A lookup mapping challenge rating strings ("0", "1/8", "1/4", "1/2", "1"-"30") to XP integer values.
- 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
PlayerCharacterentity defined in spec 005. - Combatant.cr: An optional string field on the existing
Combatantentity, accepting standard 5e CR values. Used for manual CR assignment on custom combatants. Ignored when the combatant has acreatureId. - Combatant.side: An optional string field (
"party"|"enemy") on the existingCombatantentity. When undefined, defaults are resolved by the hook layer: PC combatants default to"party", all others to"enemy".
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: The difficulty indicator correctly reflects the 2024 5.5e XP budget rules for all party level and monster CR combinations in the published tables.
- SC-002: The indicator updates within the same render cycle as combatant additions/removals — no perceptible delay.
- SC-003: Users can identify the encounter difficulty tier at a glance from the top bar without opening any modal or menu.
- SC-004: The indicator is completely hidden when the encounter lacks sufficient data for calculation, avoiding user confusion.
- 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.
- SC-010: Party-side combatants with CR correctly subtract their XP from the monster total, and the net XP is never negative.
Assumptions
- The 2024 5.5e XP Budget per Character table and CR-to-XP table are static data that do not change at runtime.
- The CR-to-XP mapping uses the standard 5e values (0 XP for CR 0, 25 XP for CR 1/8, 50 XP for CR 1/4, 100 XP for CR 1/2, 200 XP for CR 1, up to 155,000 XP for CR 30).
- Monster XP is derived solely from CR — no encounter multipliers are applied (the 5.5e system dropped the 2014 multiplier mechanic).
- The
levelfield is added to the existingPlayerCharactertype 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.
- 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 the 2014 DMG encounter multiplier mechanic or the four-tier (Easy/Medium/Hard/Deadly) system.
- MVP baseline does not include per-combatant level overrides — level is always derived from the player character template.