- 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
5.1 KiB
5.1 KiB
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
{
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
- Viewer — shows
message+type+participantName+undoneflag. Legacy logs (no type) display message only. - Undo/redo — apply
undoinverse by id. Id-based, minimal. - Replay — turn.js funcs write own lean events. Trust func return. NO read-back. JSONL trace = lean snapshots + types.
- Analyzer — reads
snapshotsequence +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_participant16KB → ~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
applyUndoor similar) - Switch to id-based inverse application
- Handle each type's undo shape
- Drop
transactionalUndofull-patch path if exists
Phase 3: Update replay trace
replay-combat.jscallStep: trust return, snapshot only (no read-back)- Trace =
{step, ts, type, pre:leanSnap, post:leanSnap}— no participant arrays
Phase 4: Update analyzer
analyze-turns.jsalready 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