diff --git a/src/App.js b/src/App.js
index 880f427..836d432 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, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices } from 'lucide-react'; // ImageIcon was already removed, ensuring it stays 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, Dices } from 'lucide-react';
// --- Firebase Configuration ---
const firebaseConfig = {
@@ -88,8 +88,6 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
-
- // Memoize the stringified queryConstraints. This is the key to stabilizing the effect's dependency.
const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);
useEffect(() => {
@@ -101,10 +99,7 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
}
setIsLoading(true);
setError(null);
-
- // queryConstraints is used here to build the query.
const q = query(collection(db, collectionPath), ...queryConstraints);
-
const unsubscribe = onSnapshot(q, (snapshot) => {
const items = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setData(items);
@@ -115,14 +110,9 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
setIsLoading(false);
setData([]);
});
-
return () => unsubscribe();
- // The effect depends on collectionPath and the memoized queryString.
- // This prevents re-running the effect if queryConstraints is a new array reference
- // but its content (and thus queryString) is the same.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionPath, queryString]);
-
return { data, isLoading, error };
}
@@ -224,14 +214,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 (
@@ -252,7 +241,6 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
}
// --- Admin View Component ---
-// ... (AdminView remains the same)
function AdminView({ userId }) {
const { data: campaignsData, isLoading: isLoadingCampaigns } = useFirestoreCollection(getCampaignsCollectionPath());
const { data: initialActiveInfoData } = useFirestoreDocument(getActiveDisplayDocPath());
@@ -344,7 +332,6 @@ function AdminView({ userId }) {
setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
{campaign.name}
- {/* ImageIcon display removed from here */}
);
@@ -366,10 +353,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.
-// The changes for ESLint warnings were primarily in the imports at the top of App.js and in the useFirestoreCollection hook.
-
function CreateCampaignForm({ onCreate, onCancel }) {
const [name, setName] = useState('');
const [backgroundUrl, setBackgroundUrl] = useState('');
@@ -392,7 +375,6 @@ function CreateCampaignForm({ onCreate, onCancel }) {
);
}
-
function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState('');
const [defaultMaxHp, setDefaultMaxHp] = useState(10);
@@ -400,6 +382,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const [editingCharacter, setEditingCharacter] = useState(null);
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
+
const handleAddCharacter = async () => {
if (!db ||!characterName.trim() || !campaignId) return;
const hp = parseInt(defaultMaxHp, 10);
@@ -582,6 +565,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const [selectedCharacterId, setSelectedCharacterId] = useState('');
const [monsterInitMod, setMonsterInitMod] = useState(2);
const [maxHp, setMaxHp] = useState(10);
+ const [isNpc, setIsNpc] = useState(false); // New state for NPC checkbox
const [editingParticipant, setEditingParticipant] = useState(null);
const [hpChangeValues, setHpChangeValues] = useState({});
const [draggedItemId, setDraggedItemId] = useState(null);
@@ -595,7 +579,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
if (participantType === 'character' && selectedCharacterId) {
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
if (selectedChar && selectedChar.defaultMaxHp) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); }
- } else if (participantType === 'monster') { setMaxHp(10); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); }
+ setIsNpc(false); // Characters cannot be NPCs in this model
+ } else if (participantType === 'monster') {
+ setMaxHp(10);
+ setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
+ // setIsNpc(false); // Reset NPC status when switching to monster, or keep last state? Let's reset.
+ }
}, [selectedCharacterId, participantType, campaignCharacters]);
const handleAddParticipant = async () => {
@@ -605,6 +594,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
let modifier = 0;
let finalInitiative;
let currentMaxHp = parseInt(maxHp, 10) || 10;
+ let participantIsNpc = false;
if (participantType === 'character') {
const character = campaignCharacters.find(c => c.id === selectedCharacterId);
@@ -614,40 +604,48 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
currentMaxHp = character.defaultMaxHp || currentMaxHp;
modifier = character.defaultInitMod || 0;
finalInitiative = initiativeRoll + modifier;
- } else {
+ } else { // Monster
modifier = parseInt(monsterInitMod, 10) || 0;
finalInitiative = initiativeRoll + modifier;
+ participantIsNpc = isNpc; // Use the state of the NPC checkbox
}
- const newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, conditions: [], isActive: true, };
+ const newParticipant = {
+ id: generateId(), name: nameToAdd, type: participantType,
+ originalCharacterId: participantType === 'character' ? selectedCharacterId : null,
+ initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp,
+ isNpc: participantType === 'monster' ? participantIsNpc : false, // Store isNpc for monsters
+ conditions: [], isActive: true,
+ };
try {
await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] });
- setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative });
+ setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative, type: participantIsNpc ? 'NPC' : participantType });
setTimeout(() => setLastRollDetails(null), 5000);
- setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
+ setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); setIsNpc(false);
} 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);
+ let consoleRollLog = "Adding all campaign characters:\n";
const newParticipants = campaignCharacters
.filter(char => !existingParticipantOriginalIds.includes(char.id))
.map(char => {
const initiativeRoll = rollD20();
const modifier = char.defaultInitMod || 0;
const finalInitiative = initiativeRoll + modifier;
- console.log(`Adding ${char.name}: Rolled ${initiativeRoll} + ${modifier} (mod) = ${finalInitiative} init`);
- return { id: generateId(), name: char.name, type: 'character', originalCharacterId: char.id, initiative: finalInitiative, maxHp: char.defaultMaxHp || 10, currentHp: char.defaultMaxHp || 10, conditions: [], isActive: true, };
+ consoleRollLog += `${char.name}: Rolled D20 (${initiativeRoll}) + ${modifier} (mod) = ${finalInitiative} init\n`;
+ return { id: generateId(), name: char.name, type: 'character', originalCharacterId: char.id, initiative: finalInitiative, maxHp: char.defaultMaxHp || 10, currentHp: char.defaultMaxHp || 10, conditions: [], isActive: true, isNpc: false }; // Characters are not NPCs
});
if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); return; }
+ console.log(consoleRollLog);
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;
- const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...restOfData } : p );
+ const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...updatedData } : p ); // updatedData now includes isNpc
try { await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); setEditingParticipant(null); } catch (err) { console.error("Error updating participant:", err); }
};
const requestDeleteParticipant = (participantId, participantName) => { setItemToDelete({ id: participantId, name: participantName, type: 'participant' }); setShowDeleteParticipantConfirm(true); };
@@ -713,10 +711,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
Add All (Roll Init)
-