fix(turn): block cross-pointer reorder (BUG-13)
reorderParticipants allowed dragging across current pointer mid-round. nextTurn walks turnOrderIds forward from pointer, so cross-pointer drag = ambiguous who-acts-next: upcoming dragged ahead of pointer = skipped (walk wraps past), acted dragged behind pointer = double-act. Full fix needs actedThisRound tracking. Pragmatic now: block both directions during active encounter. Pre-combat: free reorder (no pointer). reorderParticipants: added pointer check (currentTurn index). If drag crosses pointer, return null patch (no-op). Same-side reorder allowed. syncTurnOrder now always mirrors (was started-only — pre-combat reorder left turnOrderIds stale). Tests: - turn.bug13.test.js: cross-pointer blocked (both dirs), same-side allowed, pre-combat free. - turn.reorder.test.js: 3 tests updated to pre-combat (cross-pointer now blocked post-start). - turn.invariant.test.js: reorder test uses same-side upcoming pair. 214 tests green.
This commit is contained in:
+22
-3
@@ -536,10 +536,13 @@ function toggleCondition(encounter, participantId, conditionId) {
|
||||
};
|
||||
}
|
||||
|
||||
// REORDER_PARTICIPANTS — drag-drop. 1-list model: drag overrides initiative
|
||||
// (DM choice). Cross-init drag allowed. Splices participants[], syncs turnOrderIds.
|
||||
// REORDER_PARTICIPANTS — drag. Same-initiative ONLY (tie-break override).
|
||||
// Cross-init drag = no-op (use edit-initiative field instead). Splice move.
|
||||
// Cross-pointer drag = no-op once encounter started (BUG-13). nextTurn walks
|
||||
// turnOrderIds forward from current pointer. Dragging a not-yet-acted
|
||||
// participant to a position behind the pointer would skip them (wrap past).
|
||||
// Dragging an already-acted participant ahead of pointer would re-act them.
|
||||
// Blocking cross-pointer keeps rotation deterministic. Pre-combat: free.
|
||||
function reorderParticipants(encounter, draggedId, targetId) {
|
||||
const participants = [...(encounter.participants || [])];
|
||||
const dragged = participants.find(p => p.id === draggedId);
|
||||
@@ -550,10 +553,26 @@ function reorderParticipants(encounter, draggedId, targetId) {
|
||||
return { patch: null, log: null }; // cross-init blocked
|
||||
}
|
||||
const draggedIndex = participants.findIndex(p => p.id === draggedId);
|
||||
// BUG-13: block cross-pointer reorder while encounter running. nextTurn
|
||||
// walks turnOrderIds forward from current pointer. Dragging across the
|
||||
// pointer makes who-acts-next ambiguous — either skip (upcoming dragged
|
||||
// ahead of pointer, walk wraps past) or double-act (acted dragged behind).
|
||||
// Full fix needs actedThisRound tracking. Pragmatic now: block both dirs.
|
||||
// Pre-combat: free reorder (no pointer yet).
|
||||
if (encounter.isStarted && encounter.currentTurnParticipantId) {
|
||||
const pointerIdx = participants.findIndex(p => p.id === encounter.currentTurnParticipantId);
|
||||
const targetIdx0 = participants.findIndex(p => p.id === targetId);
|
||||
if (pointerIdx >= 0) {
|
||||
const crosses = (a, b) => (a <= pointerIdx && b > pointerIdx) || (a > pointerIdx && b <= pointerIdx);
|
||||
if (crosses(draggedIndex, targetIdx0)) {
|
||||
return { patch: null, log: null }; // cross-pointer blocked
|
||||
}
|
||||
}
|
||||
}
|
||||
const [removedItem] = participants.splice(draggedIndex, 1);
|
||||
const newTargetIndex = participants.findIndex(p => p.id === targetId);
|
||||
participants.splice(newTargetIndex, 0, removedItem);
|
||||
const turnUpdates = encounter.isStarted ? syncTurnOrder(participants) : {};
|
||||
const turnUpdates = syncTurnOrder(participants); // 1-list: always mirror
|
||||
return { patch: { participants, ...turnUpdates }, log: null };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user