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
DiceRollerport — 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
calculateInitiativeto 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
setInitiativeUseCasedirectly 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
InitiativeRolledevent 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
setInitiativeUseCaseN times — N persists and N sorts (wasteful) - New domain
setMultipleInitiativesfunction — unnecessary; loopingsetInitiativeon the evolving encounter state achieves the same result