fix: combat reveal toggle + eliminate poll flickering
Add 👁/🔒 toggle to combat row in Scene Secrets — players see combat panel only when GM reveals it (scene.reveal.combat). Stop flickering: background polls now run silent (no setLoading), and state updates are skipped when JSON content is unchanged, preventing unnecessary re-renders every 4s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -177,9 +177,8 @@ export default function MissionTab({ authedPlayer }) {
|
||||
|
||||
// ─── Data loading ───────────────────────────────────────────────
|
||||
|
||||
const loadMissions = useCallback(async function loadMissions() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const loadMissions = useCallback(async function loadMissions({ silent = false } = {}) {
|
||||
if (!silent) { setLoading(true); setError(''); }
|
||||
try {
|
||||
if (isGM) {
|
||||
const [allRes, activeRes] = await Promise.all([
|
||||
@@ -188,19 +187,18 @@ export default function MissionTab({ authedPlayer }) {
|
||||
]);
|
||||
const all = Array.isArray(allRes.data) ? allRes.data.map(normalizeMission) : [];
|
||||
const active = normalizeMission(activeRes.data);
|
||||
setMissions(all);
|
||||
setActiveMission(active);
|
||||
setSelectedMissionId(active?.id ? String(active.id) : (all[0]?.id ? String(all[0].id) : ''));
|
||||
setMissions(prev => JSON.stringify(prev) !== JSON.stringify(all) ? all : prev);
|
||||
setActiveMission(prev => JSON.stringify(prev) !== JSON.stringify(active) ? active : prev);
|
||||
if (!silent) setSelectedMissionId(active?.id ? String(active.id) : (all[0]?.id ? String(all[0].id) : ''));
|
||||
} else {
|
||||
const activeRes = await axios.get('/api/missions/active/player');
|
||||
setMissions([]);
|
||||
setActiveMission(normalizeMission(activeRes.data));
|
||||
setSelectedMissionId('');
|
||||
const active = normalizeMission(activeRes.data);
|
||||
setActiveMission(prev => JSON.stringify(prev) !== JSON.stringify(active) ? active : prev);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`Failed to load missions: ${e.response?.data?.error || e.message}`);
|
||||
if (!silent) setError(`Failed to load missions: ${e.response?.data?.error || e.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!silent) setLoading(false);
|
||||
}
|
||||
}, [isGM]);
|
||||
|
||||
@@ -208,7 +206,8 @@ export default function MissionTab({ authedPlayer }) {
|
||||
const loadRollFeed = useCallback(async function loadRollFeed() {
|
||||
try {
|
||||
const res = await axios.get('/api/missions/active/rolls/feed?limit=50');
|
||||
setRollFeed(Array.isArray(res.data) ? res.data : []);
|
||||
const next = Array.isArray(res.data) ? res.data : [];
|
||||
setRollFeed(prev => JSON.stringify(prev) !== JSON.stringify(next) ? next : prev);
|
||||
} catch (e) {
|
||||
// non-blocking
|
||||
}
|
||||
@@ -236,7 +235,7 @@ export default function MissionTab({ authedPlayer }) {
|
||||
useEffect(() => {
|
||||
if (isGM) return undefined;
|
||||
const timer = setInterval(() => {
|
||||
loadMissions();
|
||||
loadMissions({ silent: true });
|
||||
loadRollFeed();
|
||||
}, 4000);
|
||||
return () => clearInterval(timer);
|
||||
@@ -291,6 +290,7 @@ export default function MissionTab({ authedPlayer }) {
|
||||
objectives: r.objectives ?? legacy,
|
||||
complications: r.complications ?? false, // never auto-revealed
|
||||
checks: r.checks || {},
|
||||
combat: r.combat ?? false,
|
||||
};
|
||||
}, [currentScene]);
|
||||
|
||||
@@ -407,7 +407,7 @@ export default function MissionTab({ authedPlayer }) {
|
||||
i === index ? { ...scene, ...updates } : scene
|
||||
);
|
||||
await saveProgress(index, nextScenes);
|
||||
await loadMissions();
|
||||
await loadMissions({ silent: true });
|
||||
}
|
||||
|
||||
async function updateCombatState(updates) {
|
||||
@@ -1208,10 +1208,10 @@ export default function MissionTab({ authedPlayer }) {
|
||||
{/* Combat Setup */}
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-950/40 p-4">
|
||||
<h4 className="mb-3 text-sm font-semibold uppercase tracking-wide text-slate-300">Combat Setup</h4>
|
||||
{!isGM && !combatActive ? (
|
||||
{!isGM && !sceneReveal.combat ? (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
||||
<div className="text-2xl">⚔️</div>
|
||||
<p className="text-sm text-slate-400">Combat briefing is classified.<br />Awaiting GM to initiate combat.</p>
|
||||
<p className="text-sm text-slate-400">Combat briefing er klassificeret.<br />Afventer GM-ordre.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -1255,7 +1255,7 @@ export default function MissionTab({ authedPlayer }) {
|
||||
</div>
|
||||
|
||||
{/* ── Combat state panels (initiative, conditions, action ref, fear) ── */}
|
||||
{combatActive && (
|
||||
{(combatActive || (!isGM && sceneReveal.combat)) && (
|
||||
<div className="mt-4 space-y-4">
|
||||
|
||||
{/* Round counter + fear rating */}
|
||||
@@ -1786,6 +1786,14 @@ export default function MissionTab({ authedPlayer }) {
|
||||
<div className="text-sm font-medium">{sceneCombat.enemy || 'Custom/None'}</div>
|
||||
<div className="text-xs text-slate-400">BS {sceneCombat.bs || 45} / WS {sceneCombat.ws || 45} · {sceneCombat.damage || '1d10+5'}</div>
|
||||
</div>
|
||||
{/* Reveal toggle */}
|
||||
<button
|
||||
onClick={() => toggleReveal('combat')}
|
||||
title={sceneReveal.combat ? 'Hide combat from players' : 'Reveal combat to players'}
|
||||
className={`shrink-0 rounded px-2 py-1 text-xs ${sceneReveal.combat ? 'bg-emerald-900/50 text-emerald-300 hover:bg-emerald-800/50' : 'bg-slate-800 text-slate-500 hover:bg-slate-700'}`}
|
||||
>
|
||||
{sceneReveal.combat ? '👁' : '🔒'}
|
||||
</button>
|
||||
<button
|
||||
onClick={combatActive ? clearPlayPrompt : openCombatForPlayers}
|
||||
className={`shrink-0 rounded px-2 py-1 text-xs ${combatActive ? 'bg-rose-800 text-rose-200 hover:bg-rose-700' : 'bg-blue-700 text-white hover:bg-blue-600'}`}
|
||||
|
||||
Reference in New Issue
Block a user