From e23cea205ab3a87b0a2161345dd737e30bb2d4ec Mon Sep 17 00:00:00 2001 From: robert Date: Sat, 16 May 2026 10:00:17 -0400 Subject: [PATCH] Add HP toggle, new conditions, fix turn order sync bug - Add DM toggle (default on) to hide player HP bars on player display; persisted in activeDisplay Firestore doc for real-time sync - Add Alchemist Fire and Bardic Inspiration conditions; sort all conditions alphabetically - Fix turn order skipping when participants are deleted, deactivated, or killed mid-combat: turnOrderIds was never updated, causing handleNextTurn to resolve currentIndex as -1 and snap back to the first participant. Now all mutation paths (delete, toggle active, HP death/resurrection) keep turnOrderIds in sync and advance the turn pointer correctly when the current participant is removed. Co-Authored-By: Claude Sonnet 4.6 --- src/App.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/App.js b/src/App.js index b025749..03e08f3 100644 --- a/src/App.js +++ b/src/App.js @@ -152,6 +152,29 @@ const sortParticipantsByInitiative = (participants, originalOrder) => { }); }; +// Returns turnOrderIds/currentTurnParticipantId updates when a participant leaves active combat. +const computeTurnOrderAfterRemoval = (encounter, removedId, updatedParticipants) => { + if (!encounter.isStarted) return {}; + const currentIds = encounter.turnOrderIds || []; + const newIds = currentIds.filter(id => id !== removedId); + const updates = { turnOrderIds: newIds }; + if (encounter.currentTurnParticipantId === removedId) { + const removedPos = currentIds.indexOf(removedId); + const candidates = [...currentIds.slice(removedPos + 1), ...currentIds.slice(0, removedPos)]; + const nextId = candidates.find(id => updatedParticipants.find(p => p.id === id && p.isActive)) ?? null; + updates.currentTurnParticipantId = nextId; + } + return updates; +}; + +// Returns turnOrderIds update when a participant re-enters active combat mid-encounter. +const computeTurnOrderAfterAddition = (encounter, addedId) => { + if (!encounter.isStarted) return {}; + const currentIds = encounter.turnOrderIds || []; + if (currentIds.includes(addedId)) return {}; + return { turnOrderIds: [...currentIds, addedId] }; +}; + // ============================================================================ // CUSTOM HOOKS // ============================================================================ @@ -956,7 +979,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const updatedParticipants = participants.filter(p => p.id !== itemToDelete.id); try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await updateDoc(doc(db, encounterPath), { + participants: updatedParticipants, + ...computeTurnOrderAfterRemoval(encounter, itemToDelete.id, updatedParticipants) + }); } catch (err) { console.error("Error deleting participant:", err); alert("Failed to delete participant. Please try again."); @@ -969,12 +995,20 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const toggleParticipantActive = async (participantId) => { if (!db) return; + const participant = participants.find(p => p.id === participantId); + if (!participant) return; + const newIsActive = !participant.isActive; + const updatedParticipants = participants.map(p => - p.id === participantId ? { ...p, isActive: !p.isActive } : p + p.id === participantId ? { ...p, isActive: newIsActive } : p ); + const turnUpdates = newIsActive + ? computeTurnOrderAfterAddition(encounter, participantId) + : computeTurnOrderAfterRemoval(encounter, participantId, updatedParticipants); + try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await updateDoc(doc(db, encounterPath), { participants: updatedParticipants, ...turnUpdates }); } catch (err) { console.error("Error toggling active state:", err); } @@ -1030,8 +1064,14 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { return p; }); + const turnUpdates = (isDead && !wasDead) + ? computeTurnOrderAfterRemoval(encounter, participantId, updatedParticipants) + : wasResurrected + ? computeTurnOrderAfterAddition(encounter, participantId) + : {}; + try { - await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); + await updateDoc(doc(db, encounterPath), { participants: updatedParticipants, ...turnUpdates }); setHpChangeValues(prev => ({ ...prev, [participantId]: '' })); } catch (err) { console.error("Error applying HP change:", err); @@ -1594,10 +1634,19 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { return; } - const currentIndex = activePsInOrder.findIndex(p => p.id === encounter.currentTurnParticipantId); - let nextIndex = (currentIndex + 1) % activePsInOrder.length; + let currentIndex = activePsInOrder.findIndex(p => p.id === encounter.currentTurnParticipantId); let nextRound = encounter.round; + // Current participant was removed; find the next one after their old position in turnOrderIds + if (currentIndex === -1) { + const rawPos = (encounter.turnOrderIds || []).indexOf(encounter.currentTurnParticipantId); + const candidateIds = [...(encounter.turnOrderIds || []).slice(rawPos + 1), ...(encounter.turnOrderIds || []).slice(0, rawPos)]; + const nextP = candidateIds.map(id => activePsInOrder.find(p => p.id === id)).find(Boolean); + currentIndex = nextP ? activePsInOrder.findIndex(p => p.id === nextP.id) - 1 : -1; + } + + let nextIndex = (currentIndex + 1) % activePsInOrder.length; + if (nextIndex === 0 && currentIndex !== -1) { nextRound += 1; }