Implement the 026-roll-initiative feature that adds d20 roll buttons for bestiary combatants' initiative using a click-to-edit pattern (d20 icon when empty, plain text when set), plus a Roll All button in the top bar that batch-rolls for all unrolled bestiary combatants, with randomness confined to the adapter layer and the domain receiving pre-resolved dice values
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
specs/026-roll-initiative/quickstart.md
Normal file
54
specs/026-roll-initiative/quickstart.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Quickstart: Roll Initiative
|
||||
|
||||
## Overview
|
||||
|
||||
This feature adds dice-rolling to the encounter tracker. The initiative column uses a click-to-edit pattern: bestiary combatants without initiative show a d20 icon that rolls 1d20 + modifier on click; once set, the value displays as plain text (click to edit/clear). Manual combatants show "--" (click to type). A "Roll All" button in the top bar batch-rolls for all bestiary combatants that don't yet have initiative.
|
||||
|
||||
## Key Files to Touch
|
||||
|
||||
### Domain Layer (`packages/domain/src/`)
|
||||
|
||||
1. **`roll-initiative.ts`** (NEW) — Pure function: `rollInitiative(diceRoll: number, modifier: number) → number`. Validates dice roll range [1,20], returns `diceRoll + modifier`.
|
||||
|
||||
2. **`__tests__/roll-initiative.test.ts`** (NEW) — Tests for the pure function covering normal, boundary (1, 20), and negative modifier cases.
|
||||
|
||||
### Application Layer (`packages/application/src/`)
|
||||
|
||||
3. **`roll-initiative-use-case.ts`** (NEW) — Single combatant roll. Receives `(store, combatantId, diceRoll, getCreature)`. Looks up creature via `creatureId`, computes modifier via `calculateInitiative`, computes final value via `rollInitiative`, delegates to `setInitiative` domain function.
|
||||
|
||||
4. **`roll-all-initiative-use-case.ts`** (NEW) — Batch roll. Receives `(store, diceRolls: Map<CombatantId, number>, getCreature)`. Iterates eligible combatants, applies `setInitiative` in sequence on evolving encounter state, saves once.
|
||||
|
||||
### Web Adapter (`apps/web/src/`)
|
||||
|
||||
5. **`components/d20-icon.tsx`** (NEW) — React component rendering the d20.svg inline. Accepts className for sizing.
|
||||
|
||||
6. **`components/combatant-row.tsx`** (MODIFY) — Add d20 button next to initiative input. Only shown when `combatant.creatureId` is defined. New prop: `onRollInitiative: (id: CombatantId) => void`.
|
||||
|
||||
7. **`components/turn-navigation.tsx`** (MODIFY) — Add "Roll All Initiative" d20 button in right section. New prop: `onRollAllInitiative: () => void`.
|
||||
|
||||
8. **`hooks/use-encounter.ts`** (MODIFY) — Add `rollInitiative(id)` and `rollAllInitiative()` callbacks that generate dice rolls via `Math.floor(Math.random() * 20) + 1` and call the respective use cases.
|
||||
|
||||
9. **`App.tsx`** (MODIFY) — Wire new callbacks to components.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
```
|
||||
[Browser Math.random()] → diceRoll (number 1-20)
|
||||
↓
|
||||
[Application Use Case] → looks up creature, computes modifier, calls domain
|
||||
↓
|
||||
[Domain rollInitiative()] → diceRoll + modifier = initiative value
|
||||
↓
|
||||
[Domain setInitiative()] → updates combatant, re-sorts encounter
|
||||
↓
|
||||
[Store.save()] → persists to localStorage
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
pnpm --filter web dev # Dev server at localhost:5173
|
||||
pnpm test # Run all tests
|
||||
pnpm vitest run packages/domain/src/__tests__/roll-initiative.test.ts # Single test
|
||||
pnpm check # Full merge gate
|
||||
```
|
||||
Reference in New Issue
Block a user