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 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 10:00:17 -04:00
parent 6cd25dadaa
commit e23cea205a
+55 -6
View File
@@ -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;
}