Files
initiative/specs/026-roll-initiative/tasks.md

8.7 KiB
Raw Blame History

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

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

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

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

  • 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

# 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 (T001T004)
  2. Complete Phase 2: User Story 1 (T005T008)
  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)