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

72 lines
4.7 KiB
Markdown

# 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