fix(turn): log reorder + deathSave (BUG-7)

reorderParticipants returned log:null → handler skipped logAction → drag
invisible in combat log, no undo payload. Now returns
log:{ message, undo:{ participants, turnOrderIds, currentTurnParticipantId }}.
App.js handleDrop calls logAction on logged reorders.

Found second gap: deathSave also returned null log (both branches).
Fixed — message + undo (participants snapshot).

Added logging contract test (turn.logging.test.js):
- all mutating ops logged (start/next/pause/add/remove/toggle/hp/deathsave/
  condition/reorder/end)
- no-ops return null log (same-id, cross-init, cross-pointer blocks)
- undo payloads valid + restore prior state
- documents addParticipants + updateParticipant gaps (null log, intentional)

235 tests green.
This commit is contained in:
david raistrick
2026-07-04 17:22:52 -04:00
parent 5a09f71d4c
commit ab9f8fa2e7
5 changed files with 281 additions and 7 deletions
+25 -3
View File
@@ -477,6 +477,8 @@ function deathSave(encounter, participantId, saveNumber) {
if (!participant) throw new Error('Participant not found.');
const currentSaves = participant.deathSaves || 0;
const newSaves = currentSaves === saveNumber ? saveNumber - 1 : saveNumber;
const name = participant.name;
const undo = { participants: encounter.participants || [] };
if (newSaves === 3) {
// Mark dying — caller waits for animation, then calls removeParticipant.
@@ -485,7 +487,10 @@ function deathSave(encounter, participantId, saveNumber) {
);
return {
patch: { participants: updatedParticipants },
log: null,
log: {
message: `${name} failed 3 death saves — dying`,
undo,
},
isDying: true,
};
}
@@ -493,7 +498,14 @@ function deathSave(encounter, participantId, saveNumber) {
const updatedParticipants = (encounter.participants || []).map(p =>
p.id === participantId ? { ...p, deathSaves: newSaves } : p
);
return { patch: { participants: updatedParticipants }, log: null, isDying: false };
return {
patch: { participants: updatedParticipants },
log: {
message: `${name} death save: ${newSaves}/3`,
undo,
},
isDying: false,
};
}
// TOGGLE_CONDITION — verbatim from ParticipantManager.toggleCondition
@@ -553,7 +565,17 @@ function reorderParticipants(encounter, draggedId, targetId) {
const newTargetIndex = participants.findIndex(p => p.id === targetId);
participants.splice(newTargetIndex, 0, removedItem);
const turnUpdates = syncTurnOrder(participants); // 1-list: always mirror
return { patch: { participants, ...turnUpdates }, log: null };
return {
patch: { participants, ...turnUpdates },
log: {
message: `${removedItem.name} reordered before ${(participants.find(p => p.id === targetId) || {}).name || targetId}`,
undo: {
participants: encounter.participants || [],
turnOrderIds: encounter.turnOrderIds || [],
currentTurnParticipantId: encounter.currentTurnParticipantId ?? null,
},
},
};
}
// END_ENCOUNTER — verbatim from InitiativeControls.confirmEndEncounter