Add encounter difficulty indicator (5.5e XP budget)
All checks were successful
CI / check (push) Successful in 1m13s
CI / build-image (push) Successful in 16s

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>
This commit is contained in:
Lukas
2026-03-27 22:55:48 +01:00
parent 36122b500b
commit ef76b9c90b
32 changed files with 1648 additions and 11 deletions

View File

@@ -0,0 +1,171 @@
# Tasks: Encounter Difficulty Indicator
**Input**: Design documents from `/specs/008-encounter-difficulty/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
**Organization**: Tasks are grouped by user story (ED-1 through ED-4) to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., ED-1, ED-3, ED-4)
- Include exact file paths in descriptions
---
## Phase 1: Foundational (Level field on PlayerCharacter)
**Purpose**: Add optional `level` field to `PlayerCharacter` — required by all user stories since the difficulty calculation depends on party levels.
**⚠️ CRITICAL**: The difficulty indicator cannot function without PC levels. This must complete first.
- [x] T001 Add `level?: number` to `PlayerCharacter` interface in `packages/domain/src/player-character-types.ts`
- [x] T002 [P] Add level validation to `createPlayerCharacter()` in `packages/domain/src/create-player-character.ts` — validate if provided: integer, 1-20, error code `"invalid-level"`
- [x] T003 [P] Add level validation to `validateFields()` and apply in `applyFields()` in `packages/domain/src/edit-player-character.ts`
- [x] T004 [P] Add level tests to `packages/domain/src/__tests__/create-player-character.test.ts` — valid level, no level, out-of-range, non-integer
- [x] T005 [P] Add level tests to `packages/domain/src/__tests__/edit-player-character.test.ts` — set level, clear level, invalid level
- [x] T006 Update `CreatePlayerCharacterUseCase` to accept and pass `level` in `packages/application/src/create-player-character-use-case.ts`
- [x] T007 [P] Update `EditPlayerCharacterUseCase` to accept and pass `level` in `packages/application/src/edit-player-character-use-case.ts`
- [x] T008 Update player characters context to pass `level` in create/edit calls in `apps/web/src/contexts/player-characters-context.tsx`
- [x] T009 Add level input field to create player modal in `apps/web/src/components/create-player-modal.tsx` — optional number input, 1-20 range
- [x] T010 Add level display and edit support in player character manager in `apps/web/src/components/player-character-manager.tsx`
- [x] T011 Export updated `PlayerCharacter` type from `packages/domain/src/index.ts` (verify re-export includes level)
**Checkpoint**: Player characters can be created/edited with an optional level. Existing PCs without level continue to work. All quality gates pass.
---
## Phase 2: User Story 4 — XP Budget Calculation (Priority: P1) 🎯 MVP Core
**Goal**: Implement the pure domain difficulty calculation with CR-to-XP and XP Budget tables.
**Independent Test**: Verified with unit tests using known party/monster combinations from the 2024 DMG examples.
### Implementation for User Story 4
- [x] T012 Create `packages/domain/src/encounter-difficulty.ts` with `DifficultyTier` type (`"trivial" | "low" | "moderate" | "high"`), `DifficultyResult` interface, `CR_TO_XP` lookup table (Record mapping all CRs 0 through 30 including fractions to XP values), and `XP_BUDGET_PER_CHARACTER` lookup table (Record mapping levels 1-20 to `{ low, moderate, high }`)
- [x] T013 Implement `crToXp(cr: string): number` in `packages/domain/src/encounter-difficulty.ts` — returns XP for given CR string, 0 for unknown CRs
- [x] T014 Implement `calculateEncounterDifficulty(partyLevels: number[], monsterCrs: string[]): DifficultyResult` in `packages/domain/src/encounter-difficulty.ts` — sums party budget per level, sums monster XP per CR, determines tier by comparing total XP against thresholds
- [x] T015 Export `DifficultyTier`, `DifficultyResult`, `crToXp`, `calculateEncounterDifficulty` from `packages/domain/src/index.ts` (same file as T011 — merge into one edit)
- [x] T016 Create `packages/domain/src/__tests__/encounter-difficulty.test.ts` with tests for: all CR string formats (0, 1/8, 1/4, 1/2, integers 1-30), unknown CR returns 0, all difficulty tiers (trivial/low/moderate/high), DMG example encounters (4x level 1 vs Bugbear = Low, 5x level 3 vs 1125 XP = Moderate), mixed party levels, empty arrays, High as cap (XP far exceeding High threshold still returns "high")
**Checkpoint**: Domain difficulty calculation is complete, tested, and exported. All quality gates pass.
---
## Phase 3: User Story 1 — See Encounter Difficulty at a Glance (Priority: P1) 🎯 MVP
**Goal**: Display the 3-bar difficulty indicator in the top bar that updates live as combatants change.
**Independent Test**: Add PC combatants with levels and bestiary-linked monsters — indicator appears with correct tier. Add/remove combatants — indicator updates.
### Implementation for User Story 1
- [x] T017 Create `apps/web/src/hooks/use-difficulty.ts` — hook that consumes `useEncounterContext()`, `usePlayerCharactersContext()`, and `useBestiary()` to derive party levels and monster CRs from current encounter, calls `calculateEncounterDifficulty()`, returns `DifficultyResult | null` (null when insufficient data)
- [x] T018 Create `apps/web/src/components/difficulty-indicator.tsx` — renders 3 bars as small `div` elements with Tailwind classes: empty bars for trivial (all `bg-muted`), 1 green bar for low (`bg-green-500`), 2 yellow bars for moderate (`bg-yellow-500`), 3 red bars for high (`bg-red-500`). Add `title` attribute for tooltip (e.g., "Moderate encounter difficulty"). Accept `DifficultyResult` as prop.
- [x] T019 Add `DifficultyIndicator` to `TurnNavigation` in `apps/web/src/components/turn-navigation.tsx` — position to the right of the active combatant name inside the center flex section. Use `useDifficulty()` hook; render indicator only when result is non-null.
- [x] T019a Add tests for `useDifficulty` hook in `apps/web/src/hooks/__tests__/use-difficulty.test.ts` — verify correct `DifficultyResult` for known combatant/PC/creature combinations, returns null when data is insufficient, and updates when combatants change. Include tooltip text assertions for `DifficultyIndicator`.
**Checkpoint**: Difficulty indicator appears in top bar for encounters with leveled PCs + bestiary monsters. Updates live. All quality gates pass.
---
## Phase 4: User Story 2 — Indicator Hidden When Data Insufficient (Priority: P1)
**Goal**: Indicator is completely hidden when the encounter lacks PC combatants with levels or bestiary-linked monsters.
**Independent Test**: Create encounters with only custom combatants, only monsters (no PCs), only PCs without levels — indicator should not appear in any case.
### Implementation for User Story 2
- [x] T020 Review and verify `useDifficulty()` null-return paths implemented in T017 cover all edge cases: no combatants with `playerCharacterId` that have a level, no combatants with `creatureId`, all PCs without levels, all custom combatants, empty encounter. Fix any missing cases.
- [x] T021 Verify `TurnNavigation` in `apps/web/src/components/turn-navigation.tsx` renders nothing for the indicator when `useDifficulty()` returns null — confirm conditional rendering is correct.
**Checkpoint**: Indicator hides correctly for all insufficient-data scenarios. No visual artifacts when hidden.
---
## Phase 5: Polish & Cross-Cutting Concerns
**Purpose**: Final validation and documentation updates.
- [x] T022 Run `pnpm check` to verify all quality gates pass (audit, knip, biome, oxlint, typecheck, test/coverage, jscpd)
- [x] T023 Verify export compatibility — create a player character with level, export encounter JSON, re-import, confirm level is preserved. Verify old exports (without level) still import correctly. If the added `level` field causes old imports to fail, bump `ExportBundle` version and add migration logic in `validateImportBundle()` per CLAUDE.md convention.
- [x] T024 Update `specs/005-player-characters/spec.md` to note that `PlayerCharacter` now supports an optional `level` field (added by spec 008)
- [x] T025 Update `CLAUDE.md` to add spec 008 to the current feature specs list
- [x] T026 Update `README.md` if encounter difficulty is a user-facing feature worth documenting
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Foundational)**: No dependencies — can start immediately
- **Phase 2 (XP Calculation)**: T012-T014 depend on T001 (level type). T016 tests can be written in parallel with T012-T014.
- **Phase 3 (Indicator UI)**: Depends on Phase 1 (level in forms) and Phase 2 (calculation function)
- **Phase 4 (Visibility)**: Depends on Phase 3 (indicator exists to hide)
- **Phase 5 (Polish)**: Depends on all previous phases
### User Story Dependencies
- **ED-4 (Calculation)**: Depends on `level` field existing on `PlayerCharacter` (Phase 1, T001)
- **ED-1 (Indicator)**: Depends on ED-4 (calculation) + Phase 1 (level in UI)
- **ED-2 (Visibility)**: Depends on ED-1 (indicator rendering) — primarily a verification task
- **ED-3 (Level field)**: Implemented in Phase 1 as foundational — all stories depend on it
### Within Each Phase
- Tasks marked [P] can run in parallel
- Domain tasks before application tasks before web tasks
- Type definitions before functions using those types
- Implementation before tests (unless TDD requested)
### Parallel Opportunities
**Phase 1 parallel group**:
```
T002 (create validation) ‖ T003 (edit validation) ‖ T004 (create tests) ‖ T005 (edit tests)
T006 (create use case) ‖ T007 (edit use case)
```
**Phase 2 parallel group**:
```
T012 (tables + types) → T013 (crToXp) ‖ T014 (calculateDifficulty) → T016 (tests)
```
**Phase 3 parallel group**:
```
T017 (hook) ‖ T018 (component) → T019 (integration into top bar)
```
---
## Implementation Strategy
### MVP First (Phase 1 + Phase 2 + Phase 3)
1. Complete Phase 1: Level field on PlayerCharacter
2. Complete Phase 2: Domain difficulty calculation + tests
3. Complete Phase 3: Indicator in top bar
4. **STOP and VALIDATE**: Indicator shows correct difficulty for encounters with leveled PCs and bestiary monsters
5. Demo: add a party of level 3 PCs, add some goblins from bestiary, see bars change
### Incremental Delivery
1. Phase 1 → Level field works in PC forms → Can assign levels immediately
2. Phase 2 → Calculation is correct → Domain tests prove it
3. Phase 3 → Indicator visible → Feature is usable
4. Phase 4 → Edge cases verified → Feature is robust
5. Phase 5 → Docs updated → Feature is complete
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Story ED-3 (level field) is implemented in Phase 1 as it's foundational to all other stories
- The `useDifficulty` hook is the key integration point — it bridges three contexts into one domain call
- No new contexts or ports needed — existing patterns handle everything
- Commit after each phase checkpoint