Added defautl HP values.
This commit is contained in:
parent
ad11bbc648
commit
d631545570
176
src/App.js
176
src/App.js
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user