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:
Lukas
2026-03-10 16:29:09 +01:00
parent 5b0bac880d
commit d5f7b6ee36
20 changed files with 926 additions and 27 deletions

View File

@@ -0,0 +1,68 @@
# Data Model: Roll Initiative
## Existing Entities (no changes)
### Combatant
| Field | Type | Notes |
|-------|------|-------|
| id | CombatantId | Branded string identifier |
| name | string | Display name |
| initiative | number \| undefined | The initiative value (set by manual input OR roll) |
| creatureId | CreatureId \| undefined | Link to bestiary creature — presence determines roll eligibility |
| maxHp | number \| undefined | From bestiary or manual |
| currentHp | number \| undefined | Current hit points |
| ac | number \| undefined | Armor class |
| conditions | ConditionId[] | Active conditions |
| isConcentrating | boolean | Concentration flag |
### Creature (from bestiary)
| Field | Type | Notes |
|-------|------|-------|
| id | CreatureId | Branded string identifier |
| dexScore | number | Dexterity ability score (used for initiative modifier) |
| cr | string | Challenge rating (determines proficiency bonus) |
| initiativeProficiency | number | 0 = none, 1 = proficiency, 2 = expertise |
### Initiative Modifier (derived, not stored)
Calculated on demand via `calculateInitiative(creature)`:
- `modifier = Math.floor((dexScore - 10) / 2) + (initiativeProficiency × proficiencyBonus(cr))`
- `passive = 10 + modifier`
## New Concepts (no new stored entities)
### Roll Result (transient)
Not persisted — computed at the adapter boundary and immediately applied:
- `diceRoll`: integer 120 (generated via Math.random at adapter layer)
- `initiativeValue = diceRoll + modifier` (computed by domain function, stored as combatant's `initiative`)
## State Transitions
### Single Roll
```
Combatant(initiative: any | undefined)
→ rollInitiative(diceRoll, modifier)
→ Combatant(initiative: diceRoll + modifier)
→ encounter re-sorted by initiative descending
```
### Batch Roll
```
Encounter(combatants: [...])
→ for each combatant with creatureId:
→ rollInitiative(diceRoll_i, modifier_i)
→ setInitiative(encounter, id, value)
→ single save with final sorted state
```
## Validation Rules
- `diceRoll` must be an integer in range [1, 20]
- `modifier` is any integer (can be negative)
- Final `initiative` value is any integer (can be negative, e.g., 1 + (3) = 2)
- Only combatants with a non-undefined `creatureId` are eligible for rolling