From d63154557043c136bc0af58c0edce2b982d00175 Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 26 May 2025 21:48:28 -0400 Subject: [PATCH] Added defautl HP values. --- src/App.js | 176 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 129 insertions(+), 47 deletions(-) diff --git a/src/App.js b/src/App.js index a8b07f1..f0d65f6 100644 --- a/src/App.js +++ b/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 &&

Authenticating...

} ); } // --- 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 }) {
setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>

{campaign.name}

- {/* ImageIcon display removed from here */}
); @@ -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 ( <>

Campaign Characters

-
{ e.preventDefault(); handleAddCharacter(); }} className="flex gap-2 mb-4"> - 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" /> - + { e.preventDefault(); handleAddCharacter(); }} className="flex flex-wrap gap-2 mb-4 items-end"> +
+ + 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" /> +
+
+ + 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" /> +
+
{campaignCharacters.length === 0 &&

No characters added.

} @@ -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 ( <>
-

Participants

+
+

Add Participants

+ +
{ e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 p-3 bg-slate-700 rounded">
@@ -628,7 +730,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{participantType === 'monster' && (
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" />
)} - {participantType === 'character' && (
)} + {participantType === 'character' && (
)}
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" />
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" />
@@ -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
Loading Player Display...
; } if (activeDisplayError || (isPlayerDisplayActive && encounterError)) { return
{activeDisplayError || encounterError}
; } - if (!isPlayerDisplayActive || !activeEncounterData) { return (
@@ -821,7 +904,6 @@ function DisplayView() {

The Dungeon Master has not activated an encounter for display.

); } - const { name, participants, round, currentTurnParticipantId, isStarted, isPaused } = activeEncounterData; let participantsToRender = []; if (participants) {