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
-
{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
+
+