Commit Graph
6 Commits
Author SHA1 Message Date
david raistrick 0fa3928bf8 Track encounter start/end timestamps; display on card
startEncounter sets startedAt (clears endedAt on restart). endEncounter sets
endedAt. snapshotOf includes both for undo/redo fidelity. expandUndo split
start/end cases to restore old timestamps.

Display: encounter card shows Started/Ended independently (endedAt shows
even without startedAt). Undo sets null (can't delete via merge), test snap
strips only startedAt/endedAt null for compare.

Reviewed by pi gpt-5.5: stale endedAt on restart fixed, snap null-strip
narrowed. Medium-low risk.
2026-07-07 16:41:34 -04:00
david raistrick bf1fccfd3b Exclude pause/resume from undo; fix verify round-wrap false positive
togglePause no longer writes log entry (shared/turn.js). Lifecycle op, not
player action. Undo stack now targets last real action, not flag flip.
Removed dead pause/resume cases from expandUndo.

verify.js false positive: round wrap via non-nextTurn event (removeParticipant)
not detected — cycleActed never reset, actors flagged as acted_twice. Fix:
detect round change on ANY event, drain removed/inactive before finalize
(avoids false skipped when actor removed mid-round).

Tests updated: togglePause logging test asserts no log; undo test asserts
excluded. Repro confirmed via minimal verify case (removeParticipant r1->r2).
2026-07-06 18:30:51 -04:00
david raistrick 2c997de0da Fix undo/redo forward-state restore, harden test infra, skip-guard
Root cause: mutation writers stored oldValues only. Redo rebuilt from
current state via derived logic — wrong order, wrong content, or no-op.
Undo patched fields in place but ignored roster/array order changes.

Writers now store forward arrays (shared/turn.js):
- addParticipant: newTurnOrderIds, newCurrentTurnParticipantId
- removeParticipant: newTurnOrderIds, newCurrentTurnParticipantId
- updateParticipant: oldTurnOrderIds + newTurnOrderIds (initiative re-slot)
- reorderParticipants: newParticipants + newTurnOrderIds
- damage/heal/deathSave/stabilize/revive/deactivate_dead_monster:
  oldValues + newValues both capture full death state incl conditions

expandUndo redo uses stored forward state instead of deriving:
- add/remove: order via newTurnOrderIds
- update: initiative change re-orders both directions
- reorder: re-applies newParticipants
- death ops: restore via newValues (was HP-only, dropped status/conditions)

Missing expandUndo cases added: stabilize, revive, deactivate_dead_monster
(were default:null -> undo no-op).

Test infra (shared/tests/_helpers.js):
- Mock storage undo() real now: applies patch + flips undone flag (was no-op)
- addDoc persists log entries, getCollection returns them
- undoLast/redoLast harness helpers exercise real mechanism
- Undo tests assert against actual persisted doc, not expandUndo output

turn.undo.test.js rewritten: every op tested as undo->deepEqual before,
redo->deepEqual after-op (12 cases). Was undo-only, redo untested.
turn.deathsave.undo.test.js added: 11 death-path roundtrips.

Dead code removed:
- round-trip.test.js: skipped suite testing deleted replay-from-logs.js
  (5 skipped tests rotting). Coverage gap acknowledged in TODO.

Skip-guard added (scripts/run-tests.sh): pre-flight grep refuses to run
if any .skip/xdescribe/xit found in test dirs. No CI; this is the gate.
2026-07-06 18:09:28 -04:00
david raistrick 2569cc4497 Builds replayable combat logs with first-class undo/redo, unified verification tooling, fast indexed log queries, and stricter CI.
feat(combat): add first-class undo and redo controls

Add undo and redo controls to the combat UI so the DM can recover from recent
actions without leaving the encounter view.

Undo and redo operate on the current encounter's combat history. Empty stacks
produce clear feedback instead of failing silently. Redo order follows normal
stack behavior after multiple undos.

This makes combat history actionable during play, not just visible in the log.

feat(logs): make combat logs replayable

Replace plain combat log messages with structured combat events that can be
used by the UI, exported as JSON, replayed, and verified.

Each new log entry records the action type, encounter identity, participant
identity, a small action delta, undo intent, and a turn snapshot. Download and
copy now export the event stream as JSON so a saved combat log is useful for
offline analysis and debugging.

Legacy logs remain viewable, but new logs use the structured event format.

feat(logs): make undo and redo transactional

Apply undo and redo as single storage operations so the encounter state and log
state cannot drift apart.

Server storage applies the encounter update and the log undone flag inside one
SQLite transaction. Firebase storage uses a batch write for the same behavior.
The storage contract now includes undo/redo semantics.

This replaces fragile multi-write undo behavior where a failure could update
the encounter without marking the log, or mark the log without updating the
encounter.

feat(combat): add unified replay and verification tool

Add one combat CLI for replaying live combat and verifying combat logs.

Replay drives the live backend through the same shared combat logic used by the
app, writes a JSON event log to an explicit output path, and automatically
verifies the result. Verification checks for DM-visible combat problems such as
skipped turns, double actions, bad round changes, and unexpected turn order
changes.

The tool uses the same JSON event stream produced by log downloads, supports
verbose turn output, and handles Ctrl-C by ending the encounter, writing the
partial log, and verifying what was captured.

fix(perf): keep long combat logging fast

Remove the combat-time log query bottleneck that made long replays slow as log
volume grew.

Combat controls no longer subscribe to the log collection just to keep undo and
redo state warm. Undo and redo now query the latest matching encounter log only
when clicked. Server collection queries support exact filters, ordering,
limits, and offsets, and SQLite indexes keep latest-log and per-encounter log
lookups fast.

Also fix duplicate WebSocket handler registration so realtime updates do not
double-fire under write load.

fix(turns): make toggle active a status change

Make toggle active a roster/status edit instead of a turn advance.

Deactivating the current participant no longer passes the turn or increments
the round. The current turn stays where it is until the DM explicitly clicks
Next Turn, and Next Turn skips inactive participants during normal rotation.

This matches the initiative design: slot order is stable, toggle active does
not move participants, and round changes only come from explicit turn advance.

chore(ci): make warnings and hangs fail fast

Tighten test and build checks so failures are visible instead of noisy or
silent.

Builds run with CI enabled so warnings fail production builds. The full test
command runs app, shared, and server suites with hard timeouts so hangs fail
quickly. Static eslint coverage fails on warnings as well as errors.

Tests were updated around the new async combat logging flow, structured log
events, transactional undo, replay verification, and toggle-active semantics.
2026-07-06 10:33:28 -04:00
david raistrick 5d3a0607ef refactor: 1-list turn order model (turnOrderIds === participants.map(id))
Single source of truth. No re-sort after startEncounter. Drag overrides
initiative (cross-init drag allowed, DM choice). Display === rotation by
construction — same array.

shared/turn.js:
- syncTurnOrder(participants) helper: turnOrderIds = participants.map(id)
- startEncounter: sort ALL participants by init (active+inactive), inactive
  stay in slot, nextTurn skips them. currentTurn = first active.
- addParticipant: splice into participants[] by init pos, sync turnOrderIds.
  computeTurnOrderAfterAddition returns insertAt (caller splices + syncs).
- removeParticipant: filter participants[], sync turnOrderIds, advance
  current if removed==current.
- toggleParticipantActive: stay in slot (flip isActive only), sync. Advance
  current only if deact hits current.
- reorderParticipants: cross-init drag allowed (remove same-init restriction).
  Splice participants[], sync turnOrderIds. Fixes BUG-6.
- computeTurnOrderAfterRemoval: only handles current-advance now (list sync
  at call site).

Tests updated to 1-list contract:
- turn.invariant.test.js: 10 tests, turnOrderIds===participants.map(id)
  always, cross-init drag, inactive-in-slot, rotation follows list.
- turn.characterization/reorder/round-rotation/undo/remove: updated
  expectations (inactive-in-slot, cross-init drag, turnOrderIds sync on
  reorder, insertAt return).

Results: shared 90 green. 500-round replay CLEAN (0 skips, 0 doubles,
0 order shifts). BUG-6 (reorder divergence) fixed structurally.

FE App.js still has duplicate turn funcs + sortParticipantsByInitiative
display render (step 4: delete dups, render participants[] directly).
2026-07-01 16:00:00 -04:00
david raistrick be481767f0 tests: undo roundtrip (10 green) + reorderParticipants BUG-7 candidate
turn.undo.test.js: every op with log.undo roundtrips to prior state.
startEncounter, nextTurn, togglePause, applyHpChange, toggleCondition,
toggleParticipantActive, addParticipant, removeParticipant, endEncounter.

Found: reorderParticipants returns log:null. Cannot undo. Documents as
BUG-7 candidate (test green now, asserts current behavior).
2026-06-30 13:50:48 -04:00