Tightened up the UI a bit.
This commit is contained in:
86
src/App.js
86
src/App.js
@ -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, Dices } from 'lucide-react';
|
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 removed
|
||||||
|
|
||||||
// --- Firebase Configuration ---
|
// --- Firebase Configuration ---
|
||||||
const firebaseConfig = {
|
const firebaseConfig = {
|
||||||
@ -111,7 +111,7 @@ function useFirestoreCollection(collectionPath, queryConstraints = []) {
|
|||||||
setData([]);
|
setData([]);
|
||||||
});
|
});
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [collectionPath, queryString]);
|
}, [collectionPath, queryString]);
|
||||||
return { data, isLoading, error };
|
return { data, isLoading, error };
|
||||||
}
|
}
|
||||||
@ -214,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.28
|
TTRPG Initiative Tracker v0.1.30
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -327,12 +327,12 @@ function AdminView({ userId }) {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{campaigns.map(campaign => {
|
{campaigns.map(campaign => {
|
||||||
const cardStyle = campaign.playerDisplayBackgroundUrl ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`} : {};
|
const cardStyle = campaign.playerDisplayBackgroundUrl ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})`} : {};
|
||||||
const cardClasses = `p-4 rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-sky-400' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-slate-700 hover:bg-slate-600' : 'hover:shadow-xl'}`;
|
const cardClasses = `h-36 flex flex-col justify-between rounded-lg shadow-md cursor-pointer transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-sky-400' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-slate-700 hover:bg-slate-600' : 'hover:shadow-xl'}`;
|
||||||
return (
|
return (
|
||||||
<div key={campaign.id} onClick={() => setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
|
<div key={campaign.id} onClick={() => setSelectedCampaignId(campaign.id)} className={cardClasses} style={cardStyle}>
|
||||||
<div className={`relative z-10 ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3 rounded-md' : ''}`}>
|
<div className={`relative z-10 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`}>
|
||||||
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
|
||||||
<button onClick={(e) => { e.stopPropagation(); requestDeleteCampaign(campaign.id, campaign.name); }} className="mt-2 text-red-300 hover:text-red-100 text-xs flex items-center bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"><Trash2 size={14} className="mr-1" /> Delete</button>
|
<button onClick={(e) => { e.stopPropagation(); requestDeleteCampaign(campaign.id, campaign.name); }} className="mt-auto text-red-300 hover:text-red-100 text-xs flex items-center self-start bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"><Trash2 size={14} className="mr-1" /> Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
})}
|
})}
|
||||||
@ -353,6 +353,9 @@ function AdminView({ userId }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- CreateCampaignForm, CharacterManager, EncounterManager, CreateEncounterForm, ParticipantManager, EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons ---
|
||||||
|
// These components are identical to the previous version (v0.1.28) and are included below for completeness.
|
||||||
|
|
||||||
function CreateCampaignForm({ onCreate, onCancel }) {
|
function CreateCampaignForm({ onCreate, onCancel }) {
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [backgroundUrl, setBackgroundUrl] = useState('');
|
const [backgroundUrl, setBackgroundUrl] = useState('');
|
||||||
@ -375,6 +378,7 @@ 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);
|
const [defaultMaxHp, setDefaultMaxHp] = useState(10);
|
||||||
@ -382,7 +386,6 @@ function CharacterManager({ campaignId, campaignCharacters }) {
|
|||||||
const [editingCharacter, setEditingCharacter] = useState(null);
|
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);
|
||||||
@ -565,7 +568,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
const [selectedCharacterId, setSelectedCharacterId] = useState('');
|
const [selectedCharacterId, setSelectedCharacterId] = useState('');
|
||||||
const [monsterInitMod, setMonsterInitMod] = useState(2);
|
const [monsterInitMod, setMonsterInitMod] = useState(2);
|
||||||
const [maxHp, setMaxHp] = useState(10);
|
const [maxHp, setMaxHp] = useState(10);
|
||||||
const [isNpc, setIsNpc] = useState(false); // New state for NPC checkbox
|
const [isNpc, setIsNpc] = useState(false);
|
||||||
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);
|
||||||
@ -579,11 +582,10 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
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) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); }
|
if (selectedChar && selectedChar.defaultMaxHp) { setMaxHp(selectedChar.defaultMaxHp); } else { setMaxHp(10); }
|
||||||
setIsNpc(false); // Characters cannot be NPCs in this model
|
setIsNpc(false);
|
||||||
} else if (participantType === 'monster') {
|
} else if (participantType === 'monster') {
|
||||||
setMaxHp(10);
|
setMaxHp(10);
|
||||||
setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
|
setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
|
||||||
// setIsNpc(false); // Reset NPC status when switching to monster, or keep last state? Let's reset.
|
|
||||||
}
|
}
|
||||||
}, [selectedCharacterId, participantType, campaignCharacters]);
|
}, [selectedCharacterId, participantType, campaignCharacters]);
|
||||||
|
|
||||||
@ -604,16 +606,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
currentMaxHp = character.defaultMaxHp || currentMaxHp;
|
currentMaxHp = character.defaultMaxHp || currentMaxHp;
|
||||||
modifier = character.defaultInitMod || 0;
|
modifier = character.defaultInitMod || 0;
|
||||||
finalInitiative = initiativeRoll + modifier;
|
finalInitiative = initiativeRoll + modifier;
|
||||||
} else { // Monster
|
} else {
|
||||||
modifier = parseInt(monsterInitMod, 10) || 0;
|
modifier = parseInt(monsterInitMod, 10) || 0;
|
||||||
finalInitiative = initiativeRoll + modifier;
|
finalInitiative = initiativeRoll + modifier;
|
||||||
participantIsNpc = isNpc; // Use the state of the NPC checkbox
|
participantIsNpc = isNpc;
|
||||||
}
|
}
|
||||||
const newParticipant = {
|
const newParticipant = {
|
||||||
id: generateId(), name: nameToAdd, type: participantType,
|
id: generateId(), name: nameToAdd, type: participantType,
|
||||||
originalCharacterId: participantType === 'character' ? selectedCharacterId : null,
|
originalCharacterId: participantType === 'character' ? selectedCharacterId : null,
|
||||||
initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp,
|
initiative: finalInitiative, maxHp: currentMaxHp, currentHp: currentMaxHp,
|
||||||
isNpc: participantType === 'monster' ? participantIsNpc : false, // Store isNpc for monsters
|
isNpc: participantType === 'monster' ? participantIsNpc : false,
|
||||||
conditions: [], isActive: true,
|
conditions: [], isActive: true,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@ -635,7 +637,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
const modifier = char.defaultInitMod || 0;
|
const modifier = char.defaultInitMod || 0;
|
||||||
const finalInitiative = initiativeRoll + modifier;
|
const finalInitiative = initiativeRoll + modifier;
|
||||||
consoleRollLog += `${char.name}: Rolled D20 (${initiativeRoll}) + ${modifier} (mod) = ${finalInitiative} init\n`;
|
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
|
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 };
|
||||||
});
|
});
|
||||||
if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); return; }
|
if (newParticipants.length === 0) { alert("All campaign characters are already in this encounter."); return; }
|
||||||
console.log(consoleRollLog);
|
console.log(consoleRollLog);
|
||||||
@ -645,7 +647,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
|
|
||||||
const handleUpdateParticipant = async (updatedData) => {
|
const handleUpdateParticipant = async (updatedData) => {
|
||||||
if (!db || !editingParticipant) return;
|
if (!db || !editingParticipant) return;
|
||||||
const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...updatedData } : p ); // updatedData now includes isNpc
|
const updatedParticipants = participants.map(p => p.id === editingParticipant.id ? { ...p, ...updatedData } : p );
|
||||||
try { await updateDoc(doc(db, encounterPath), { participants: updatedParticipants }); setEditingParticipant(null); } catch (err) { console.error("Error updating participant:", err); }
|
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); };
|
const requestDeleteParticipant = (participantId, participantName) => { setItemToDelete({ id: participantId, name: participantName, type: 'participant' }); setShowDeleteParticipantConfirm(true); };
|
||||||
@ -711,37 +713,60 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
<Users2 size={16} className="mr-1.5"/><Dices size={16} className="mr-1.5"/> Add All (Roll Init)
|
<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-x-4 gap-y-3 mb-4 p-3 bg-slate-700 rounded">
|
<form onSubmit={(e) => { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded items-end">
|
||||||
<div>
|
<div className="md:col-span-2">
|
||||||
<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); setSelectedCharacterId(''); setIsNpc(false);}} 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(''); setIsNpc(false);}} 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' && (
|
{participantType === 'monster' && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="md:col-span-4">
|
||||||
<label htmlFor="monsterName" className="block text-sm font-medium text-slate-300">Monster Name</label>
|
<label htmlFor="monsterName" className="block text-sm font-medium text-slate-300">Monster Name</label>
|
||||||
<input type="text" id="monsterName" 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" />
|
<input type="text" id="monsterName" 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>
|
||||||
<div>
|
<div className="md:col-span-2">
|
||||||
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-slate-300">Monster Init Mod</label>
|
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-slate-300">Monster Init Mod</label>
|
||||||
<input type="number" id="monsterInitMod" 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" />
|
<input type="number" id="monsterInitMod" 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>
|
</div>
|
||||||
<div className="md:col-span-2 flex items-center">
|
<div className="md:col-span-2">
|
||||||
|
<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 items-center pt-5">
|
||||||
<input type="checkbox" id="isNpc" checked={isNpc} onChange={(e) => setIsNpc(e.target.checked)} className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" />
|
<input type="checkbox" id="isNpc" checked={isNpc} onChange={(e) => setIsNpc(e.target.checked)} className="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" />
|
||||||
<label htmlFor="isNpc" className="ml-2 block text-sm text-slate-300">Is NPC?</label>
|
<label htmlFor="isNpc" className="ml-2 block text-sm text-slate-300">Is NPC?</label>
|
||||||
</div>
|
</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>)}
|
{participantType === 'character' && (
|
||||||
<div className={participantType === 'monster' ? 'md:col-span-2' : ''}><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"><Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)</button></div>
|
<div className="md:col-span-4">
|
||||||
|
<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 className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-300">Max HP (Encounter)</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-6 flex justify-end mt-2">
|
||||||
|
<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 && (
|
{lastRollDetails && (
|
||||||
<p className="text-sm text-green-400 mt-2 mb-2 text-center">
|
<p className="text-sm text-green-400 mt-2 mb-2 text-center">
|
||||||
{lastRollDetails.name} ({lastRollDetails.type}): Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative
|
{lastRollDetails.name} ({lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type}): Rolled D20 ({lastRollDetails.roll}) {lastRollDetails.mod >= 0 ? '+' : ''} {lastRollDetails.mod} (mod) = {lastRollDetails.total} Initiative
|
||||||
</p>
|
</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>}
|
||||||
@ -750,7 +775,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
|
|||||||
const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId;
|
const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId;
|
||||||
const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
|
const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
|
||||||
const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character';
|
const participantDisplayType = p.type === 'monster' ? (p.isNpc ? 'NPC' : 'Monster') : 'Character';
|
||||||
let bgColor = p.type === 'character' ? 'bg-sky-800' : (p.isNpc ? 'bg-slate-600' : 'bg-red-800');
|
let bgColor = p.type === 'character' ? 'bg-sky-800' : (p.isNpc ? 'bg-slate-600' : 'bg-red-800');
|
||||||
if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';
|
if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -793,7 +818,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
|||||||
initiative: parseInt(initiative, 10),
|
initiative: parseInt(initiative, 10),
|
||||||
currentHp: parseInt(currentHp, 10),
|
currentHp: parseInt(currentHp, 10),
|
||||||
maxHp: parseInt(maxHp, 10),
|
maxHp: parseInt(maxHp, 10),
|
||||||
isNpc: participant.type === 'monster' ? isNpc : false, // Only save isNpc for monsters
|
isNpc: participant.type === 'monster' ? isNpc : false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@ -820,10 +845,6 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (InitiativeControls, DisplayView, Modal, Icons)
|
|
||||||
// The rest of the components are assumed to be the same as v0.1.25 for this update.
|
|
||||||
// DisplayView will need to be updated to reflect NPC styling.
|
|
||||||
|
|
||||||
function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
function InitiativeControls({ campaignId, encounter, encounterPath }) {
|
||||||
const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false);
|
const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false);
|
||||||
const handleStartEncounter = async () => {
|
const handleStartEncounter = async () => {
|
||||||
@ -964,7 +985,7 @@ function DisplayView() {
|
|||||||
let participantBgColor = 'bg-red-700'; // Default for monster
|
let participantBgColor = 'bg-red-700'; // Default for monster
|
||||||
if (p.type === 'character') {
|
if (p.type === 'character') {
|
||||||
participantBgColor = 'bg-sky-700';
|
participantBgColor = 'bg-sky-700';
|
||||||
} else if (p.isNpc) {
|
} else if (p.isNpc) { // Monster that is an NPC
|
||||||
participantBgColor = 'bg-slate-700'; // Muted gray for NPC
|
participantBgColor = 'bg-slate-700'; // Muted gray for NPC
|
||||||
}
|
}
|
||||||
if (p.id === currentTurnParticipantId && isStarted && !isPaused) {
|
if (p.id === currentTurnParticipantId && isStarted && !isPaused) {
|
||||||
@ -987,8 +1008,7 @@ function DisplayView() {
|
|||||||
</div>
|
</div>
|
||||||
{p.conditions?.length > 0 && <p className="text-sm text-yellow-300 mt-2">Conditions: {p.conditions.join(', ')}</p>}
|
{p.conditions?.length > 0 && <p className="text-sm text-yellow-300 mt-2">Conditions: {p.conditions.join(', ')}</p>}
|
||||||
{!p.isActive && <p className="text-center text-lg font-semibold text-slate-300 mt-2">(Inactive)</p>}
|
{!p.isActive && <p className="text-center text-lg font-semibold text-slate-300 mt-2">(Inactive)</p>}
|
||||||
</div>
|
</div>);
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user