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>
This commit is contained in:
36
specs/008-encounter-difficulty/checklists/requirements.md
Normal file
36
specs/008-encounter-difficulty/checklists/requirements.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Specification Quality Checklist: Encounter Difficulty Indicator
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-03-27
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
|
||||
- The spec references `creatureId` and `playerCharacterId` field names — these are domain entity attributes, not implementation details.
|
||||
- Cross-dependency with spec 005 (PlayerCharacter) is documented in FR-011 and Assumptions.
|
||||
145
specs/008-encounter-difficulty/data-model.md
Normal file
145
specs/008-encounter-difficulty/data-model.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Data Model: Encounter Difficulty Indicator
|
||||
|
||||
**Date**: 2026-03-27 | **Feature**: 008-encounter-difficulty
|
||||
|
||||
## Entities
|
||||
|
||||
### PlayerCharacter (modified)
|
||||
|
||||
Existing entity from spec 005. Adding one optional field.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| id | PlayerCharacterId | yes | Existing — branded string |
|
||||
| name | string | yes | Existing |
|
||||
| ac | number | yes | Existing |
|
||||
| maxHp | number | yes | Existing |
|
||||
| color | PlayerColor | no | Existing |
|
||||
| icon | PlayerIcon | no | Existing |
|
||||
| **level** | **number** | **no** | **NEW — integer 1-20. Used for XP budget calculation. PCs without level are excluded from difficulty calc.** |
|
||||
|
||||
**Validation rules for `level`**:
|
||||
- If provided, must be an integer
|
||||
- If provided, must be >= 1 and <= 20
|
||||
- If omitted/undefined, PC is excluded from difficulty budget
|
||||
|
||||
### DifficultyTier (new)
|
||||
|
||||
Enumeration of encounter difficulty categories.
|
||||
|
||||
| Value | Display Label | Visual |
|
||||
|-------|---------------|--------|
|
||||
| `"trivial"` | Trivial | 3 empty bars |
|
||||
| `"low"` | Low | 1 green bar |
|
||||
| `"moderate"` | Moderate | 2 yellow bars |
|
||||
| `"high"` | High | 3 red bars |
|
||||
|
||||
### DifficultyResult (new)
|
||||
|
||||
Output of the difficulty calculation. Pure data object.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| tier | DifficultyTier | The determined difficulty category |
|
||||
| totalMonsterXp | number | Sum of XP for all bestiary-linked combatants |
|
||||
| partyBudget | { low: number; moderate: number; high: number } | XP thresholds for the party |
|
||||
|
||||
### XP Budget per Character (static lookup)
|
||||
|
||||
Maps character level to XP thresholds. Data from 2024 5.5e DMG.
|
||||
|
||||
| Level | Low | Moderate | High |
|
||||
|-------|-----|----------|------|
|
||||
| 1 | 50 | 75 | 100 |
|
||||
| 2 | 100 | 150 | 200 |
|
||||
| 3 | 150 | 225 | 400 |
|
||||
| 4 | 250 | 375 | 500 |
|
||||
| 5 | 500 | 750 | 1,100 |
|
||||
| 6 | 600 | 1,000 | 1,400 |
|
||||
| 7 | 750 | 1,300 | 1,700 |
|
||||
| 8 | 1,000 | 1,700 | 2,100 |
|
||||
| 9 | 1,300 | 2,000 | 2,600 |
|
||||
| 10 | 1,600 | 2,300 | 3,100 |
|
||||
| 11 | 1,900 | 2,900 | 4,100 |
|
||||
| 12 | 2,200 | 3,700 | 4,700 |
|
||||
| 13 | 2,600 | 4,200 | 5,400 |
|
||||
| 14 | 2,900 | 4,900 | 6,200 |
|
||||
| 15 | 3,300 | 5,400 | 7,800 |
|
||||
| 16 | 3,800 | 6,100 | 9,800 |
|
||||
| 17 | 4,500 | 7,200 | 11,700 |
|
||||
| 18 | 5,000 | 8,700 | 14,200 |
|
||||
| 19 | 5,500 | 10,700 | 17,200 |
|
||||
| 20 | 6,400 | 13,200 | 22,000 |
|
||||
|
||||
### CR-to-XP (static lookup)
|
||||
|
||||
Maps challenge rating strings to XP values. Standard 5e values.
|
||||
|
||||
| CR | XP |
|
||||
|----|-----|
|
||||
| 0 | 0 |
|
||||
| 1/8 | 25 |
|
||||
| 1/4 | 50 |
|
||||
| 1/2 | 100 |
|
||||
| 1 | 200 |
|
||||
| 2 | 450 |
|
||||
| 3 | 700 |
|
||||
| 4 | 1,100 |
|
||||
| 5 | 1,800 |
|
||||
| 6 | 2,300 |
|
||||
| 7 | 2,900 |
|
||||
| 8 | 3,900 |
|
||||
| 9 | 5,000 |
|
||||
| 10 | 5,900 |
|
||||
| 11 | 7,200 |
|
||||
| 12 | 8,400 |
|
||||
| 13 | 10,000 |
|
||||
| 14 | 11,500 |
|
||||
| 15 | 13,000 |
|
||||
| 16 | 15,000 |
|
||||
| 17 | 18,000 |
|
||||
| 18 | 20,000 |
|
||||
| 19 | 22,000 |
|
||||
| 20 | 25,000 |
|
||||
| 21 | 33,000 |
|
||||
| 22 | 41,000 |
|
||||
| 23 | 50,000 |
|
||||
| 24 | 62,000 |
|
||||
| 25 | 75,000 |
|
||||
| 26 | 90,000 |
|
||||
| 27 | 105,000 |
|
||||
| 28 | 120,000 |
|
||||
| 29 | 135,000 |
|
||||
| 30 | 155,000 |
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
PlayerCharacter (has optional level)
|
||||
│
|
||||
▼ linked via playerCharacterId
|
||||
Combatant (in Encounter)
|
||||
│
|
||||
▼ linked via creatureId
|
||||
Creature (has cr string)
|
||||
│
|
||||
▼ lookup via CR_TO_XP table
|
||||
XP value (number)
|
||||
|
||||
Party levels ──► XP_BUDGET_TABLE ──► { low, moderate, high } thresholds
|
||||
Monster XP total ──► compare against thresholds ──► DifficultyTier
|
||||
```
|
||||
|
||||
## State Transitions
|
||||
|
||||
The difficulty calculation is stateless — it's a pure derivation from current encounter state. No state machine or transitions to model.
|
||||
|
||||
**Input derivation** (at adapter layer):
|
||||
1. For each combatant with `playerCharacterId` → look up `PlayerCharacter.level` → collect non-undefined levels
|
||||
2. For each combatant with `creatureId` → look up `Creature.cr` → collect CR strings
|
||||
3. Pass `(levels[], crs[])` to domain function
|
||||
|
||||
**Pure calculation** (domain layer):
|
||||
1. Sum XP budget per level → `partyBudget.{low, moderate, high}`
|
||||
2. Convert each CR to XP → sum → `totalMonsterXp`
|
||||
3. Compare `totalMonsterXp` against thresholds → `DifficultyTier`
|
||||
81
specs/008-encounter-difficulty/plan.md
Normal file
81
specs/008-encounter-difficulty/plan.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Implementation Plan: Encounter Difficulty Indicator
|
||||
|
||||
**Branch**: `008-encounter-difficulty` | **Date**: 2026-03-27 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from `/specs/008-encounter-difficulty/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Add a live 3-bar encounter difficulty indicator to the top bar, based on the 2024 5.5e XP budget system. The domain layer gains pure lookup tables (CR-to-XP, XP Budget per Character) and a difficulty calculation function. The `PlayerCharacter` type gains an optional `level` field (1-20). The UI renders a compact bar indicator that derives difficulty from encounter combatants, player character levels, and bestiary creature CRs.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.8 (strict mode, `verbatimModuleSyntax`)
|
||||
**Primary Dependencies**: React 19, Vite 6, Tailwind CSS v4, Lucide React (icons)
|
||||
**Storage**: localStorage (encounter + player characters), IndexedDB (bestiary cache)
|
||||
**Testing**: Vitest (v8 coverage)
|
||||
**Target Platform**: Web (mobile-first responsive, desktop side panels)
|
||||
**Project Type**: Web application (monorepo: domain → application → web adapter)
|
||||
**Performance Goals**: Indicator updates within the same render cycle as combatant changes
|
||||
**Constraints**: Offline-capable, local-first, single-user. Max 8 props per component.
|
||||
**Scale/Scope**: Single-page app, ~15 components, 3-layer architecture
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Principle | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| I. Deterministic Domain Core | PASS | Difficulty calculation is a pure function: `(partyLevels, monsterCRs) → DifficultyResult`. No I/O, no randomness, no clocks. Lookup tables are static data. |
|
||||
| II. Layered Architecture | PASS | New domain module (`encounter-difficulty.ts`) has zero imports from application/adapter. UI component composes data from existing contexts. |
|
||||
| II-A. Context-Based State Flow | PASS | Difficulty indicator reads from existing contexts (encounter, player characters, bestiary). No new props beyond what's needed for the component itself. |
|
||||
| III. Clarification-First | PASS | All design decisions resolved in spec: optional level, 3 tiers, 5.5e rules only, no multipliers, hidden when insufficient data. |
|
||||
| IV. Escalation Gates | PASS | Feature scoped to spec. MVP exclusions documented (no custom CR, no 2014 rules, no XP numbers in UI). |
|
||||
| V. MVP Baseline Language | PASS | Exclusions use "MVP baseline does not include" language. |
|
||||
| VI. No Gameplay Rules in Constitution | PASS | XP tables and difficulty rules live in the feature spec, not the constitution. |
|
||||
|
||||
No violations. Complexity Tracking section not needed.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/008-encounter-difficulty/
|
||||
├── plan.md # This file
|
||||
├── research.md # Phase 0 output
|
||||
├── data-model.md # Phase 1 output
|
||||
├── quickstart.md # Phase 1 output
|
||||
└── tasks.md # Phase 2 output (/speckit.tasks)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
packages/domain/src/
|
||||
├── encounter-difficulty.ts # NEW: CR-to-XP, XP budget tables, difficulty calc
|
||||
├── player-character-types.ts # MODIFY: add optional level field
|
||||
├── create-player-character.ts # MODIFY: add level validation
|
||||
├── edit-player-character.ts # MODIFY: add level validation + apply
|
||||
├── __tests__/
|
||||
│ ├── encounter-difficulty.test.ts # NEW: unit tests for difficulty calc
|
||||
│ ├── create-player-character.test.ts # MODIFY: add level tests
|
||||
│ └── edit-player-character.test.ts # MODIFY: add level tests
|
||||
└── index.ts # MODIFY: export new functions/types
|
||||
|
||||
packages/application/src/
|
||||
├── create-player-character-use-case.ts # MODIFY: pass level through
|
||||
└── edit-player-character-use-case.ts # MODIFY: pass level through
|
||||
|
||||
apps/web/src/
|
||||
├── components/
|
||||
│ ├── difficulty-indicator.tsx # NEW: 3-bar indicator component
|
||||
│ ├── turn-navigation.tsx # MODIFY: add indicator to top bar
|
||||
│ ├── create-player-modal.tsx # MODIFY: add level field
|
||||
│ └── player-character-manager.tsx # MODIFY: show level, pass to edit
|
||||
├── hooks/
|
||||
│ └── use-difficulty.ts # NEW: hook composing contexts → difficulty result
|
||||
└── contexts/
|
||||
└── player-characters-context.tsx # MODIFY: pass level to create/edit
|
||||
```
|
||||
|
||||
**Structure Decision**: Follows existing layered architecture. New domain module for difficulty calculation. New UI component + hook at adapter layer. No new contexts needed — the difficulty hook composes existing contexts.
|
||||
67
specs/008-encounter-difficulty/quickstart.md
Normal file
67
specs/008-encounter-difficulty/quickstart.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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).
|
||||
87
specs/008-encounter-difficulty/research.md
Normal file
87
specs/008-encounter-difficulty/research.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Research: Encounter Difficulty Indicator
|
||||
|
||||
**Date**: 2026-03-27 | **Feature**: 008-encounter-difficulty
|
||||
|
||||
## Research Questions & Findings
|
||||
|
||||
### 1. Where does the CR-to-XP mapping come from?
|
||||
|
||||
**Decision**: Create a static lookup table in the domain layer as a `Record<string, number>`.
|
||||
|
||||
**Rationale**: No CR-to-XP mapping exists in the codebase. The bestiary index stores CR as a string but does not include XP. The standard 5e CR-to-XP table is fixed (published rules), so a static lookup is the simplest approach. The existing `proficiencyBonus(cr)` function in `creature-types.ts` demonstrates the pattern: a pure function that maps a CR string to a derived value.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Adding XP to the bestiary index: Would require rebuilding the index generation script and inflating the index size. Unnecessary since XP is deterministically derived from CR.
|
||||
- Computing XP from CR via formula: No clean formula exists for the 5e table — it's an irregular curve. A lookup table is more reliable and readable.
|
||||
|
||||
### 2. How does the difficulty component access creature CR at runtime?
|
||||
|
||||
**Decision**: The difficulty hook (`useDifficulty`) will consume `useBestiary()` to access the creature map, then look up CR for each combatant with a `creatureId`.
|
||||
|
||||
**Rationale**: The `useBestiary()` hook already provides `getCreature(id): Creature | undefined`, which returns the full Creature including `cr`. This is the established pattern — `CombatantRow` and `StatBlockPanel` already use it. No new adapter or port is needed.
|
||||
|
||||
**Data flow**:
|
||||
```
|
||||
useDifficulty hook
|
||||
├── useEncounterContext() → encounter.combatants[]
|
||||
├── usePlayerCharactersContext() → characters[] (for level lookup)
|
||||
└── useBestiary() → getCreature(creatureId) → creature.cr
|
||||
└── domain: crToXp(cr) → xp
|
||||
└── domain: calculateDifficulty({ partyLevels, monsterXp }) → DifficultyResult
|
||||
```
|
||||
|
||||
**Alternatives considered**:
|
||||
- Passing CR through the application layer port: Would add a `BestiarySourceCache` dependency to the difficulty calculation, breaking domain purity. Better to resolve CRs to XP values in the hook (adapter layer) and pass pure data to the domain function.
|
||||
|
||||
### 3. How should the domain difficulty function be structured?
|
||||
|
||||
**Decision**: A single pure function that takes resolved inputs (party levels as `number[]`, monster XP values as `number[]`) and returns a `DifficultyResult`.
|
||||
|
||||
**Rationale**: Keeping the domain function agnostic to how levels and XP are obtained preserves purity and testability. The function doesn't need to know about `PlayerCharacter`, `Combatant`, or `Creature` types — just numbers. This follows the pattern of `advanceTurn(encounter)` and `proficiencyBonus(cr)`: pure inputs → pure outputs.
|
||||
|
||||
**Function signature** (domain):
|
||||
```
|
||||
calculateEncounterDifficulty(partyLevels: number[], monsterCrs: string[]): DifficultyResult
|
||||
```
|
||||
|
||||
Takes party levels (already filtered to only PCs with levels) and monster CR strings (already filtered to only bestiary-linked combatants). Returns the tier, total XP, and budget thresholds.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Taking full `Combatant[]` + `PlayerCharacter[]`: Would couple the difficulty module to combatant/PC types unnecessarily.
|
||||
- Separate functions for budget and XP: The calculation is simple enough for one function. Internal helpers can split the logic without exposing multiple public functions.
|
||||
|
||||
### 4. How does `level` integrate with the PlayerCharacter CRUD flow?
|
||||
|
||||
**Decision**: Add `level?: number` to `PlayerCharacter` interface. Validation in `createPlayerCharacter` and `editPlayerCharacter` domain functions. Passed through use cases and context like existing fields.
|
||||
|
||||
**Rationale**: The existing pattern for optional fields (e.g., `color`, `icon`) is well-established:
|
||||
- Domain type: optional field on interface
|
||||
- Create function: validate if provided, skip if undefined
|
||||
- Edit function: validate in `validateFields()`, apply in `applyFields()`
|
||||
- Use case: pass through from adapter
|
||||
- Context: expose in create/edit methods
|
||||
- UI: optional input field in modal
|
||||
|
||||
Following this exact pattern for `level` minimizes risk and code churn.
|
||||
|
||||
### 5. Does adding `level` to PlayerCharacter affect export compatibility?
|
||||
|
||||
**Decision**: No version bump needed. The field is optional, so existing exports (version 1) import correctly — `level` will be `undefined` for old data.
|
||||
|
||||
**Rationale**: The `ExportBundle` includes `playerCharacters: readonly PlayerCharacter[]`. Adding an optional field to `PlayerCharacter` is backward-compatible: old exports simply lack the field, which TypeScript treats as `undefined`. The `validateImportBundle()` function doesn't validate individual PlayerCharacter fields beyond basic structure checks.
|
||||
|
||||
### 6. Should the difficulty calculation live in a new domain module or extend an existing one?
|
||||
|
||||
**Decision**: New module `encounter-difficulty.ts` in `packages/domain/src/`.
|
||||
|
||||
**Rationale**: The difficulty calculation is a self-contained concern with its own lookup tables and types. It doesn't naturally belong in `creature-types.ts` (which is about creature data structures) or `types.ts` (which is about encounter/combatant structure). A dedicated module keeps concerns separated and makes the feature easy to find, test, and potentially remove.
|
||||
|
||||
### 7. How should the 3-bar indicator be rendered?
|
||||
|
||||
**Decision**: Simple `div` elements with Tailwind CSS classes for color and fill state. No external icon or SVG needed.
|
||||
|
||||
**Rationale**: The bars are simple rectangles with conditional fill colors (green/yellow/red). Tailwind's `bg-*` utilities handle this trivially. The existing codebase uses Tailwind for all styling with no CSS-in-JS or external style libraries. A native HTML tooltip (`title` attribute) handles the hover tooltip requirement.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Lucide icons: No suitable "signal bars" icon exists. Custom SVG would be overkill for 3 rectangles.
|
||||
- CSS custom properties for colors: Unnecessary abstraction for 3 fixed states.
|
||||
205
specs/008-encounter-difficulty/spec.md
Normal file
205
specs/008-encounter-difficulty/spec.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# 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)"
|
||||
|
||||
## 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**:
|
||||
|
||||
1. **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).
|
||||
|
||||
2. **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".
|
||||
|
||||
3. **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".
|
||||
|
||||
4. **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".
|
||||
|
||||
5. **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).
|
||||
|
||||
6. **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.
|
||||
|
||||
7. **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**:
|
||||
|
||||
1. **Given** an encounter with only custom combatants (no `creatureId`), **When** the top bar renders, **Then** no difficulty indicator is shown.
|
||||
|
||||
2. **Given** an encounter with bestiary-linked monsters but no PC combatants, **When** the top bar renders, **Then** no difficulty indicator is shown.
|
||||
|
||||
3. **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).
|
||||
|
||||
4. **Given** an encounter with one leveled PC combatant and one bestiary-linked monster, **When** the last leveled PC is removed, **Then** the indicator disappears.
|
||||
|
||||
5. **Given** an encounter with one leveled PC combatant and one bestiary-linked monster, **When** the last bestiary-linked monster is removed (only custom combatants remain), **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**:
|
||||
|
||||
1. **Given** the create player character form is open, **When** the user sets level to 5, **Then** the player character is created with level 5.
|
||||
|
||||
2. **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.
|
||||
|
||||
3. **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.
|
||||
|
||||
4. **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.
|
||||
|
||||
5. **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**:
|
||||
|
||||
1. **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).
|
||||
|
||||
2. **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.
|
||||
|
||||
3. **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).
|
||||
|
||||
4. **Given** an encounter with both bestiary-linked and custom combatants, **When** the XP total is calculated, **Then** only bestiary-linked combatants contribute XP (custom combatants are excluded).
|
||||
|
||||
5. **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).
|
||||
|
||||
---
|
||||
|
||||
### 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 `creatureId` have 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 silently excluded**: Custom combatants without `creatureId` do not appear in the XP total and are not flagged as warnings or errors.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## 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 — Monster XP total calculation
|
||||
The system MUST calculate the total monster XP by summing the XP value (derived from CR) for each combatant that has a `creatureId`. Combatants without `creatureId` are excluded.
|
||||
|
||||
#### 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 bestiary-linked combatants.
|
||||
|
||||
#### 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.
|
||||
|
||||
### 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 `PlayerCharacter` entity defined in spec 005.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## 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 `level` field is added to the existing `PlayerCharacter` type 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.
|
||||
- MVP baseline does not include CR assignment for custom (non-bestiary) combatants.
|
||||
- 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 showing XP totals or budget numbers in the indicator — only the visual bars and tooltip label.
|
||||
- MVP baseline does not include per-combatant level overrides — level is always derived from the player character template.
|
||||
171
specs/008-encounter-difficulty/tasks.md
Normal file
171
specs/008-encounter-difficulty/tasks.md
Normal 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
|
||||
Reference in New Issue
Block a user