From 6adcd0f8e082939c44e2c8346568066bbfab6217 Mon Sep 17 00:00:00 2001 From: robert Date: Mon, 26 May 2025 22:31:43 -0400 Subject: [PATCH] Adding random inits --- src/App.js | 168 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 95 insertions(+), 73 deletions(-) diff --git a/src/App.js b/src/App.js index f0d65f6..3bdbca4 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 } from 'lucide-react'; // Added Users2 for Add All +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 = { @@ -48,6 +48,7 @@ const getActiveDisplayDocPath = () => `${PUBLIC_DATA_PATH}/activeDisplay/status` // --- Helper Functions --- const generateId = () => crypto.randomUUID(); +const rollD20 = () => Math.floor(Math.random() * 20) + 1; // --- Custom Hooks for Firestore --- function useFirestoreDocument(docPath) { @@ -213,7 +214,7 @@ function App() { {!isAuthReady && !error &&

Authenticating...

} ); @@ -376,35 +377,48 @@ function CreateCampaignForm({ onCreate, onCancel }) { function CharacterManager({ campaignId, campaignCharacters }) { const [characterName, setCharacterName] = useState(''); - const [defaultMaxHp, setDefaultMaxHp] = useState(10); // New state for default HP - const [editingCharacter, setEditingCharacter] = useState(null); // { id, name, defaultMaxHp } + const [defaultMaxHp, setDefaultMaxHp] = useState(10); + const [defaultInitMod, setDefaultInitMod] = useState(0); + 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); + const initMod = parseInt(defaultInitMod, 10); if (isNaN(hp) || hp <= 0) { - alert("Please enter a valid positive number for Default Max HP."); // TODO: Replace with better notification + alert("Please enter a valid positive number for Default Max HP."); return; } - const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp }; + if (isNaN(initMod)) { + alert("Please enter a valid number for Default Initiative Modifier."); + return; + } + const newCharacter = { id: generateId(), name: characterName.trim(), defaultMaxHp: hp, defaultInitMod: initMod }; try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); setCharacterName(''); - setDefaultMaxHp(10); // Reset default HP input + setDefaultMaxHp(10); + setDefaultInitMod(0); } catch (err) { console.error("Error adding character:", err); } }; - const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp) => { + const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod) => { if (!db ||!newName.trim() || !campaignId) return; const hp = parseInt(newDefaultMaxHp, 10); + const initMod = parseInt(newDefaultInitMod, 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 + alert("Please enter a valid positive number for Default Max HP."); + setEditingCharacter(null); return; } - const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp } : c); + if (isNaN(initMod)) { + alert("Please enter a valid number for Default Initiative Modifier."); + setEditingCharacter(null); + return; + } + const updatedCharacters = campaignCharacters.map(c => c.id === characterId ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod } : c); try { await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); setEditingCharacter(null); @@ -424,33 +438,38 @@ function CharacterManager({ campaignId, campaignCharacters }) { <>

Campaign Characters

-
{ e.preventDefault(); handleAddCharacter(); }} className="flex flex-wrap gap-2 mb-4 items-end"> -
+ { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 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" />
- +
+ + setDefaultInitMod(e.target.value)} placeholder="+0" 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.

}
    {campaignCharacters.map(character => (
  • {editingCharacter && editingCharacter.id === character.id ? ( -
    {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp);}} className="flex-grow flex gap-2 items-center"> - setEditingCharacter({...editingCharacter, name: e.target.value})} className="flex-grow px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/> - setEditingCharacter({...editingCharacter, defaultMaxHp: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/> + {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp, editingCharacter.defaultInitMod);}} className="flex-grow flex flex-wrap gap-2 items-center"> + setEditingCharacter({...editingCharacter, name: e.target.value})} className="flex-grow min-w-[100px] px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white"/> + setEditingCharacter({...editingCharacter, defaultMaxHp: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Max HP"/> + setEditingCharacter({...editingCharacter, defaultInitMod: e.target.value})} className="w-20 px-2 py-1 bg-slate-600 border border-slate-500 rounded-md text-white" title="Default Init Mod"/>
    ) : ( <> - {character.name} (HP: {character.defaultMaxHp || 'N/A'}) + {character.name} (HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {character.defaultInitMod !== undefined ? (character.defaultInitMod >= 0 ? `+${character.defaultInitMod}` : character.defaultInitMod) : 'N/A'})
    - +
    @@ -464,13 +483,6 @@ 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()); @@ -576,82 +588,77 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { const [participantName, setParticipantName] = useState(''); const [participantType, setParticipantType] = useState('monster'); const [selectedCharacterId, setSelectedCharacterId] = useState(''); - const [initiative, setInitiative] = useState(10); + const [monsterInitMod, setMonsterInitMod] = useState(2); // Default monster init mod const [maxHp, setMaxHp] = useState(10); const [editingParticipant, setEditingParticipant] = useState(null); const [hpChangeValues, setHpChangeValues] = useState({}); const [draggedItemId, setDraggedItemId] = useState(null); const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); + const [lastRollDetails, setLastRollDetails] = useState(null); // { name, roll, mod, total } 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 + setMaxHp(10); } } else if (participantType === 'monster') { - setMaxHp(10); // Reset for monsters + setMaxHp(10); + setMonsterInitMod(2); // Reset monster init mod when switching to monster } }, [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 + const initiativeRoll = rollD20(); + let modifier = 0; + let finalInitiative; + let currentMaxHp = parseInt(maxHp, 10) || 10; 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 + currentMaxHp = character.defaultMaxHp || currentMaxHp; + modifier = character.defaultInitMod || 0; + finalInitiative = initiativeRoll + modifier; + } else { // Monster + modifier = parseInt(monsterInitMod, 10) || 0; // Use state for monster mod + finalInitiative = initiativeRoll + modifier; } - 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 newParticipant = { id: generateId(), name: nameToAdd, type: participantType, originalCharacterId: participantType === 'character' ? selectedCharacterId : null, initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp, conditions: [], isActive: true, }; + try { + await updateDoc(doc(db, encounterPath), { participants: [...participants, newParticipant] }); + setLastRollDetails({ name: nameToAdd, roll: initiativeRoll, mod: modifier, total: finalInitiative }); + setTimeout(() => setLastRollDetails(null), 5000); // Clear after 5 seconds + setParticipantName(''); setMaxHp(10); setSelectedCharacterId(''); setMonsterInitMod(2); + } 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 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] + .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, }; }); - console.log(`Added ${newParticipants.length} characters to the encounter.`); - } catch (err) { - console.error("Error adding all campaign characters:", err); - } + if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); 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; @@ -718,23 +725,38 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {

    Add Participants

    { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4 p-3 bg-slate-700 rounded">
    - {setParticipantType(e.target.value); setSelectedCharacterId('');}} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white">
    - {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' && (
    )} -
    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" />
    + {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" /> +
    +
    + + setMonsterInitMod(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" /> +
    + + )} + {participantType === 'character' && (
    )}
    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" />
    -
    +
    + {lastRollDetails && ( +

    + {lastRollDetails.name}: Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative +

    + )} {participants.length === 0 &&

    No participants.

    }
      {sortedAdminParticipants.map((p) => { @@ -768,7 +790,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) { function EditParticipantModal({ participant, onClose, onSave }) { const [name, setName] = useState(participant.name); - const [initiative, setInitiative] = useState(participant.initiative); + const [initiative, setInitiative] = useState(participant.initiative); const [currentHp, setCurrentHp] = useState(participant.currentHp); const [maxHp, setMaxHp] = useState(participant.maxHp); const handleSubmit = (e) => { e.preventDefault(); onSave({ name: name.trim(), initiative: parseInt(initiative, 10), currentHp: parseInt(currentHp, 10), maxHp: parseInt(maxHp, 10), }); };