This commit is contained in:
legop3
2026-05-13 19:55:55 -04:00
parent b38a81d002
commit 36a6ee012c
32 changed files with 898 additions and 933 deletions
@@ -0,0 +1,102 @@
import { useEffect, useRef, useState } from 'react';
import { useSessionSelector } from '../../../context/SessionContext.jsx';
const DISMISS_AFTER_INPUT_MS = 5000;
const LARGE_FADE_MS = 700;
export default function RoverDescriptionOverlay({
roverId = null,
description,
variant = 'default',
mobileHud = false,
displayKey = '',
controlIntentAt,
}) {
const assignedRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const effectiveRoverId = roverId ?? assignedRoverId;
const rosterDescription = useSessionSelector((state) => {
if (!effectiveRoverId) return null;
const roster = state.session?.roster || [];
const rover = roster.find((entry) => String(entry.id) === String(effectiveRoverId));
return rover?.description || null;
});
const resolvedDescription = description ?? rosterDescription;
const resolvedControlIntentAt =
typeof controlIntentAt === 'number' ? controlIntentAt : 0;
const resolvedDisplayKey =
displayKey || `${effectiveRoverId || ''}::${resolvedDescription || ''}`;
const [largeVisible, setLargeVisible] = useState(false);
const [largeFading, setLargeFading] = useState(false);
const baselineIntentRef = useRef(0);
const fadeTimerRef = useRef(null);
const hideTimerRef = useRef(null);
useEffect(
() => () => {
clearTimeout(fadeTimerRef.current);
clearTimeout(hideTimerRef.current);
},
[],
);
useEffect(() => {
if (variant !== 'default' || !resolvedDescription) {
setLargeVisible(false);
setLargeFading(false);
clearTimeout(fadeTimerRef.current);
clearTimeout(hideTimerRef.current);
return undefined;
}
clearTimeout(fadeTimerRef.current);
clearTimeout(hideTimerRef.current);
setLargeVisible(true);
setLargeFading(false);
baselineIntentRef.current = Number(resolvedControlIntentAt) || 0;
return undefined;
}, [resolvedDescription, resolvedDisplayKey, resolvedControlIntentAt, variant]);
useEffect(() => {
if (variant !== 'default' || !resolvedDescription || !largeVisible || largeFading) return;
const nextIntent = Number(resolvedControlIntentAt) || 0;
if (nextIntent <= baselineIntentRef.current) return;
if (fadeTimerRef.current || hideTimerRef.current) return;
fadeTimerRef.current = setTimeout(() => {
setLargeFading(true);
fadeTimerRef.current = null;
}, DISMISS_AFTER_INPUT_MS);
hideTimerRef.current = setTimeout(() => {
setLargeVisible(false);
hideTimerRef.current = null;
}, DISMISS_AFTER_INPUT_MS + LARGE_FADE_MS);
}, [resolvedControlIntentAt, resolvedDescription, largeFading, largeVisible, variant]);
if (!resolvedDescription) return null;
if (variant === 'spectator') {
return (
<div className="pointer-events-none absolute inset-x-0 top-1 z-50 flex justify-center">
<div className="surface max-w-[92%] border border-slate-600/80 px-1 py-0.5 text-center text-[0.62rem] leading-tight text-slate-100 shadow-lg">
{resolvedDescription}
</div>
</div>
);
}
if (variant !== 'default' || !largeVisible) return null;
return (
<div className="pointer-events-none absolute inset-0 z-50 flex items-center justify-center">
<div
className={`surface max-w-[94%] border border-slate-400 bg-neutral-900 px-2 py-1 text-center font-semibold text-slate-100 shadow-xl transition-opacity duration-700 ${
largeFading ? 'opacity-0' : 'opacity-100'
} ${mobileHud ? 'text-[1rem] leading-tight' : 'text-[1.5rem] leading-tight'}`}
>
<p>Just so you know, this rover</p>
<p>{resolvedDescription}</p>
<p className={`${mobileHud ? 'text-[0.58rem]' : 'text-[0.72rem]'} mt-0.5 font-normal text-slate-300`}>
This fades 5 seconds after your first control input.
</p>
</div>
</div>
);
}