Added defautl HP values.

This commit is contained in:
Robert Johnson 2025-05-26 21:48:28 -04:00
parent ad11bbc648
commit d631545570

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, /* ImageIcon removed */ EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon } from 'lucide-react'; // ImageIcon removed
import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2 } from 'lucide-react'; // Added Users2 for Add All
// --- Firebase Configuration ---
const firebaseConfig = {
@ -213,14 +213,13 @@ function App() {
{!isAuthReady && !error && <p>Authenticating...</p>}
</main>
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
TTRPG Initiative Tracker v0.1.25
TTRPG Initiative Tracker v0.1.26
</footer>
</div>
);
}
// --- Confirmation Modal Component ---
// ... (ConfirmationModal remains the same)
function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
if (!isOpen) return null;
return (
@ -332,7 +331,6 @@ function AdminView({ userId }) {
<div key={campaign.id} onClick={() => setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
<div className={`relative z-10 ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3 rounded-md' : ''}`}>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
{/* ImageIcon display removed from here */}
<button onClick={(e) => { e.stopPropagation(); requestDeleteCampaign(campaign.id, campaign.name); }} className="mt-2 text-red-300 hover:text-red-100 text-xs flex items-center bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"><Trash2 size={14} className="mr-1" /> Delete</button>
</div>
</div>);
@ -354,9 +352,6 @@ function AdminView({ userId }) {
);
}
// ... (CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
// These components are identical to the previous version (v0.1.24) and are included below for completeness.
function CreateCampaignForm({ onCreate, onCancel }) {
const [name, setName] = useState('');
const [backgroundUrl, setBackgroundUrl] = useState('');
@ -381,19 +376,41 @@ function CreateCampaignForm({ onCreate, onCancel }) {
function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState('');
const [editingCharacter, setEditingCharacter] = useState(null);
const [defaultMaxHp, setDefaultMaxHp] = useState(10); // New state for default HP
const [editingCharacter, setEditingCharacter] = useState(null); // { id, name, defaultMaxHp }
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const handleAddCharacter = async () => {
if (!db ||!characterName.trim() || !campaignId) return;
const newCharacter = { id: generateId(), name: characterName.trim() };
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); } catch (err) { console.error("Error adding character:", err); }
const hp = parseInt(defaultMaxHp, 10);
if (isNaN(hp) || hp <= 0) {
alert("Please enter a valid positive number for Default Max HP."); // TODO: Replace with better notification
return;
}
const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp };
try {
await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] });
setCharacterName('');
setDefaultMaxHp(10); // Reset default HP input
} catch (err) { console.error("Error adding character:", err); }
};
const handleUpdateCharacter = async (characterId, newName) => {
const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp) => {
if (!db ||!newName.trim() || !campaignId) return;
const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim() } : c);
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); setEditingCharacter(null); } catch (err) { console.error("Error updating character:", err); }
const hp = parseInt(newDefaultMaxHp, 10);
if (isNaN(hp) || hp <= 0) {
alert("Please enter a valid positive number for Default Max HP."); // TODO: Replace with better notification
setEditingCharacter(null); // Close edit mode on invalid input
return;
}
const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp } : c);
try {
await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters });
setEditingCharacter(null);
} catch (err) { console.error("Error updating character:", err); }
};
const requestDeleteCharacter = (characterId, charName) => { setItemToDelete({ id: characterId, name: charName, type: 'character' }); setShowDeleteCharConfirm(true); };
const confirmDeleteCharacter = async () => {
if (!db || !itemToDelete || itemToDelete.type !== 'character') return;
@ -402,25 +419,42 @@ function CharacterManager({ campaignId, campaignCharacters }) {
try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); } catch (err) { console.error("Error deleting character:", err); }
setShowDeleteCharConfirm(false); setItemToDelete(null);
};
return (
<>
<div className="p-4 bg-slate-800 rounded-lg shadow">
<h3 className="text-xl font-semibold text-sky-300 mb-3 flex items-center"><Users size={24} className="mr-2" /> Campaign Characters</h3>
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="flex gap-2 mb-4">
<input type="text" value={characterName} onChange={(e) => setCharacterName(e.target.value)} placeholder="New character name" className="flex-grow px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" />
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-md transition-colors flex items-center"><PlusCircle size={18} className="mr-1" /> Add Character</button>
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="flex flex-wrap gap-2 mb-4 items-end">
<div className="flex-grow">
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400">Name</label>
<input type="text" id="characterName" value={characterName} onChange={(e) => setCharacterName(e.target.value)} placeholder="New character name" className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" />
</div>
<div className="w-24">
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400">Default HP</label>
<input type="number" id="defaultMaxHp" value={defaultMaxHp} onChange={(e) => setDefaultMaxHp(e.target.value)} placeholder="HP" className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" />
</div>
<button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-md transition-colors flex items-center self-end"><PlusCircle size={18} className="mr-1" /> Add</button>
</form>
{campaignCharacters.length === 0 && <p className="text-sm text-slate-400">No characters added.</p>}
<ul className="space-y-2">
{campaignCharacters.map(character => (
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md">
{editingCharacter && editingCharacter.id === character.id ? (
<input type="text" defaultValue={character.name} onBlur={(e) => handleUpdateCharacter(character.id, e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleUpdateCharacter(character.id, e.target.value); if (e.key === 'Escape') setEditingCharacter(null); }} autoFocus className="flex-grow px-3 py-2 bg-slate-600 border border-slate-500 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white"/>
) : ( <span className="text-slate-100">{character.name}</span> )}
<div className="flex space-x-2">
<button onClick={() => setEditingCharacter(character)} className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" aria-label="Edit character"><Edit3 size={18} /></button>
<button onClick={() => requestDeleteCharacter(character.id, character.name)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" aria-label="Delete character"><Trash2 size={18} /></button>
</div>
<form onSubmit={(e) => {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp);}} className="flex-grow flex gap-2 items-center">
<input type="text" value={editingCharacter.name} onChange={(e) => setEditingCharacter({...editingCharacter, name: e.target.value})} className="flex-grow px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/>
<input type="number" value={editingCharacter.defaultMaxHp} onChange={(e) => setEditingCharacter({...editingCharacter, defaultMaxHp: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/>
<button type="submit" className="p-1 text-green-400 hover:text-green-300"><Save size={18}/></button>
<button type="button" onClick={() => setEditingCharacter(null)} className="p-1 text-slate-400 hover:text-slate-200"><XCircle size={18}/></button>
</form>
) : (
<>
<span className="text-slate-100">{character.name} <span className="text-xs text-slate-400">(HP: {character.defaultMaxHp || 'N/A'})</span></span>
<div className="flex space-x-2">
<button onClick={() => setEditingCharacter({id: character.id, name: character.name, defaultMaxHp: character.defaultMaxHp || 10})} className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-slate-600 hover:bg-slate-500" aria-label="Edit character"><Edit3 size={18} /></button>
<button onClick={() => requestDeleteCharacter(character.id, character.name)} className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-slate-600 hover:bg-slate-500" aria-label="Delete character"><Trash2 size={18} /></button>
</div>
</>
)}
</li>
))}
</ul>
@ -430,6 +464,13 @@ function CharacterManager({ campaignId, campaignCharacters }) {
);
}
// ... (EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
// For brevity, only the changed CharacterManager is shown in full.
// ParticipantManager and InitiativeControls will also need updates.
// The rest of the components (EncounterManager, CreateEncounterForm, EditParticipantModal, DisplayView, Modal, Icons)
// are assumed to be the same as v0.1.25 unless specified.
// --- EncounterManager --- (No changes in this iteration for EncounterManager itself, but it passes campaignCharacters to ParticipantManager)
function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) {
const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null);
const {data: activeDisplayInfoFromHook } = useFirestoreDocument(getActiveDisplayDocPath());
@ -543,18 +584,74 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const participants = encounter.participants || [];
// Pre-fill Max HP when a character is selected
useEffect(() => {
if (participantType === 'character' && selectedCharacterId) {
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
if (selectedChar && selectedChar.defaultMaxHp) {
setMaxHp(selectedChar.defaultMaxHp);
} else {
setMaxHp(10); // Fallback if no default HP
}
} else if (participantType === 'monster') {
setMaxHp(10); // Reset for monsters
}
}, [selectedCharacterId, participantType, campaignCharacters]);
const handleAddParticipant = async () => {
if (!db || (participantType === 'monster' && !participantName.trim()) || (participantType === 'character' && !selectedCharacterId)) return;
let nameToAdd = participantName.trim();
let characterDefaultHp = maxHp; // Use current maxHp input by default
if (participantType === 'character') {
const character = campaignCharacters.find(c => c.id === selectedCharacterId);
if (!character) { console.error("Selected character not found"); return; }
if (participants.some(p => p.type === 'character' && p.originalCharacterId === selectedCharacterId)) { alert(`${character.name} is already in this encounter.`); return; }
nameToAdd = character.name;
characterDefaultHp = character.defaultMaxHp || maxHp; // Use campaign default HP if available
}
const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: parseInt(initiative, 10) || 0, maxHp: parseInt(maxHp, 10) || 1, currentHp: parseInt(maxHp, 10) || 1, conditions: [], isActive: true, };
const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: parseInt(initiative, 10) || 0, maxHp: parseInt(characterDefaultHp, 10) || 1, currentHp: parseInt(characterDefaultHp, 10) || 1, conditions: [], isActive: true, };
try { await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] }); setParticipantName(''); setInitiative(10); setMaxHp(10); setSelectedCharacterId(''); } catch (err) { console.error("Error adding participant:", err); }
};
const handleAddAllCampaignCharacters = async () => {
if (!db || !campaignCharacters || campaignCharacters.length === 0) return;
const existingParticipantOriginalIds = participants
.filter(p => p.type === 'character' && p.originalCharacterId)
.map(p => p.originalCharacterId);
const newParticipants = campaignCharacters
.filter(char => !existingParticipantOriginalIds.includes(char.id)) // Only add if not already in encounter
.map(char => ({
id: generateId(),
name: char.name,
type: 'character',
originalCharacterId: char.id,
initiative: 10, // Default initiative
maxHp: char.defaultMaxHp || 10,
currentHp: char.defaultMaxHp || 10,
conditions: [],
isActive: true,
}));
if (newParticipants.length === 0) {
alert("All campaign characters are already in this encounter."); // TODO: Better notification
return;
}
try {
await updateDoc(doc(db, encounterPath), {
participants: [...participants, ...newParticipants]
});
console.log(`Added ${newParticipants.length} characters to the encounter.`);
} catch (err) {
console.error("Error adding all campaign characters:", err);
}
};
const handleUpdateParticipant = async (updatedData) => {
if (!db || !editingParticipant) return;
const { flavorText, ...restOfData } = updatedData;
@ -618,7 +715,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
return (
<>
<div className="p-3 bg-slate-800 rounded-md mt-4">
<h4 className="text-lg font-medium text-sky-200 mb-3">Participants</h4>
<div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-medium text-sky-200">Add Participants</h4>
<button onClick={handleAddAllCampaignCharacters} className="px-3 py-1.5 text-xs font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors flex items-center" disabled={!campaignCharacters || campaignCharacters.length === 0}>
<Users2 size={16} className="mr-1.5"/> Add All From Campaign
</button>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 p-3 bg-slate-700 rounded">
<div>
<label className="block text-sm font-medium text-slate-300">Type</label>
@ -628,7 +730,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
</select>
</div>
{participantType === 'monster' && (<div className="md:col-span-2"> <label className="block text-sm font-medium text-slate-300">Monster Name</label><input type="text" value={participantName} onChange={(e) => setParticipantName(e.target.value)} placeholder="e.g., Dire Wolf" className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" /></div>)}
{participantType === 'character' && (<div><label className="block text-sm font-medium text-slate-300">Select Character</label><select value={selectedCharacterId} onChange={(e) => setSelectedCharacterId(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"><option value="">-- Select from Campaign --</option>{campaignCharacters.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}</select></div>)}
{participantType === 'character' && (<div><label className="block text-sm font-medium text-slate-300">Select Character</label><select value={selectedCharacterId} onChange={(e) => setSelectedCharacterId(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"><option value="">-- Select from Campaign --</option>{campaignCharacters.map(c => <option key={c.id} value={c.id}>{c.name} (HP: {c.defaultMaxHp || 'N/A'})</option>)}</select></div>)}
<div><label className="block text-sm font-medium text-slate-300">Initiative</label><input type="number" value={initiative} onChange={(e) => setInitiative(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" /></div>
<div><label className="block text-sm font-medium text-slate-300">Max HP</label><input type="number" value={maxHp} onChange={(e) => setMaxHp(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md shadow-sm focus:outline-none focus:ring-sky-500 focus:border-sky-500 sm:text-sm text-white" /></div>
<div className="md:col-span-2 flex justify-end"><button type="submit" className="px-4 py-2 text-sm font-medium text-white bg-green-500 hover:bg-green-600 rounded-md transition-colors flex items-center"><PlusCircle size={18} className="mr-1" /> Add to Encounter</button></div>
@ -690,7 +792,6 @@ function EditParticipantModal({ participant, onClose, onSave }) {
function InitiativeControls({ campaignId, encounter, encounterPath }) {
const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false);
const handleStartEncounter = async () => {
if (!db ||!encounter.participants || encounter.participants.length === 0) { alert("Add participants first."); return; }
const activePs = encounter.participants.filter(p => p.isActive);
@ -705,33 +806,20 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
console.log("Encounter started and set as active display.");
} catch (err) { console.error("Error starting encounter:", err); }
};
const handleTogglePause = async () => {
if (!db || !encounter || !encounter.isStarted) return;
const newPausedState = !encounter.isPaused;
let newTurnOrderIds = encounter.turnOrderIds;
if (!newPausedState && encounter.isPaused) {
const activeParticipants = encounter.participants.filter(p => p.isActive);
const sortedActiveParticipants = [...activeParticipants].sort((a, b) => {
if (a.initiative === b.initiative) {
const indexA = encounter.participants.findIndex(p => p.id === a.id);
const indexB = encounter.participants.findIndex(p => p.id === b.id);
return indexA - indexB;
}
if (a.initiative === b.initiative) { const indexA = encounter.participants.findIndex(p => p.id === a.id); const indexB = encounter.participants.findIndex(p => p.id === b.id); return indexA - indexB; }
return b.initiative - a.initiative;
});
newTurnOrderIds = sortedActiveParticipants.map(p => p.id);
}
try {
await updateDoc(doc(db, encounterPath), {
isPaused: newPausedState,
turnOrderIds: newTurnOrderIds
});
console.log(`Encounter ${newPausedState ? 'paused' : 'resumed'}.`);
} catch (err) { console.error("Error toggling pause state:", err); }
try { await updateDoc(doc(db, encounterPath), { isPaused: newPausedState, turnOrderIds: newTurnOrderIds }); console.log(`Encounter ${newPausedState ? 'paused' : 'resumed'}.`); } catch (err) { console.error("Error toggling pause state:", err); }
};
const handleNextTurn = async () => {
if (!db ||!encounter.isStarted || encounter.isPaused || !encounter.currentTurnParticipantId || !encounter.turnOrderIds || encounter.turnOrderIds.length === 0) return;
const activePsInOrder = encounter.turnOrderIds.map(id => encounter.participants.find(p => p.id === id && p.isActive)).filter(Boolean);
@ -742,7 +830,6 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
if (nextIndex === 0 && currentIndex !== -1) nextRound += 1;
try { await updateDoc(doc(db, encounterPath), { currentTurnParticipantId: activePsInOrder[nextIndex].id, round: nextRound }); } catch (err) { console.error("Error advancing turn:", err); }
};
const requestEndEncounter = () => { setShowEndEncounterConfirm(true); };
const confirmEndEncounter = async () => {
if (!db) return;
@ -753,7 +840,6 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
} catch (err) { console.error("Error ending encounter:", err); }
setShowEndEncounterConfirm(false);
};
if (!encounter || !encounter.participants) return null;
return (
<>
@ -788,7 +874,6 @@ function DisplayView() {
const [encounterError, setEncounterError] = useState(null);
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
useEffect(() => {
if (!db) { setEncounterError("Firestore not available."); setIsLoadingEncounter(false); setIsPlayerDisplayActive(false); return; }
let unsubscribeEncounter;
@ -809,10 +894,8 @@ function DisplayView() {
} else if (!isLoadingActiveDisplay) { setActiveEncounterData(null); setCampaignBackgroundUrl(''); setIsPlayerDisplayActive(false); setIsLoadingEncounter(false); }
return () => { if (unsubscribeEncounter) unsubscribeEncounter(); if (unsubscribeCampaign) unsubscribeCampaign(); };
}, [activeDisplayData, isLoadingActiveDisplay]);
if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) { return <div className="text-center py-10 text-2xl text-slate-300">Loading Player Display...</div>; }
if (activeDisplayError || (isPlayerDisplayActive && encounterError)) { return <div className="text-center py-10 text-2xl text-red-400">{activeDisplayError || encounterError}</div>; }
if (!isPlayerDisplayActive || !activeEncounterData) {
return (
<div className="min-h-screen bg-black text-slate-400 flex flex-col items-center justify-center p-4 text-center">
@ -821,7 +904,6 @@ function DisplayView() {
<p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p>
</div>);
}
const { name, participants, round, currentTurnParticipantId, isStarted, isPaused } = activeEncounterData;
let participantsToRender = [];
if (participants) {