Generic (non-5e) ruleset mode + UI polish + combat switch fix #7

Merged
robert merged 25 commits from feat/generic-ruleset into main 2026-07-08 22:54:04 -04:00
Showing only changes of commit a5b6d61e83 - Show all commits
+41 -5
View File
@@ -1993,7 +1993,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
// ENCOUNTER MANAGER COMPONENT
// ============================================================================
function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters }) {
function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters, encounterStartedRef, encounterActiveRef }) {
const { showToast } = useUIFeedback();
const { data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(
campaignId ? getPath.encounters(campaignId) : null
@@ -2130,6 +2130,15 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId);
useEffect(() => {
if (encounterStartedRef) {
encounterStartedRef.current = !!(selectedEncounter && selectedEncounter.isStarted && !selectedEncounter.isPaused);
}
if (encounterActiveRef) {
encounterActiveRef.current = !!(selectedEncounter && selectedEncounter.isStarted);
}
}, [selectedEncounter, encounterStartedRef, encounterActiveRef]);
if (isLoadingEncounters && campaignId) {
return <p className="text-center text-stone-300 mt-4">Loading encounters...</p>;
}
@@ -2166,9 +2175,9 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
>
<div className="flex justify-between items-center">
<div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
<h4 className="font-medium text-white">{encounter.name}</h4>
<h4 className="font-medium text-white">{encounter.name} <span className={`ml-1 px-1.5 py-0.5 rounded text-xs font-bold tracking-wide ${encounter.ruleset === 'generic' ? 'bg-purple-900/80 text-purple-200 border border-purple-500' : 'bg-amber-900/80 text-amber-200 border border-amber-500'}`}>{encounter.ruleset === 'generic' ? 'GEN' : '5e'}</span></h4>
<p className="text-xs text-stone-300">
Participants: {encounter.participants?.length || 0}
{encounter.createdAt && `${new Date(encounter.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })} · `}Participants: {encounter.participants?.length || 0}
</p>
{isLive && (
<span className="text-xs text-green-400 font-semibold block mt-1">
@@ -2265,6 +2274,19 @@ function AdminView({ userId }) {
const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
const encounterStartedRef = useRef(false);
const encounterActiveRef = useRef(false);
const manualSelectRef = useRef(false);
const prevDisplayCampaignRef = useRef(null);
// External display change (replay/other DM) = clear manual override, allow follow.
useEffect(() => {
const cur = initialActiveInfo?.activeCampaignId || null;
if (cur !== prevDisplayCampaignRef.current) {
manualSelectRef.current = false;
prevDisplayCampaignRef.current = cur;
}
}, [initialActiveInfo]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
@@ -2324,10 +2346,15 @@ function AdminView({ userId }) {
}, [campaignsData]);
useEffect(() => {
// Skip follow only if user manually selected AND display hasn't changed.
// (external display change clears manualSelectRef via prevDisplay effect)
if (manualSelectRef.current && selectedCampaignId !== initialActiveInfo?.activeCampaignId) return;
if (
initialActiveInfo &&
initialActiveInfo.activeCampaignId &&
campaignsWithDetails.length > 0
campaignsWithDetails.length > 0 &&
!encounterStartedRef.current &&
!encounterActiveRef.current
) {
const campaignExists = campaignsWithDetails.some(c => c.id === initialActiveInfo.activeCampaignId);
if (campaignExists && selectedCampaignId !== initialActiveInfo.activeCampaignId) {
@@ -2500,7 +2527,14 @@ function AdminView({ userId }) {
return (
<div
key={campaign.id}
onClick={() => setSelectedCampaignId(campaign.id)}
onClick={() => {
if (encounterStartedRef.current) {
showToast('End or pause active encounter before switching campaigns.');
return;
}
manualSelectRef.current = true;
setSelectedCampaignId(campaign.id);
}}
className={cardClasses}
style={cardStyle}
>
@@ -2574,6 +2608,8 @@ function AdminView({ userId }) {
: null
}
campaignCharacters={selectedCampaign.characters || []}
encounterStartedRef={encounterStartedRef}
encounterActiveRef={encounterActiveRef}
/>
</div>
)}