Added NPC flag.

This commit is contained in:
Robert Johnson 2025-05-27 10:51:29 -04:00
parent 9f73dedcad
commit 99a38bb75a

View File

@ -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 && <p>Authenticating...</p>}
</main>
<footer className="bg-slate-900 p-4 text-center text-sm text-slate-400 mt-8">
TTRPG Initiative Tracker v0.1.30
TTRPG Initiative Tracker v0.1.28
</footer>
</div>
);
}
// --- 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 }) {
<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' : ''}`}>
<h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
{/* ImageIcon display removed from here */}
<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>
</div>
</div>);
@ -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 }) {
<Users2 size={16} className="mr-1.5"/><Dices size={16} className="mr-1.5"/> Add All (Roll Init)
</button>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleAddParticipant(); }} className="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2 mb-4 p-3 bg-slate-700 rounded">
<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">
<div>
<label className="block text-sm font-medium text-slate-300">Type</label>
<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">
<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="character">Character</option>
</select>
@ -724,12 +722,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{participantType === 'monster' && (
<>
<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" />
<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" />
</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" />
<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" />
</div>
<div className="md:col-span-2 flex items-center">
<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>
</div>
</>
)}
@ -739,7 +741,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
</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
{lastRollDetails.name} ({lastRollDetails.type}): 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>}
@ -747,13 +749,17 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
{sortedAdminParticipants.map((p) => {
const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId;
const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
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');
if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';
return (
<li key={p.id} draggable={isDraggable} onDragStart={isDraggable ? (e) => handleDragStart(e, p.id) : undefined} onDragOver={isDraggable ? handleDragOver : undefined} onDrop={isDraggable ? (e) => handleDrop(e, p.id) : undefined} onDragEnd={isDraggable ? handleDragEnd : undefined}
className={`p-3 rounded-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 transition-all ${isCurrentTurn && !encounter.isPaused ? 'bg-green-600 ring-2 ring-green-300 shadow-lg' : (p.type === 'character' ? 'bg-sky-800' : 'bg-red-800')} ${!p.isActive ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : ''} ${draggedItemId === p.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`}>
className={`p-3 rounded-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 transition-all ${bgColor} ${isCurrentTurn && !encounter.isPaused ? 'ring-2 ring-green-300 shadow-lg' : ''} ${!p.isActive ? 'opacity-50' : ''} ${isDraggable ? 'cursor-grab' : ''} ${draggedItemId === p.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`}>
<div className="flex-1 flex items-center">
{isDraggable && <ChevronsUpDown size={18} className="mr-2 text-slate-400 flex-shrink-0" title="Drag to reorder in tie"/>}
<div>
<p className={`font-semibold text-lg ${isCurrentTurn && !encounter.isPaused ? 'text-white' : 'text-white'}`}>{p.name} <span className="text-xs">({p.type})</span>{isCurrentTurn && !encounter.isPaused && <span className="ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center"><Zap size={12} className="mr-1"/> CURRENT</span>}</p>
<p className={`font-semibold text-lg text-white`}>{p.name} <span className="text-xs">({participantDisplayType})</span>{isCurrentTurn && !encounter.isPaused && <span className="ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center"><Zap size={12} className="mr-1"/> CURRENT</span>}</p>
<p className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-slate-200'}`}>Init: {p.initiative} | HP: {p.currentHp}/{p.maxHp}</p>
</div>
</div>
@ -773,15 +779,23 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters }) {
);
}
// ... (EditParticipantModal, InitiativeControls, DisplayView, Modal, Icons)
// The rest of the components are assumed to be the same as v0.1.25 for this update.
function EditParticipantModal({ participant, onClose, onSave }) {
const [name, setName] = useState(participant.name);
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), }); };
const [isNpc, setIsNpc] = useState(participant.type === 'monster' ? (participant.isNpc || false) : false);
const handleSubmit = (e) => {
e.preventDefault();
onSave({
name: name.trim(),
initiative: parseInt(initiative, 10),
currentHp: parseInt(currentHp, 10),
maxHp: parseInt(maxHp, 10),
isNpc: participant.type === 'monster' ? isNpc : false, // Only save isNpc for monsters
});
};
return (
<Modal onClose={onClose} title={`Edit ${participant.name}`}>
<form onSubmit={handleSubmit} className="space-y-4">
@ -791,6 +805,12 @@ function EditParticipantModal({ participant, onClose, onSave }) {
<div className="flex-1"><label className="block text-sm font-medium text-slate-300">Current HP</label><input type="number" value={currentHp} onChange={(e) => setCurrentHp(e.target.value)} className="mt-1 block 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="flex-1"><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 block 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>
{participant.type === 'monster' && (
<div className="flex items-center">
<input type="checkbox" id="editIsNpc" 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="editIsNpc" className="ml-2 block text-sm text-slate-300">Is NPC?</label>
</div>
)}
<div className="flex justify-end space-x-3 pt-2">
<button type="button" onClick={onClose} className="px-4 py-2 text-sm font-medium text-slate-300 bg-slate-600 hover:bg-slate-500 rounded-md transition-colors">Cancel</button>
<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"><Save size={18} className="mr-1 inline-block" /> Save</button>
@ -800,6 +820,10 @@ 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 }) {
const [showEndEncounterConfirm, setShowEndEncounterConfirm] = useState(false);
const handleStartEncounter = async () => {
@ -936,10 +960,23 @@ function DisplayView() {
{!isStarted && (!participants || participants.length === 0) && <p className="text-2xl text-slate-500 mb-6">No participants.</p>}
{participantsToRender.length === 0 && isStarted && <p className="text-xl text-slate-400">No active participants.</p>}
<div className="space-y-4 max-w-3xl mx-auto">
{participantsToRender.map(p => (
<div key={p.id} className={`p-4 md:p-6 rounded-lg shadow-lg transition-all ${p.id === currentTurnParticipantId && isStarted && !isPaused ? 'bg-green-700 ring-4 ring-green-400 scale-105' : (p.type === 'character' ? 'bg-sky-700' : 'bg-red-700')} ${!p.isActive ? 'opacity-40 grayscale' : ''} ${isPaused && p.id === currentTurnParticipantId ? 'ring-2 ring-yellow-400' : ''}`}>
{participantsToRender.map(p => {
let participantBgColor = 'bg-red-700'; // Default for monster
if (p.type === 'character') {
participantBgColor = 'bg-sky-700';
} else if (p.isNpc) {
participantBgColor = 'bg-slate-700'; // Muted gray for NPC
}
if (p.id === currentTurnParticipantId && isStarted && !isPaused) {
participantBgColor = 'bg-green-700 ring-4 ring-green-400 scale-105';
} else if (isPaused && p.id === currentTurnParticipantId) {
participantBgColor += ' ring-2 ring-yellow-400';
}
return (
<div key={p.id} className={`p-4 md:p-6 rounded-lg shadow-lg transition-all ${participantBgColor} ${!p.isActive ? 'opacity-40 grayscale' : ''}`}>
<div className="flex justify-between items-center mb-2">
<h3 className={`text-2xl md:text-3xl font-bold ${p.id === currentTurnParticipantId && isStarted && !isPaused ? 'text-white' : (p.type === 'character' ? 'text-sky-100' : 'text-red-100')}`}>{p.name}{p.id === currentTurnParticipantId && isStarted && !isPaused && <span className="text-yellow-300 animate-pulse ml-2">(Current)</span>}</h3>
<h3 className={`text-2xl md:text-3xl font-bold ${p.id === currentTurnParticipantId && isStarted && !isPaused ? 'text-white' : (p.type === 'character' ? 'text-sky-100' : (p.isNpc ? 'text-slate-100' : 'text-red-100'))}`}>{p.name}{p.id === currentTurnParticipantId && isStarted && !isPaused && <span className="text-yellow-300 animate-pulse ml-2">(Current)</span>}</h3>
<span className={`text-xl md:text-2xl font-semibold ${p.id === currentTurnParticipantId && isStarted && !isPaused ? 'text-green-200' : 'text-slate-200'}`}>Init: {p.initiative}</span>
</div>
<div className="flex justify-between items-center">
@ -950,7 +987,9 @@ function DisplayView() {
</div>
{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>}
</div>))}
</div>
);
})}
</div>
</div>
</div>);