8.7 KiB
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 fromd20.svgin project root) accepting className prop, usingstroke="currentColor"andfill="none". Follow Lucide icon conventions (size via className). The project rootd20.svgremains as the source asset and is not moved or deleted. - T002 [P] Create
rollInitiativepure domain function inpackages/domain/src/roll-initiative.ts— accepts(diceRoll: number, modifier: number), validates diceRoll is integer in [1, 20], returnsdiceRoll + modifier. ReturnDomainErrorfor 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 fromset-initiative.test.ts. - T004 Export
rollInitiativefrompackages/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
rollInitiativeUseCaseinpackages/application/src/roll-initiative-use-case.ts— signature:(store: EncounterStore, combatantId: CombatantId, diceRoll: number, getCreature: (id: CreatureId) => Creature | undefined). Looks up combatant'screatureId, callsgetCreatureto get creature data, computes modifier viacalculateInitiative, computes final value viarollInitiative, then calls domainsetInitiativeto apply and persist. ReturnsDomainEvent[] | DomainError. Export frompackages/application/src/index.ts. - T006 [US1] Add
rollInitiativecallback inapps/web/src/App.tsx— new functionrollInitiative(id: CombatantId)that generatesMath.floor(Math.random() * 20) + 1, callsrollInitiativeUseCasewith the store, combatant ID, dice roll, andgetCreaturefromuseBestiary. Defined in App.tsx where bothuseEncounteranduseBestiaryare composed. - T007 [US1] Add
onRollInitiativeprop toCombatantRowinapps/web/src/components/combatant-row.tsx— new optional proponRollInitiative?: (id: CombatantId) => void. When defined (combatant hascreatureId), 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 theD20Iconcomponent. Button should be small (matching initiative input height), with hover/active states consistent with existing icon buttons. - T008 [US1] Wire
rollInitiativecallback inapps/web/src/App.tsx— passonRollInitiativeto eachCombatantRow. Only provide the callback for combatants that have acreatureId(i.e., passonRollInitiative={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
rollAllInitiativeUseCaseinpackages/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 withcreatureId, for each: callsrollDice()to get a d20 value, computes modifier viacalculateInitiative, computes final value via domainrollInitiative, then applies via domainsetInitiative(pure function, not the use case) to evolve the encounter state. After all rolls are applied, callsstore.save(encounter)once. Collects and returns allDomainEvent[]or firstDomainError. Export frompackages/application/src/index.ts. - T010 [US2] Add
rollAllInitiativecallback inapps/web/src/App.tsx— new functionrollAllInitiative()that callsrollAllInitiativeUseCasewith() => Math.floor(Math.random() * 20) + 1as the dice roller andgetCreaturefromuseBestiary. Defined in App.tsx alongside the single-roll callback. - T011 [US2] Add Roll All button to
TurnNavigationinapps/web/src/components/turn-navigation.tsx— new proponRollAllInitiative: () => void. Render a d20 icon button in the right section (alongside existing clear/trash button). UseD20Iconcomponent. Include a tooltip or aria-label "Roll all initiative". - T012 [US2] Wire
rollAllInitiativecallback inapps/web/src/App.tsx— passonRollAllInitiativetoTurnNavigationcomponent.
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)
- Complete Phase 1: Setup (T001–T004)
- Complete Phase 2: User Story 1 (T005–T008)
- STOP and VALIDATE: Click d20 button on a bestiary combatant, verify initiative rolls and sorts
- Deploy/demo if ready
Incremental Delivery
- Phase 1 → Shared components ready
- Add US1 (Phase 2) → Single combatant rolling works → Demo
- Add US2 (Phase 3) → Batch rolling works → Demo
- 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
setInitiativedomain function andInitiativeSetevent — no new event types - Batch roll reads encounter once, applies all rolls, saves once (efficient single-persist strategy)