Adding random inits

This commit is contained in:
Robert Johnson 2025-05-26 22:31:43 -04:00
parent d631545570
commit 6adcd0f8e0

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
import { initializeApp } from 'firebase/app'; import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, setDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore'; 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 --- // --- Firebase Configuration ---
const firebaseConfig = { const firebaseConfig = {
@ -48,6 +48,7 @@ const getActiveDisplayDocPath = () => `${PUBLIC_DATA_PATH}/activeDisplay/status`
// --- Helper Functions --- // --- Helper Functions ---
const generateId = () => crypto.randomUUID(); const generateId = () => crypto.randomUUID();
const rollD20 = () => Math.floor(Math.random() * 20) + 1;
// --- Custom Hooks for Firestore --- // --- Custom Hooks for Firestore ---
function useFirestoreDocument(docPath) { function useFirestoreDocument(docPath) {
@ -213,7 +214,7 @@ function App() {
{!isAuthReady && !error && <p>Authenticating...</p>} {!isAuthReady && !error && <p>Authenticating...</p>}
</main> </main>
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8"> <footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
TTRPG Initiative Tracker v0.1.26 TTRPG Initiative Tracker v0.1.28
</footer> </footer>
</div> </div>
); );
@ -376,35 +377,48 @@ function CreateCampaignForm({ onCreate, onCancel }) {
function CharacterManager({ campaignId, campaignCharacters }) { function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState(''); const [characterName, setCharacterName] = useState('');
const [defaultMaxHp, setDefaultMaxHp] = useState(10); // New state for default HP const [defaultMaxHp, setDefaultMaxHp] = useState(10);
const [editingCharacter, setEditingCharacter] = useState(null); // { id, name, defaultMaxHp } const [defaultInitMod, setDefaultInitMod] = useState(0);
const [editingCharacter, setEditingCharacter] = useState(null);
const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false); const [showDeleteCharConfirm, setShowDeleteCharConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
const handleAddCharacter = async () => { const handleAddCharacter = async () => {
if (!db ||!characterName.trim() || !campaignId) return; if (!db ||!characterName.trim() || !campaignId) return;
const hp = parseInt(defaultMaxHp, 10); const hp = parseInt(defaultMaxHp, 10);
const initMod = parseInt(defaultInitMod, 10);
if (isNaN(hp) || hp <= 0) { 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; 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 { try {
await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] }); await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: [...campaignCharacters, newCharacter] });
setCharacterName(''); setCharacterName('');
setDefaultMaxHp(10); // Reset default HP input setDefaultMaxHp(10);
setDefaultInitMod(0);
} catch (err) { console.error("Error adding character:", err); } } 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; if (!db ||!newName.trim() || !campaignId) return;
const hp = parseInt(newDefaultMaxHp, 10); const hp = parseInt(newDefaultMaxHp, 10);
const initMod = parseInt(newDefaultInitMod, 10);
if (isNaN(hp) || hp <= 0) { 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.");
setEditingCharacter(null); // Close edit mode on invalid input setEditingCharacter(null);
return; 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 { try {
await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters }); await updateDoc(doc(db, getCampaignDocPath(campaignId)), { players: updatedCharacters });
setEditingCharacter(null); setEditingCharacter(null);
@ -424,33 +438,38 @@ function CharacterManager({ campaignId, campaignCharacters }) {
<> <>
<div className="p-4 bg-slate-800 rounded-lg shadow"> <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> <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 flex-wrap gap-2 mb-4 items-end"> <form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end">
<div className="flex-grow"> <div className="sm:col-span-1">
<label htmlFor="characterName" className="block text-xs font-medium text-slate-400">Name</label> <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" /> <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>
<div className="w-24"> <div className="w-full sm:w-auto">
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-slate-400">Default HP</label> <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" /> <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> </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> <div className="w-full sm:w-auto">
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-slate-400">Init Mod</label>
<input type="number" id="defaultInitMod" value={defaultInitMod} onChange={(e) => 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" />
</div>
<button type="submit" className="sm:col-span-3 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-sky-500 hover:bg-sky-600 rounded-md transition-colors flex items-center justify-center"><PlusCircle size={18} className="mr-1" /> Add Character</button>
</form> </form>
{campaignCharacters.length === 0 && <p className="text-sm text-slate-400">No characters added.</p>} {campaignCharacters.length === 0 && <p className="text-sm text-slate-400">No characters added.</p>}
<ul className="space-y-2"> <ul className="space-y-2">
{campaignCharacters.map(character => ( {campaignCharacters.map(character => (
<li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md"> <li key={character.id} className="flex justify-between items-center p-3 bg-slate-700 rounded-md">
{editingCharacter && editingCharacter.id === character.id ? ( {editingCharacter && editingCharacter.id === character.id ? (
<form onSubmit={(e) => {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp);}} className="flex-grow flex gap-2 items-center"> <form onSubmit={(e) => {e.preventDefault(); handleUpdateCharacter(character.id, editingCharacter.name, editingCharacter.defaultMaxHp, editingCharacter.defaultInitMod);}} className="flex-grow flex flex-wrap 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="text" value={editingCharacter.name} onChange={(e) => 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"/>
<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"/> <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" title="Default Max HP"/>
<input type="number" value={editingCharacter.defaultInitMod} onChange={(e) => 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"/>
<button type="submit" className="p-1 text-green-400 hover:text-green-300"><Save size={18}/></button> <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> <button type="button" onClick={() => setEditingCharacter(null)} className="p-1 text-slate-400 hover:text-slate-200"><XCircle size={18}/></button>
</form> </form>
) : ( ) : (
<> <>
<span className="text-slate-100">{character.name} <span className="text-xs text-slate-400">(HP: {character.defaultMaxHp || 'N/A'})</span></span> <span className="text-slate-100">{character.name} <span className="text-xs text-slate-400">(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {character.defaultInitMod !== undefined ? (character.defaultInitMod >= 0 ? `+${character.defaultInitMod}` : character.defaultInitMod) : 'N/A'})</span></span>
<div className="flex space-x-2"> <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={() => setEditingCharacter({id: character.id, name: character.name, defaultMaxHp: character.defaultMaxHp || 10, defaultInitMod: character.defaultInitMod || 0})} 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> <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> </div>
</> </>
@ -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 }) { function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) {
const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null); const {data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(campaignId ? getEncountersCollectionPath(campaignId) : null);
const {data: activeDisplayInfoFromHook } = useFirestoreDocument(getActiveDisplayDocPath()); const {data: activeDisplayInfoFromHook } = useFirestoreDocument(getActiveDisplayDocPath());
@ -576,82 +588,77 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
const [participantName, setParticipantName] = useState(''); const [participantName, setParticipantName] = useState('');
const [participantType, setParticipantType] = useState('monster'); const [participantType, setParticipantType] = useState('monster');
const [selectedCharacterId, setSelectedCharacterId] = useState(''); const [selectedCharacterId, setSelectedCharacterId] = useState('');
const [initiative, setInitiative] = useState(10); const [monsterInitMod, setMonsterInitMod] = useState(2); // Default monster init mod
const [maxHp, setMaxHp] = useState(10); const [maxHp, setMaxHp] = useState(10);
const [editingParticipant, setEditingParticipant] = useState(null); const [editingParticipant, setEditingParticipant] = useState(null);
const [hpChangeValues, setHpChangeValues] = useState({}); const [hpChangeValues, setHpChangeValues] = useState({});
const [draggedItemId, setDraggedItemId] = useState(null); const [draggedItemId, setDraggedItemId] = useState(null);
const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false); const [showDeleteParticipantConfirm, setShowDeleteParticipantConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
const [lastRollDetails, setLastRollDetails] = useState(null); // { name, roll, mod, total }
const participants = encounter.participants || []; const participants = encounter.participants || [];
// Pre-fill Max HP when a character is selected
useEffect(() => { useEffect(() => {
if (participantType === 'character' && selectedCharacterId) { if (participantType === 'character' && selectedCharacterId) {
const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId); const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
if (selectedChar && selectedChar.defaultMaxHp) { if (selectedChar && selectedChar.defaultMaxHp) {
setMaxHp(selectedChar.defaultMaxHp); setMaxHp(selectedChar.defaultMaxHp);
} else { } else {
setMaxHp(10); // Fallback if no default HP setMaxHp(10);
} }
} else if (participantType === 'monster') { } else if (participantType === 'monster') {
setMaxHp(10); // Reset for monsters setMaxHp(10);
setMonsterInitMod(2); // Reset monster init mod when switching to monster
} }
}, [selectedCharacterId, participantType, campaignCharacters]); }, [selectedCharacterId, participantType, campaignCharacters]);
const handleAddParticipant = async () => { const handleAddParticipant = async () => {
if (!db || (participantType === 'monster' && !participantName.trim()) || (participantType === 'character' && !selectedCharacterId)) return; if (!db || (participantType === 'monster' && !participantName.trim()) || (participantType === 'character' && !selectedCharacterId)) return;
let nameToAdd = participantName.trim(); 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') { if (participantType === 'character') {
const character = campaignCharacters.find(c => c.id === selectedCharacterId); const character = campaignCharacters.find(c => c.id === selectedCharacterId);
if (!character) { console.error("Selected character not found"); return; } 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; } if (participants.some(p => p.type === 'character' && p.originalCharacterId === selectedCharacterId)) { alert(`${character.name} is already in this encounter.`); return; }
nameToAdd = character.name; 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 () => { const handleAddAllCampaignCharacters = async () => {
if (!db || !campaignCharacters || campaignCharacters.length === 0) return; if (!db || !campaignCharacters || campaignCharacters.length === 0) return;
const existingParticipantOriginalIds = participants const existingParticipantOriginalIds = participants.filter(p => p.type === 'character' && p.originalCharacterId).map(p => p.originalCharacterId);
.filter(p => p.type === 'character' && p.originalCharacterId)
.map(p => p.originalCharacterId);
const newParticipants = campaignCharacters const newParticipants = campaignCharacters
.filter(char => !existingParticipantOriginalIds.includes(char.id)) // Only add if not already in encounter .filter(char => !existingParticipantOriginalIds.includes(char.id))
.map(char => ({ .map(char => {
id: generateId(), const initiativeRoll = rollD20();
name: char.name, const modifier = char.defaultInitMod || 0;
type: 'character', const finalInitiative = initiativeRoll + modifier;
originalCharacterId: char.id, console.log(`Adding ${char.name}: Rolled ${initiativeRoll} + ${modifier} (mod) = ${finalInitiative} init`);
initiative: 10, // Default initiative return { id: generateId(), name: char.name, type: 'character', originalCharacterId: char.id, initiative: finalInitiative, maxHp: char.defaultMaxHp || 10, currentHp: char.defaultMaxHp || 10, conditions: [], isActive: true, };
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.`); if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); return; }
} catch (err) { try { await updateDoc(doc(db, encounterPath), { participants: [...participants, ...newParticipants] }); console.log(`Added ${newParticipants.length} characters to the encounter.`); }
console.error("Error adding all campaign characters:", err); catch (err) { console.error("Error adding all campaign characters:", err); }
}
}; };
const handleUpdateParticipant = async (updatedData) => { const handleUpdateParticipant = async (updatedData) => {
if (!db || !editingParticipant) return; if (!db || !editingParticipant) return;
const { flavorText, ...restOfData } = updatedData; const { flavorText, ...restOfData } = updatedData;
@ -718,23 +725,38 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h4 className="text-lg font-medium text-sky-200">Add Participants</h4> <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}> <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 <Users2 size={16} className="mr-1.5"/><Dices size={16} className="mr-1.5"/> Add All (Roll Init)
</button> </button>
</div> </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"> <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> <div>
<label className="block text-sm font-medium text-slate-300">Type</label> <label className="block text-sm font-medium text-slate-300">Type</label>
<select value={participantType} onChange={(e) => setParticipantType(e.target.value)} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white"> <select value={participantType} onChange={(e) => {setParticipantType(e.target.value); setSelectedCharacterId('');}} className="mt-1 w-full px-3 py-2 bg-slate-600 border-slate-500 rounded text-white">
<option value="monster">Monster</option> <option value="monster">Monster</option>
<option value="character">Character</option> <option value="character">Character</option>
</select> </select>
</div> </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 === 'monster' && (
{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">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>
<div>
<label className="block text-sm font-medium text-slate-300">Monster Init Mod</label>
<input type="number" value={monsterInitMod} onChange={(e) => 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" />
</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'}, Mod: {c.defaultInitMod >=0 ? `+${c.defaultInitMod}` : c.defaultInitMod})</option>)}</select></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><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> <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"><Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)</button></div>
</form> </form>
{lastRollDetails && (
<p className="text-sm text-green-400 mt-2 mb-2 text-center">
{lastRollDetails.name}: Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative
</p>
)}
{participants.length === 0 && <p className="text-sm text-slate-400">No participants.</p>} {participants.length === 0 && <p className="text-sm text-slate-400">No participants.</p>}
<ul className="space-y-2"> <ul className="space-y-2">
{sortedAdminParticipants.map((p) => { {sortedAdminParticipants.map((p) => {