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:
71
specs/026-roll-initiative/research.md
Normal file
71
specs/026-roll-initiative/research.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Research: Roll Initiative
|
||||
|
||||
## Decision 1: Where to generate random dice rolls
|
||||
|
||||
**Decision**: Generate random numbers at the adapter (React/browser) layer and pass resolved values into application use cases.
|
||||
|
||||
**Rationale**: Constitution Principle I (Deterministic Domain Core) requires all domain logic to be pure. Random values must be injected at the boundary. The adapter layer calls `Math.floor(Math.random() * 20) + 1` and passes the result as a parameter.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Generate in domain layer — violates constitution
|
||||
- Generate in application layer — still impure; application should only orchestrate
|
||||
- Inject a `DiceRoller` port — over-engineered for Math.random(); would need mocking in tests for no real benefit
|
||||
|
||||
## Decision 2: New domain function vs. inline calculation
|
||||
|
||||
**Decision**: Create a minimal `rollInitiative` pure function in the domain that computes `diceRoll + modifier` and returns the final initiative value. The modifier is obtained from the existing `calculateInitiative` function.
|
||||
|
||||
**Rationale**: Keeps the formula explicit and testable. Even though it's simple arithmetic, having it as a named function documents the intent and makes tests self-describing.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Inline the addition in the use case — less testable, mixes concerns
|
||||
- Extend `calculateInitiative` to accept a dice roll — conflates two different calculations (passive vs. rolled)
|
||||
|
||||
## Decision 3: Reuse existing `setInitiativeUseCase` vs. new use case
|
||||
|
||||
**Decision**: Create new use cases (`rollInitiativeUseCase` and `rollAllInitiativeUseCase`) that internally call `setInitiativeUseCase` (or the domain `setInitiative` directly) after computing the rolled value.
|
||||
|
||||
**Rationale**: The roll use cases add the dice-roll-to-modifier step, then delegate to the existing set-initiative pipeline for persistence and sorting. This avoids duplicating sort/persist logic.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Call `setInitiativeUseCase` directly from the React hook — would leak initiative modifier calculation into the adapter layer
|
||||
|
||||
## Decision 4: Event type for rolled initiative
|
||||
|
||||
**Decision**: Reuse the existing `InitiativeSet` event. No new event type needed.
|
||||
|
||||
**Rationale**: From the domain's perspective, rolling initiative is indistinguishable from manually setting it — the combatant's initiative field is updated to a new value. The UI doesn't need to distinguish "rolled" from "typed" initiative values for any current feature.
|
||||
|
||||
**Alternatives considered**:
|
||||
- New `InitiativeRolled` event with dice details — adds complexity with no current consumer; can be added later if roll history/animation is needed
|
||||
|
||||
## Decision 5: d20.svg integration approach
|
||||
|
||||
**Decision**: Move `d20.svg` into `apps/web/src/assets/` and create a thin React component (`D20Icon`) that renders it as an inline SVG, inheriting `currentColor` for theming.
|
||||
|
||||
**Rationale**: The SVG already uses `stroke="currentColor"` and `fill="none"`, making it theme-compatible. An inline SVG component allows sizing via className props, consistent with how Lucide React icons work in the project.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Use as `<img>` tag — loses currentColor theming
|
||||
- Import as Vite asset URL — same limitation as img tag
|
||||
- Keep in project root and reference — clutters root, not idiomatic for web assets
|
||||
|
||||
## Decision 6: Roll-all button placement
|
||||
|
||||
**Decision**: Add the "Roll All Initiative" button to the turn navigation bar (top bar), in the right section alongside the existing clear/trash button.
|
||||
|
||||
**Rationale**: The turn navigation bar is the encounter-level action area. Rolling all initiative is an encounter-level action, not per-combatant. Placing it here follows the existing pattern (clear encounter button is already there).
|
||||
|
||||
**Alternatives considered**:
|
||||
- Separate toolbar row — adds visual clutter for a single button
|
||||
- Floating action button — inconsistent with existing UI patterns
|
||||
|
||||
## Decision 7: Batch roll — multiple setInitiative calls vs. single bulk operation
|
||||
|
||||
**Decision**: The batch roll use case iterates over eligible combatants and calls `setInitiative` for each one sequentially within a single store transaction (get once, apply all, save once).
|
||||
|
||||
**Rationale**: Calling `setInitiativeUseCase` per combatant would cause N store reads/writes and N re-sorts. Instead, the batch use case reads the encounter once, applies all initiative values via the domain `setInitiative` function in a loop (each call returns a new encounter), and saves once at the end. This is both more efficient and produces correct final sort order.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Call `setInitiativeUseCase` N times — N persists and N sorts (wasteful)
|
||||
- New domain `setMultipleInitiatives` function — unnecessary; looping `setInitiative` on the evolving encounter state achieves the same result
|
||||
Reference in New Issue
Block a user