Implement the 012-turn-navigation feature that adds a RetreatTurn domain operation and relocates turn controls to a navigation bar at the top of the encounter tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 23:11:11 +01:00
parent a0d85a07e3
commit 7d440677be
19 changed files with 946 additions and 13 deletions

View File

@@ -0,0 +1,58 @@
# Data Model: Turn Navigation
## Existing Entities (unchanged)
### Encounter
- `combatants`: readonly array of Combatant
- `activeIndex`: number (0-based index into combatants)
- `roundNumber`: positive integer (>= 1)
### Combatant
- `id`: CombatantId (branded string)
- `name`: string
- `initiative?`: number
- `maxHp?`: number
- `currentHp?`: number
## New Domain Events
### TurnRetreated
Emitted on every successful RetreatTurn operation.
| Field | Type | Description |
|-------|------|-------------|
| type | `"TurnRetreated"` (literal) | Discriminant for event union |
| previousCombatantId | CombatantId | The combatant whose turn was active before retreat |
| newCombatantId | CombatantId | The combatant who is now active after retreat |
| roundNumber | number | The round number after the retreat |
### RoundRetreated
Emitted when RetreatTurn crosses a round boundary (activeIndex wraps from 0 to last combatant).
| Field | Type | Description |
|-------|------|-------------|
| type | `"RoundRetreated"` (literal) | Discriminant for event union |
| newRoundNumber | number | The round number after decrementing |
## State Transitions
### RetreatTurn
**Input**: Encounter
**Output**: `{ encounter: Encounter, events: DomainEvent[] }` | `DomainError`
**Rules**:
1. If `combatants.length === 0` -> DomainError("invalid-encounter")
2. If `roundNumber === 1 && activeIndex === 0` -> DomainError("no-previous-turn")
3. If `activeIndex > 0`: newIndex = activeIndex - 1, roundNumber unchanged
4. If `activeIndex === 0`: newIndex = combatants.length - 1, roundNumber - 1
**Events emitted**:
- Always: TurnRetreated
- On round boundary crossing (rule 4): TurnRetreated then RoundRetreated (order matters)
## Validation Rules
- RetreatTurn MUST NOT produce roundNumber < 1
- RetreatTurn MUST NOT produce activeIndex < 0 or >= combatants.length
- RetreatTurn is a pure function: identical input produces identical output