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>
11 KiB
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.
- T001 Add
level?: numbertoPlayerCharacterinterface inpackages/domain/src/player-character-types.ts - T002 [P] Add level validation to
createPlayerCharacter()inpackages/domain/src/create-player-character.ts— validate if provided: integer, 1-20, error code"invalid-level" - T003 [P] Add level validation to
validateFields()and apply inapplyFields()inpackages/domain/src/edit-player-character.ts - T004 [P] Add level tests to
packages/domain/src/__tests__/create-player-character.test.ts— valid level, no level, out-of-range, non-integer - T005 [P] Add level tests to
packages/domain/src/__tests__/edit-player-character.test.ts— set level, clear level, invalid level - T006 Update
CreatePlayerCharacterUseCaseto accept and passlevelinpackages/application/src/create-player-character-use-case.ts - T007 [P] Update
EditPlayerCharacterUseCaseto accept and passlevelinpackages/application/src/edit-player-character-use-case.ts - T008 Update player characters context to pass
levelin create/edit calls inapps/web/src/contexts/player-characters-context.tsx - T009 Add level input field to create player modal in
apps/web/src/components/create-player-modal.tsx— optional number input, 1-20 range - T010 Add level display and edit support in player character manager in
apps/web/src/components/player-character-manager.tsx - T011 Export updated
PlayerCharactertype frompackages/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
- T012 Create
packages/domain/src/encounter-difficulty.tswithDifficultyTiertype ("trivial" | "low" | "moderate" | "high"),DifficultyResultinterface,CR_TO_XPlookup table (Record mapping all CRs 0 through 30 including fractions to XP values), andXP_BUDGET_PER_CHARACTERlookup table (Record mapping levels 1-20 to{ low, moderate, high }) - T013 Implement
crToXp(cr: string): numberinpackages/domain/src/encounter-difficulty.ts— returns XP for given CR string, 0 for unknown CRs - T014 Implement
calculateEncounterDifficulty(partyLevels: number[], monsterCrs: string[]): DifficultyResultinpackages/domain/src/encounter-difficulty.ts— sums party budget per level, sums monster XP per CR, determines tier by comparing total XP against thresholds - T015 Export
DifficultyTier,DifficultyResult,crToXp,calculateEncounterDifficultyfrompackages/domain/src/index.ts(same file as T011 — merge into one edit) - T016 Create
packages/domain/src/__tests__/encounter-difficulty.test.tswith 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
- T017 Create
apps/web/src/hooks/use-difficulty.ts— hook that consumesuseEncounterContext(),usePlayerCharactersContext(), anduseBestiary()to derive party levels and monster CRs from current encounter, callscalculateEncounterDifficulty(), returnsDifficultyResult | null(null when insufficient data) - T018 Create
apps/web/src/components/difficulty-indicator.tsx— renders 3 bars as smalldivelements with Tailwind classes: empty bars for trivial (allbg-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). Addtitleattribute for tooltip (e.g., "Moderate encounter difficulty"). AcceptDifficultyResultas prop. - T019 Add
DifficultyIndicatortoTurnNavigationinapps/web/src/components/turn-navigation.tsx— position to the right of the active combatant name inside the center flex section. UseuseDifficulty()hook; render indicator only when result is non-null. - T019a Add tests for
useDifficultyhook inapps/web/src/hooks/__tests__/use-difficulty.test.ts— verify correctDifficultyResultfor known combatant/PC/creature combinations, returns null when data is insufficient, and updates when combatants change. Include tooltip text assertions forDifficultyIndicator.
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
- T020 Review and verify
useDifficulty()null-return paths implemented in T017 cover all edge cases: no combatants withplayerCharacterIdthat have a level, no combatants withcreatureId, all PCs without levels, all custom combatants, empty encounter. Fix any missing cases. - T021 Verify
TurnNavigationinapps/web/src/components/turn-navigation.tsxrenders nothing for the indicator whenuseDifficulty()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.
- T022 Run
pnpm checkto verify all quality gates pass (audit, knip, biome, oxlint, typecheck, test/coverage, jscpd) - 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
levelfield causes old imports to fail, bumpExportBundleversion and add migration logic invalidateImportBundle()per CLAUDE.md convention. - T024 Update
specs/005-player-characters/spec.mdto note thatPlayerCharacternow supports an optionallevelfield (added by spec 008) - T025 Update
CLAUDE.mdto add spec 008 to the current feature specs list - T026 Update
README.mdif 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
levelfield existing onPlayerCharacter(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)
- Complete Phase 1: Level field on PlayerCharacter
- Complete Phase 2: Domain difficulty calculation + tests
- Complete Phase 3: Indicator in top bar
- STOP and VALIDATE: Indicator shows correct difficulty for encounters with leveled PCs and bestiary monsters
- Demo: add a party of level 3 PCs, add some goblins from bestiary, see bars change
Incremental Delivery
- Phase 1 → Level field works in PC forms → Can assign levels immediately
- Phase 2 → Calculation is correct → Domain tests prove it
- Phase 3 → Indicator visible → Feature is usable
- Phase 4 → Edge cases verified → Feature is robust
- 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
useDifficultyhook 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