Files
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

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?: number to PlayerCharacter interface in packages/domain/src/player-character-types.ts
  • 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"
  • T003 [P] Add level validation to validateFields() and apply in applyFields() in packages/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 CreatePlayerCharacterUseCase to accept and pass level in packages/application/src/create-player-character-use-case.ts
  • T007 [P] Update EditPlayerCharacterUseCase to accept and pass level in packages/application/src/edit-player-character-use-case.ts
  • T008 Update player characters context to pass level in create/edit calls in apps/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 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

  • 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 })
  • T013 Implement crToXp(cr: string): number in packages/domain/src/encounter-difficulty.ts — returns XP for given CR string, 0 for unknown CRs
  • 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
  • T015 Export DifficultyTier, DifficultyResult, crToXp, calculateEncounterDifficulty from packages/domain/src/index.ts (same file as T011 — merge into one edit)
  • 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

  • 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)
  • 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.
  • 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.
  • 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

  • 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.
  • 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.

  • T022 Run pnpm check to 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 level field causes old imports to fail, bump ExportBundle version and add migration logic in validateImportBundle() per CLAUDE.md convention.
  • T024 Update specs/005-player-characters/spec.md to note that PlayerCharacter now supports an optional level field (added by spec 008)
  • T025 Update CLAUDE.md to add spec 008 to the current feature specs list
  • 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