Files
initiative/specs/008-encounter-difficulty/quickstart.md
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

3.2 KiB

Quickstart: Encounter Difficulty Indicator

Date: 2026-03-27 | Feature: 008-encounter-difficulty

Implementation Order

Phase 1: Domain — Level field + validation

  1. Add level?: number to PlayerCharacter in player-character-types.ts
  2. Add level validation to createPlayerCharacter() — validate if provided: integer, 1-20
  3. Add level validation to editPlayerCharacter() — same rules in validateFields(), apply in applyFields()
  4. Add tests for level validation in existing test files
  5. Export updated types from index.ts

Phase 2: Domain — Difficulty calculation

  1. Create encounter-difficulty.ts with:
    • CR_TO_XP lookup (Record<string, number>)
    • XP_BUDGET_PER_CHARACTER lookup (Record<number, { low, moderate, high }>)
    • crToXp(cr: string): number — returns 0 for unknown CRs
    • calculateEncounterDifficulty(partyLevels: number[], monsterCrs: string[]): DifficultyResult
    • DifficultyTier type and DifficultyResult type
  2. Add comprehensive unit tests covering:
    • All CR string formats (0, 1/8, 1/4, 1/2, integers)
    • All difficulty tiers including trivial
    • DMG example encounters (from issue comments)
    • Edge cases: empty arrays, unknown CRs, mixed levels
  3. Export from index.ts

Phase 3: Application — Pass level through use cases

  1. Update CreatePlayerCharacterUseCase to accept and pass level
  2. Update EditPlayerCharacterUseCase to accept and pass level

Phase 4: Web — Level field in PC forms

  1. Update player characters context to pass level in create/edit calls
  2. Add level input field to create player modal (optional number, 1-20)
  3. Add level display + edit in player character manager
  4. Test: create PC with level, edit level, verify persistence

Phase 5: Web — Difficulty indicator

  1. Create useDifficulty() hook:
    • Consume encounter context, player characters context, bestiary hook
    • Map combatants → party levels + monster CRs
    • Call domain calculateEncounterDifficulty()
    • Return DifficultyResult | null (null when insufficient data)
  2. Create DifficultyIndicator component:
    • Render 3 bars with conditional fill colors
    • Add title attribute for tooltip
    • Hidden when hook returns null
  3. Add indicator to TurnNavigation component, right of active combatant name
  4. Test: manual verification with various encounter compositions

Key Patterns to Follow

  • Domain purity: calculateEncounterDifficulty takes number[] and string[], not domain types
  • Validation pattern: Follow color/icon optional field pattern in create/edit
  • Hook composition: useDifficulty composes multiple contexts like useInitiativeRolls
  • Component size: DifficultyIndicator should be <8 props (likely 0-1, just the result)

Testing Strategy

  • Domain tests (unit): Exhaustive coverage of calculateEncounterDifficulty and crToXp with table-driven tests. Cover all 34 CR values, all 20 levels, and the DMG example encounters.
  • Domain tests (level validation): Test create/edit with valid levels, invalid levels, and undefined level.
  • Integration: Verify indicator appears/hides correctly through component rendering (if existing test patterns support this).