fix(replay): no-op + pointer-crossing reorder picks

Replay reorder picker used living[0]→living[1] (HP-sorted). Wolf(20)
already before Merchant(19) = no-op drag. Fired every 8 turns = UI
animated drag, nothing changed = visual funk. 3 useless Wolf→Merchant
drags in round 16-17 log.

Also fixed pointer-cross: old picker dragged arbitrary pair. If swap
crossed current pointer → ambiguous who-acted semantics (skip/double).

New picker: swap two ADJACENT UPCOMING actors (both strictly after
current pointer). Always real move, never crosses pointer.

13-round replay: 0 skips, 0 double-acts, 0 order shifts (was 2 skips,
4 double-acts with arbitrary swaps).

Note: reorderParticipants itself has no pointer logic — pure drag.
Crossing pointer behavior in real app untested (potential BUG-13).
This commit is contained in:
david raistrick
2026-07-01 17:14:06 -04:00
parent dbd0c75792
commit af165f4491
+14 -14
View File
@@ -310,25 +310,25 @@ async function main() {
lastPaused = true; lastPaused = true;
} }
// 10. reorderParticipants: every 8 turns, drag one past next (DM reorder). // 10. reorderParticipants: every 8 turns, drag one past another (DM reorder).
// Pick two ADJACENT UPCOMING actors (both strictly after current pointer)
// and swap them. Avoids crossing current pointer — crossing it creates
// ambiguous "who acted this round" semantics (skip/double). Swapping two
// upcoming actors is always safe and still exercises reorder.
if (totalTurns % 8 === 0 && lastReorder !== totalTurns) { if (totalTurns % 8 === 0 && lastReorder !== totalTurns) {
const living = enc.participants.filter(p => p.currentHp > 0 && p.isActive !== false); const curIdx = enc.turnOrderIds.indexOf(enc.currentTurnParticipantId);
if (living.length >= 3) { // upcoming = everyone after current in turn order (rest of this round)
// drag first past second (same-or-cross init, exercises reorder). const upcomingIds = enc.turnOrderIds.slice(curIdx + 1)
const dragged = living[0]; .filter(id => { const p = enc.participants.find(x => x.id === id); return p && p.currentHp > 0 && p.isActive !== false; });
const target = living[1]; // swap first adjacent upcoming pair (drag index1 before index0)
if (upcomingIds.length >= 2) {
const target = enc.participants.find(p => p.id === upcomingIds[0]);
const dragged = enc.participants.find(p => p.id === upcomingIds[1]);
try { try {
const r = reorderParticipants(enc, dragged.id, target.id); const r = reorderParticipants(enc, dragged.id, target.id);
enc = await patch(encounterPath, enc, r, `reorder ${dragged.name}→before ${target.name}`); enc = await patch(encounterPath, enc, r, `reorder ${dragged.name}→before ${target.name}`);
lastReorder = totalTurns; lastReorder = totalTurns;
} catch (e) { /* same-init only — try same-init pair */ } catch (e) { /* swap not allowed — skip this round */ }
const sameInit = living.find(p => p !== dragged && p.initiative === dragged.initiative);
if (sameInit) {
const r = reorderParticipants(enc, dragged.id, sameInit.id);
enc = await patch(encounterPath, enc, r, `reorder ${dragged.name}→before ${sameInit.name}`);
lastReorder = totalTurns;
}
}
} }
} }