Implement D&D 5e death-save state machine and cleanup combat ordering
- Add status-driven death-save model: - status: conscious | dying | stable | dead - deathSaveSuccesses/deathSaveFailures as display counters - remove old death-save fields as source of truth - Add death-save actions: - success, fail, nat1, nat20 - stabilize participant - revive dead participant to 0 HP, stable, unconscious - Apply 5e HP transition rules: - characters and NPCs at 0 HP become dying - stable/dying participants gain unconscious condition - healing clears death saves and returns conscious - massive damage kills - non-NPC monsters at 0 HP become dead and inactive - Split NPCs from monsters with type="npc": - NPCs use death saves like characters - NPCs remain DM-controlled/monster-colored in UI - new NPCs no longer persist isNpc flag - Keep dead PCs/NPCs in encounter and initiative until DM removes or deactivates - Allow DM active/inactive toggle for dead participants - Hide inactive participants from player display - Improve DM and player death-state UI: - show Dying/Dead/Unconscious states consistently - add revive button for dead participants - distinguish dead visual from inactive visual in DM view - Fix initiative drag/reorder behavior: - downward drag now changes order instead of no-op - paused combat can reorder across current turn pointer - turnOrderIds stay synced to participants order - Persist campaign collapse state in localStorage - Update death-save docs and encounter builder docs - Add/expand tests for: - death saves and HP transitions - dead/active/inactive behavior - NPC death-save behavior - player display visibility - drag reorder semantics - logging expectations Tests: - npm run test:all - app: 94 passed - shared: 158 passed, 5 skipped - server: 32 passed
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# Log System Redesign Plan
|
||||
|
||||
**Status:** DRAFT — not started
|
||||
**Branch:** `feat-turn-writes-logs`
|
||||
**Goal:** Lean mutation deltas. Kill bloated payload/undo duplication.
|
||||
|
||||
## Problem
|
||||
|
||||
Current events = 10-16KB each. `add_participant` row = 16KB to say "Fighter added, init 22." Root cause: full participant roster stored 3x (payload + undo_payload.updates + undo_payload.redo).
|
||||
|
||||
DB: 2486 docs, most bloated. Replay trace 1.6-5.5MB for ~250 steps.
|
||||
|
||||
Old log (pre-refactor) = message string + small undo object. Proven worked for undo/redo + viewer. Just wanted MORE STRUCTURE, not MORE DATA. Overbuilt instead.
|
||||
|
||||
## Intention
|
||||
|
||||
Each event = mutation delta + lean snapshot. Structured for filter/group/analyzer. No roster arrays.
|
||||
|
||||
### Canonical lean event shape
|
||||
|
||||
```js
|
||||
{
|
||||
id, ts, type, // identity + filter key
|
||||
message, // human text (viewer displays)
|
||||
encounterId, encounterName, // which encounter
|
||||
participantId, participantName, // who (nullable: pause/nextTurn have none)
|
||||
// type-specific scalar delta (one of these clusters per type):
|
||||
// add: { init, maxHp, type: 'character'|'monster' }
|
||||
// remove: { dead: bool }
|
||||
// damage/heal:{ amount }
|
||||
// condition: { condition }
|
||||
// toggleActive:{ revive: bool }
|
||||
// reorder: { draggedId, targetId }
|
||||
// update: { changedFields: ['notes', ...] }
|
||||
// nextTurn: (none — snapshot carries new round/pointer)
|
||||
// pause/resume/start/end: (none)
|
||||
snapshot: { // lean, ALREADY GOOD — keep as-is
|
||||
round, currentTurnParticipantId, turnOrderIds, activeIds
|
||||
},
|
||||
undo: <type-specific inverse, id-based> // minimal
|
||||
}
|
||||
```
|
||||
|
||||
### Undo inverses (id-based, minimal)
|
||||
|
||||
| type | undo |
|
||||
|------|------|
|
||||
| add_participant | `{ participantId }` → removeParticipant by id |
|
||||
| remove_participant | full participant obj (must restore — only place full obj needed) |
|
||||
| damage | `{ amount: -amount }` or heal back |
|
||||
| heal | `{ amount: -amount }` or damage back |
|
||||
| condition | `{ condition }` (toggle is its own inverse) |
|
||||
| toggleActive | `{ revive: !revive }` |
|
||||
| reorder | `{ draggedId, targetId }` (swap back) |
|
||||
| update | `{ changedFields: [...], oldValues: {...} }` |
|
||||
| nextTurn | `{ currentTurnParticipantId, round }` from snapshot delta |
|
||||
| pause/resume | toggle |
|
||||
| start/end | full prior scalar state |
|
||||
|
||||
**No `redo` field.** Redo = re-apply forward delta from event itself.
|
||||
|
||||
**No `undo_payload` wrapper.** Just `undo` object.
|
||||
|
||||
## 4 Consumers
|
||||
|
||||
1. **Viewer** — shows `message` + `type` + `participantName` + `undone` flag. Legacy logs (no type) display message only.
|
||||
2. **Undo/redo** — apply `undo` inverse by id. Id-based, minimal.
|
||||
3. **Replay** — turn.js funcs write own lean events. Trust func return. NO read-back. JSONL trace = lean snapshots + types.
|
||||
4. **Analyzer** — reads `snapshot` sequence + `type`. Reconstructs roster from event stream (add/remove by id). NO payload arrays needed.
|
||||
|
||||
## Legacy Support
|
||||
|
||||
- **Viewer:** yes — show old message-only logs, no type parse
|
||||
- **Undo:** no — old logs unundoable (acceptable, sit inert)
|
||||
- **Analyzer:** best-effort — snapshot may be absent on legacy → skip invariant check for that run
|
||||
|
||||
## Why Lean
|
||||
|
||||
- `add_participant` 16KB → ~150 bytes
|
||||
- Replay trace 5.5MB → ~250KB
|
||||
- Undo still works (proven old log did same)
|
||||
- Analyzer unaffected (uses snapshot+type, not payload)
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Redesign event writers (shared/turn.js)
|
||||
|
||||
- [ ] Rewrite `buildEntry()` to produce lean shape (delta fields per type)
|
||||
- [ ] Rewrite `commit()` — no patch storage, write lean event
|
||||
- [ ] Per-func: compute minimal delta + inverse from enc before/after
|
||||
- [ ] Drop `payload`, `undo_payload`, `undo_payload.redo`
|
||||
|
||||
### Phase 2: Update undo executor
|
||||
|
||||
- [ ] Find undo consumer (App.js `applyUndo` or similar)
|
||||
- [ ] Switch to id-based inverse application
|
||||
- [ ] Handle each type's undo shape
|
||||
- [ ] Drop `transactionalUndo` full-patch path if exists
|
||||
|
||||
### Phase 3: Update replay trace
|
||||
|
||||
- [ ] `replay-combat.js` callStep: trust return, snapshot only (no read-back)
|
||||
- [ ] Trace = `{step, ts, type, pre:leanSnap, post:leanSnap}` — no participant arrays
|
||||
|
||||
### Phase 4: Update analyzer
|
||||
|
||||
- [ ] `analyze-turns.js` already uses snapshot+type only — verify no payload dep
|
||||
- [ ] Reconstruct roster from event stream for skip/double-act checks (id-level)
|
||||
|
||||
### Phase 5: Legacy compat
|
||||
|
||||
- [ ] `logEvent.js normalizeEvent` — lift legacy into lean shape (type='legacy', message only)
|
||||
- [ ] Viewer handles legacy gracefully
|
||||
- [ ] Download = lean events only (legacy excluded or message-only)
|
||||
|
||||
### Phase 6: Cleanup
|
||||
|
||||
- [ ] Delete old bloated docs from DB? Or leave (inert, viewer reads message)?
|
||||
- [ ] Verify build CI=true passes
|
||||
- [ ] Verify analyzer clean on fresh replay
|
||||
- [ ] Verify undo works on new events
|
||||
|
||||
## NOT in scope
|
||||
|
||||
- Fixing 5 pre-existing initiative bugs (logged in TODO.md, separate)
|
||||
- Changing snapshot shape (already lean)
|
||||
- Changing viewer UI (just data source)
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Undo for `remove_participant`: must restore full obj (only place full obj needed). Acceptable?
|
||||
- Should legacy logs be deleted from DB or left inert?
|
||||
- Download format: lean JSON array (no legacy) — confirm
|
||||
Reference in New Issue
Block a user