- 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
134 lines
5.1 KiB
Markdown
134 lines
5.1 KiB
Markdown
# 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
|