131 lines
8.7 KiB
Markdown
131 lines
8.7 KiB
Markdown
# Tasks: Roll Initiative
|
||
|
||
**Input**: Design documents from `/specs/026-roll-initiative/`
|
||
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, quickstart.md
|
||
|
||
**Tests**: Tests are included for the domain layer (pure functions are trivially testable and the project convention includes domain tests).
|
||
|
||
**Organization**: Tasks are grouped by user story 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., US1, US2)
|
||
- Include exact file paths in descriptions
|
||
|
||
## Phase 1: Setup (Shared Infrastructure)
|
||
|
||
**Purpose**: Create the d20 icon component and domain roll function shared by both user stories
|
||
|
||
- [x] T001 [P] Create D20Icon React component in `apps/web/src/components/d20-icon.tsx` — inline SVG component (copy path data from `d20.svg` in project root) accepting className prop, using `stroke="currentColor"` and `fill="none"`. Follow Lucide icon conventions (size via className). The project root `d20.svg` remains as the source asset and is not moved or deleted.
|
||
- [x] T002 [P] Create `rollInitiative` pure domain function in `packages/domain/src/roll-initiative.ts` — accepts `(diceRoll: number, modifier: number)`, validates diceRoll is integer in [1, 20], returns `diceRoll + modifier`. Return `DomainError` for invalid dice roll values.
|
||
- [x] T003 [P] Create domain tests in `packages/domain/src/__tests__/roll-initiative.test.ts` — test normal rolls (e.g., roll 15 + modifier 7 = 22), boundary values (roll 1, roll 20), negative modifiers (roll 1 + (−3) = −2), zero modifier, and invalid dice roll values (0, 21, non-integer). Follow existing test patterns from `set-initiative.test.ts`.
|
||
- [x] T004 Export `rollInitiative` from `packages/domain/src/index.ts` (add to existing barrel export)
|
||
|
||
**Checkpoint**: D20 icon component exists, domain roll function passes all tests
|
||
|
||
---
|
||
|
||
## Phase 2: User Story 1 — Roll Initiative for a Single Combatant (Priority: P1) 🎯 MVP
|
||
|
||
**Goal**: A d20 button next to each bestiary combatant's initiative field rolls 1d20 + modifier and sets the result.
|
||
|
||
**Independent Test**: Add a bestiary creature to the encounter, click its d20 button, verify initiative value appears and list re-sorts.
|
||
|
||
### Implementation for User Story 1
|
||
|
||
- [x] T005 [US1] Create `rollInitiativeUseCase` in `packages/application/src/roll-initiative-use-case.ts` — signature: `(store: EncounterStore, combatantId: CombatantId, diceRoll: number, getCreature: (id: CreatureId) => Creature | undefined)`. Looks up combatant's `creatureId`, calls `getCreature` to get creature data, computes modifier via `calculateInitiative`, computes final value via `rollInitiative`, then calls domain `setInitiative` to apply and persist. Returns `DomainEvent[] | DomainError`. Export from `packages/application/src/index.ts`.
|
||
- [x] T006 [US1] Add `rollInitiative` callback in `apps/web/src/App.tsx` — new function `rollInitiative(id: CombatantId)` that generates `Math.floor(Math.random() * 20) + 1`, calls `rollInitiativeUseCase` with the store, combatant ID, dice roll, and `getCreature` from `useBestiary`. Defined in App.tsx where both `useEncounter` and `useBestiary` are composed.
|
||
- [x] T007 [US1] Add `onRollInitiative` prop to `CombatantRow` in `apps/web/src/components/combatant-row.tsx` — new optional prop `onRollInitiative?: (id: CombatantId) => void`. When defined (combatant has `creatureId`), render a d20 icon button adjacent to the initiative input field (left of the input, within the same grid cell or an expanded cell). Use the `D20Icon` component. Button should be small (matching initiative input height), with hover/active states consistent with existing icon buttons.
|
||
- [x] T008 [US1] Wire `rollInitiative` callback in `apps/web/src/App.tsx` — pass `onRollInitiative` to each `CombatantRow`. Only provide the callback for combatants that have a `creatureId` (i.e., pass `onRollInitiative={c.creatureId ? rollInitiative : undefined}`). The callback is already defined in App.tsx from T006.
|
||
|
||
**Checkpoint**: Bestiary combatants show d20 button, clicking it rolls initiative and re-sorts. Manual combatants have no d20 button.
|
||
|
||
---
|
||
|
||
## Phase 3: User Story 2 — Roll All Initiative (Priority: P2)
|
||
|
||
**Goal**: A button in the turn navigation bar batch-rolls initiative for all bestiary combatants in one click.
|
||
|
||
**Independent Test**: Add mix of bestiary and manual combatants, click Roll All, verify only bestiary combatants get initiative values.
|
||
|
||
### Implementation for User Story 2
|
||
|
||
- [x] T009 [US2] Create `rollAllInitiativeUseCase` in `packages/application/src/roll-all-initiative-use-case.ts` — signature: `(store: EncounterStore, rollDice: () => number, getCreature: (id: CreatureId) => Creature | undefined)`. Reads encounter once from store, iterates combatants with `creatureId`, for each: calls `rollDice()` to get a d20 value, computes modifier via `calculateInitiative`, computes final value via domain `rollInitiative`, then applies via domain `setInitiative` (pure function, not the use case) to evolve the encounter state. After all rolls are applied, calls `store.save(encounter)` once. Collects and returns all `DomainEvent[]` or first `DomainError`. Export from `packages/application/src/index.ts`.
|
||
- [x] T010 [US2] Add `rollAllInitiative` callback in `apps/web/src/App.tsx` — new function `rollAllInitiative()` that calls `rollAllInitiativeUseCase` with `() => Math.floor(Math.random() * 20) + 1` as the dice roller and `getCreature` from `useBestiary`. Defined in App.tsx alongside the single-roll callback.
|
||
- [x] T011 [US2] Add Roll All button to `TurnNavigation` in `apps/web/src/components/turn-navigation.tsx` — new prop `onRollAllInitiative: () => void`. Render a d20 icon button in the right section (alongside existing clear/trash button). Use `D20Icon` component. Include a tooltip or aria-label "Roll all initiative".
|
||
- [x] T012 [US2] Wire `rollAllInitiative` callback in `apps/web/src/App.tsx` — pass `onRollAllInitiative` to `TurnNavigation` component.
|
||
|
||
**Checkpoint**: Roll All button in top bar rolls initiative for all bestiary combatants; manual combatants untouched.
|
||
|
||
---
|
||
|
||
## Phase 4: Polish & Cross-Cutting Concerns
|
||
|
||
**Purpose**: Final validation and cleanup
|
||
|
||
- [x] T013 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
|
||
- [ ] T014 (removed — d20.svg stays in project root as source asset; D20Icon inlines the SVG paths)
|
||
|
||
---
|
||
|
||
## Dependencies & Execution Order
|
||
|
||
### Phase Dependencies
|
||
|
||
- **Setup (Phase 1)**: No dependencies — can start immediately
|
||
- **US1 (Phase 2)**: Depends on T002 (domain function) and T001 (icon component)
|
||
- **US2 (Phase 3)**: Depends on T002 (domain function) and T001 (icon component). Can run in parallel with US1 since they touch different use case files, but US2's hook callback (T010) depends on the same hook file as T006, so they should be sequenced.
|
||
- **Polish (Phase 4)**: Depends on all previous phases
|
||
|
||
### User Story Dependencies
|
||
|
||
- **User Story 1 (P1)**: Can start after Phase 1 — no dependencies on US2
|
||
- **User Story 2 (P2)**: Can start after Phase 1 — shares hook file with US1, so best done sequentially after US1
|
||
|
||
### Parallel Opportunities
|
||
|
||
- T001, T002, T003 can all run in parallel (different files, no dependencies)
|
||
- T005 can start as soon as T002 completes (different layer)
|
||
- T007 and T011 touch different component files and can run in parallel
|
||
- T009 can start as soon as T002 completes (different layer from T005)
|
||
|
||
---
|
||
|
||
## Parallel Example: Phase 1
|
||
|
||
```bash
|
||
# Launch all setup tasks together (3 different files):
|
||
Task T001: "Create D20Icon component in apps/web/src/components/d20-icon.tsx"
|
||
Task T002: "Create rollInitiative domain function in packages/domain/src/roll-initiative.ts"
|
||
Task T003: "Create domain tests in packages/domain/src/__tests__/roll-initiative.test.ts"
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Strategy
|
||
|
||
### MVP First (User Story 1 Only)
|
||
|
||
1. Complete Phase 1: Setup (T001–T004)
|
||
2. Complete Phase 2: User Story 1 (T005–T008)
|
||
3. **STOP and VALIDATE**: Click d20 button on a bestiary combatant, verify initiative rolls and sorts
|
||
4. Deploy/demo if ready
|
||
|
||
### Incremental Delivery
|
||
|
||
1. Phase 1 → Shared components ready
|
||
2. Add US1 (Phase 2) → Single combatant rolling works → Demo
|
||
3. Add US2 (Phase 3) → Batch rolling works → Demo
|
||
4. Phase 4 → Polish and final checks
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
- [P] tasks = different files, no dependencies
|
||
- [Story] label maps task to specific user story for traceability
|
||
- Randomness (Math.random) stays in the adapter layer — domain receives resolved dice values
|
||
- Reuses existing `setInitiative` domain function and `InitiativeSet` event — no new event types
|
||
- Batch roll reads encounter once, applies all rolls, saves once (efficient single-persist strategy)
|