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:
58
specs/012-turn-navigation/data-model.md
Normal file
58
specs/012-turn-navigation/data-model.md
Normal 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
|
||||
Reference in New Issue
Block a user