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.
This commit is contained in:
david raistrick
2026-07-07 16:41:34 -04:00
parent 9870d4df0d
commit 0fa3928bf8
3 changed files with 29 additions and 6 deletions
+17 -5
View File
@@ -102,6 +102,8 @@ function snapshotOf(enc) {
currentTurnParticipantId: enc.currentTurnParticipantId ?? null,
turnOrderIds: [...(enc.turnOrderIds || [])],
activeIds: (enc.participants || []).filter(p => p.isActive).map(p => p.id),
startedAt: enc.startedAt ?? null,
endedAt: enc.endedAt ?? null,
};
}
@@ -305,12 +307,18 @@ function expandUndo(entry, currentEnc) {
redo: { currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [] },
};
}
case 'start_encounter':
case 'start_encounter': {
return {
encounterPath: encPath,
updates: { ...u, startedAt: u.startedAt ?? null },
redo: { isStarted: true, isPaused: false, currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [], startedAt: snap.startedAt ?? Date.now() },
};
}
case 'end_encounter': {
return {
encounterPath: encPath,
updates: u,
redo: { isStarted: entry.type === 'start_encounter', isPaused: false, currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [] },
updates: { ...u, endedAt: u.endedAt ?? null },
redo: { isStarted: false, isPaused: false, currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [], endedAt: snap.endedAt ?? Date.now() },
};
}
default:
@@ -391,6 +399,8 @@ async function startEncounter(encounter, ctx) {
participants: sortedParticipants,
currentTurnParticipantId: firstActive.id,
turnOrderIds: sortedParticipants.map(p => p.id),
startedAt: Date.now(),
endedAt: null,
};
const log = {
type: 'start_encounter',
@@ -403,6 +413,8 @@ async function startEncounter(encounter, ctx) {
round: encounter.round ?? 0,
currentTurnParticipantId: encounter.currentTurnParticipantId ?? null,
turnOrderIds: [...(encounter.turnOrderIds || [])],
startedAt: encounter.startedAt ?? null,
endedAt: encounter.endedAt ?? null,
},
};
return commit(encounter, patch, log, ctx);
@@ -1003,12 +1015,12 @@ async function reorderParticipants(encounter, draggedId, targetId, ctx) {
}
async function endEncounter(encounter, ctx) {
const patch = { isStarted: false, isPaused: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] };
const patch = { isStarted: false, isPaused: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [], endedAt: Date.now() };
const log = {
type: 'end_encounter',
message: `Combat ended: "${encounter.name}"`,
delta: {},
undo: { isStarted: encounter.isStarted ?? false, isPaused: encounter.isPaused ?? false, round: encounter.round ?? 0, currentTurnParticipantId: encounter.currentTurnParticipantId ?? null, turnOrderIds: [...(encounter.turnOrderIds || [])] },
undo: { isStarted: encounter.isStarted ?? false, isPaused: encounter.isPaused ?? false, round: encounter.round ?? 0, currentTurnParticipantId: encounter.currentTurnParticipantId ?? null, turnOrderIds: [...(encounter.turnOrderIds || [])], endedAt: encounter.endedAt ?? null },
};
return commit(encounter, patch, log, ctx);
}