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).
This commit is contained in:
@@ -220,7 +220,18 @@ function checkCycles(steps, rounds) {
|
|||||||
if (started && pre && post.currentTurnParticipantId &&
|
if (started && pre && post.currentTurnParticipantId &&
|
||||||
pre.currentTurnParticipantId !== post.currentTurnParticipantId) {
|
pre.currentTurnParticipantId !== post.currentTurnParticipantId) {
|
||||||
const wrapped = pre.round !== undefined && post.round !== undefined && post.round !== pre.round;
|
const wrapped = pre.round !== undefined && post.round !== undefined && post.round !== pre.round;
|
||||||
if (wrapped && isNextTurn) {
|
if (wrapped) {
|
||||||
|
// drain removed/inactive before finalizing so removed actors
|
||||||
|
// aren't flagged as skipped.
|
||||||
|
if (pre && pre.activeIds && post.activeIds) {
|
||||||
|
const postSet = new Set(post.activeIds);
|
||||||
|
for (const id of [...cycleActive]) {
|
||||||
|
if (!postSet.has(id)) {
|
||||||
|
cycleActive.delete(id);
|
||||||
|
cycleActed.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
finalize(s.step);
|
finalize(s.step);
|
||||||
cycleRound = post.round;
|
cycleRound = post.round;
|
||||||
cycleActive = new Set(post.activeIds || []);
|
cycleActive = new Set(post.activeIds || []);
|
||||||
|
|||||||
@@ -61,12 +61,11 @@ describe('Logging contract: mutating ops', () => {
|
|||||||
expectLastLogged(storage);
|
expectLastLogged(storage);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('togglePause logs', async () => {
|
test('togglePause does NOT log (lifecycle excluded from undo)', async () => {
|
||||||
const e = enc([p('a', 10), p('b', 7), p('c', 3)]);
|
const e = enc([p('a', 10), p('b', 7), p('c', 3)]);
|
||||||
const started = await startEncounter(e, ctx);
|
const started = await startEncounter(e, ctx);
|
||||||
await togglePause(started, ctx);
|
await togglePause(started, ctx);
|
||||||
expect(storage.logs()).toHaveLength(2);
|
expect(storage.logs()).toHaveLength(1); // start only
|
||||||
expectLastLogged(storage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('addParticipant logs', async () => {
|
test('addParticipant logs', async () => {
|
||||||
|
|||||||
@@ -66,18 +66,16 @@ describe('undo + redo roundtrip', () => {
|
|||||||
expect(snap(afterRedo)).toEqual(snap(newEnc));
|
expect(snap(afterRedo)).toEqual(snap(newEnc));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('togglePause', async () => {
|
test('togglePause excluded from undo (no log entry)', async () => {
|
||||||
const { storage, ctx } = mockCtx();
|
const { storage, ctx } = mockCtx();
|
||||||
let e = enc([p('a',10),p('b',20)]);
|
let e = enc([p('a',10),p('b',20)]);
|
||||||
e = await startEncounter(e, ctx);
|
e = await startEncounter(e, ctx);
|
||||||
await storage.setDoc(ctx.encPath, e);
|
await storage.setDoc(ctx.encPath, e);
|
||||||
const before = snap(e);
|
const before = snap(e);
|
||||||
const newEnc = await togglePause(e, ctx);
|
const newEnc = await togglePause(e, ctx);
|
||||||
const last = storage.logs().at(-1);
|
// state flips (isPaused), but no log written
|
||||||
const afterUndo = await undoLast(ctx, newEnc);
|
expect(newEnc.isPaused).toBe(true);
|
||||||
expect(snap(afterUndo)).toEqual(before);
|
expect(storage.logs()).toHaveLength(1); // start only
|
||||||
const afterRedo = await redoLast(ctx, afterUndo, last);
|
|
||||||
expect(snap(afterRedo)).toEqual(snap(newEnc));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('applyHpChange damage restores HP on undo, re-applies on redo', async () => {
|
test('applyHpChange damage restores HP on undo, re-applies on redo', async () => {
|
||||||
|
|||||||
+3
-15
@@ -304,14 +304,6 @@ function expandUndo(entry, currentEnc) {
|
|||||||
redo: { currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [] },
|
redo: { currentTurnParticipantId: snap.currentTurnParticipantId, round: snap.round, turnOrderIds: snap.turnOrderIds || [] },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'pause':
|
|
||||||
case 'resume': {
|
|
||||||
return {
|
|
||||||
encounterPath: encPath,
|
|
||||||
updates: { isPaused: u.isPaused },
|
|
||||||
redo: { isPaused: !u.isPaused },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'start_encounter':
|
case 'start_encounter':
|
||||||
case 'end_encounter': {
|
case 'end_encounter': {
|
||||||
return {
|
return {
|
||||||
@@ -456,13 +448,9 @@ async function togglePause(encounter, ctx) {
|
|||||||
if (!encounter || !encounter.isStarted) throw new Error('Encounter not started.');
|
if (!encounter || !encounter.isStarted) throw new Error('Encounter not started.');
|
||||||
const newPausedState = !encounter.isPaused;
|
const newPausedState = !encounter.isPaused;
|
||||||
const patch = { isPaused: newPausedState, turnOrderIds: encounter.turnOrderIds };
|
const patch = { isPaused: newPausedState, turnOrderIds: encounter.turnOrderIds };
|
||||||
const log = {
|
// Lifecycle op: no log entry. Pause/resume excluded from undo stack so DM
|
||||||
type: newPausedState ? 'pause' : 'resume',
|
// undo targets the last player action (damage/condition/etc), not a flag flip.
|
||||||
message: `Combat ${newPausedState ? 'paused' : 'resumed'}: "${encounter.name}"`,
|
return commit(encounter, patch, null, ctx);
|
||||||
delta: { paused: newPausedState },
|
|
||||||
undo: { isPaused: encounter.isPaused ?? false },
|
|
||||||
};
|
|
||||||
return commit(encounter, patch, log, ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addParticipant(encounter, participant, ctx) {
|
async function addParticipant(encounter, participant, ctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user