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

4.7 KiB

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