turns stuff testings

This commit is contained in:
legop3
2026-05-12 14:45:03 -04:00
parent 53ca7aca0a
commit c4f470b329
8 changed files with 202 additions and 147 deletions
@@ -4,6 +4,7 @@ Context:
- You are in a chat room with people driving rovers. - You are in a chat room with people driving rovers.
- The rovers enjoy chaos and destruction, not order and precision. - The rovers enjoy chaos and destruction, not order and precision.
- It is your job to monitor them. - It is your job to monitor them.
- tsk and !tskip are chat bot commands, ignore them.
Character: Character:
- Calm and highly intelligent with dry sarcasm. - Calm and highly intelligent with dry sarcasm.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CH1QpIkJ.js"></script> <script type="module" crossorigin src="/assets/index-BjZM2uht.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C8rSwF1O.css"> <link rel="stylesheet" crossorigin href="/assets/index-CxKDohQn.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+52 -12
View File
@@ -9,10 +9,24 @@ import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js';
import { useControlSystem } from '../../controls/index.js'; import { useControlSystem } from '../../controls/index.js';
import VideoTile from '../VideoTile/index.jsx'; import VideoTile from '../VideoTile/index.jsx';
export default function DriverVideoPanel({layoutFormat = 'desktop'}) { function countEligibleDrivers(users = []) {
const unique = new Set();
users.forEach((entry) => {
const role = String(entry?.role || '');
if (role === 'spectator') return;
const roverId = String(entry?.roverId || '').trim();
const socketId = String(entry?.socketId || '').trim();
if (!roverId || !socketId) return;
unique.add(socketId);
});
return unique.size;
}
export default function DriverVideoPanel({ layoutFormat = 'desktop' }) {
const mode = useSessionSelector((state) => state.session?.mode || null); const mode = useSessionSelector((state) => state.session?.mode || null);
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? []); const roster = useSessionSelector((state) => state.session?.roster ?? []);
const users = useSessionSelector((state) => state.session?.users ?? []);
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {}); const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null); const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {}); const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
@@ -23,7 +37,9 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
const [turnCueVisible, setTurnCueVisible] = useState(false); const [turnCueVisible, setTurnCueVisible] = useState(false);
const [turnCueStartAt, setTurnCueStartAt] = useState(null); const [turnCueStartAt, setTurnCueStartAt] = useState(null);
const [notTurnFlashAt, setNotTurnFlashAt] = useState(0);
const lastTurnRef = useRef({ active: false, roverId: null }); const lastTurnRef = useRef({ active: false, roverId: null });
const lastIntentRef = useRef(lastControlIntentAt || 0);
useEffect(() => { useEffect(() => {
if (mode !== 'turns') { if (mode !== 'turns') {
return undefined; return undefined;
@@ -51,22 +67,34 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const idleDeadline = turnInfo?.idleDeadline || null; const idleDeadline = turnInfo?.idleDeadline || null;
const msUntilTurn = deadline ? deadline - now : null; const msUntilTurn = deadline ? deadline - now : null;
const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null; const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null;
const isPreSwitchWindow = const isTurnsMode = mode === 'turns';
mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0; const totalRovers = roster.length;
const shouldShowVideo = mode !== 'turns' || isActiveDriver || isPreSwitchWindow; const totalDrivers = useMemo(() => countEligibleDrivers(users), [users]);
const shouldUsePreviewByLoad = isTurnsMode && totalDrivers > totalRovers;
const isPreSwitchWindow = isTurnsMode && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
const isNotYourTurn = isTurnsMode && !isActiveDriver;
const shouldUsePreview = isNotYourTurn && !isPreSwitchWindow && shouldUsePreviewByLoad;
const shouldShowVideo = !shouldUsePreview;
const turnSeconds = const turnSeconds =
msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null; msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null;
const idleSkipSeconds = const idleSkipSeconds =
msUntilIdleSkip != null && Number.isFinite(msUntilIdleSkip) msUntilIdleSkip != null && Number.isFinite(msUntilIdleSkip)
? Math.max(0, Math.ceil(msUntilIdleSkip / 1000)) ? Math.max(0, Math.ceil(msUntilIdleSkip / 1000))
: null; : null;
const turnTimerText = isActiveDriver const turnTimerText = useMemo(() => {
? turnSeconds != null if (!isTurnsMode) return null;
? `${turnSeconds}s left` if (isActiveDriver) {
: null return turnSeconds != null ? `${turnSeconds}s left` : 'Your turn';
: isNextDriver && turnSeconds != null }
? `Your turn in ${turnSeconds}s` const parts = ['Not your turn'];
: null; if (isNextDriver && turnSeconds != null) {
parts.push(`your turn in ${turnSeconds}s`);
}
if (shouldUsePreview) {
parts.push('preview to save upload bandwidth');
}
return parts.join(' • ');
}, [isTurnsMode, isActiveDriver, isNextDriver, turnSeconds, shouldUsePreview]);
const entries = roverId const entries = roverId
? [ ? [
...(shouldShowVideo ? [{ type: 'rover', id: roverId, key: roverId }] : []), ...(shouldShowVideo ? [{ type: 'rover', id: roverId, key: roverId }] : []),
@@ -117,6 +145,16 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
} }
}, [lastControlIntentAt, turnCueStartAt, turnCueVisible]); }, [lastControlIntentAt, turnCueStartAt, turnCueVisible]);
useEffect(() => {
const lastIntent = Number(lastIntentRef.current) || 0;
const nextIntent = Number(lastControlIntentAt) || 0;
const advanced = nextIntent > lastIntent;
if (advanced && isNotYourTurn) {
setNotTurnFlashAt(Date.now());
}
lastIntentRef.current = nextIntent;
}, [isNotYourTurn, lastControlIntentAt]);
return ( return (
<section className="panel"> <section className="panel">
{roverId ? ( {roverId ? (
@@ -132,12 +170,14 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
layoutFormat={layoutFormat} layoutFormat={layoutFormat}
overcurrentLimiter={overcurrentLimiter} overcurrentLimiter={overcurrentLimiter}
songNote={song?.note} songNote={song?.note}
qualityNotice={!shouldShowVideo ? 'Preview feed (low FPS) until your turn.' : null} qualityNotice={null}
showTurnCue={turnCueVisible} showTurnCue={turnCueVisible}
turnTimerText={turnTimerText} turnTimerText={turnTimerText}
turnSeconds={turnSeconds} turnSeconds={turnSeconds}
isActiveDriver={isActiveDriver} isActiveDriver={isActiveDriver}
idleSkipSeconds={idleSkipSeconds} idleSkipSeconds={idleSkipSeconds}
showNotTurnNotice={isNotYourTurn}
notTurnFlashAt={notTurnFlashAt}
/> />
) : ( ) : (
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]"> <div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
@@ -19,6 +19,7 @@ function HudOverlay({
mobileHud = false, mobileHud = false,
mapPosition = 'top-center', mapPosition = 'top-center',
turnTimerText = null, turnTimerText = null,
turnTimerFlashActive = false,
labelScale = 1, labelScale = 1,
}) { }) {
const isMobile = mobileHud; const isMobile = mobileHud;
@@ -147,7 +148,9 @@ function HudOverlay({
</div> </div>
{turnTimerText ? ( {turnTimerText ? (
<div <div
className={`absolute left-1/2 top-0.5 -translate-x-1/2 rounded bg-black/70 text-slate-100 ${timerPadClass} ${timerTextClass}`} className={`absolute left-1/2 top-0.5 -translate-x-1/2 rounded transition-colors ${
turnTimerFlashActive ? 'bg-red-900/80 text-red-100' : 'bg-black/70 text-slate-100'
} ${timerPadClass} ${timerTextClass}`}
> >
{turnTimerText} {turnTimerText}
</div> </div>
+11
View File
@@ -47,6 +47,8 @@ export default function VideoTile({
turnTimerText = null, turnTimerText = null,
isActiveDriver = false, isActiveDriver = false,
idleSkipSeconds = null, idleSkipSeconds = null,
showNotTurnNotice = false,
notTurnFlashAt = 0,
}) { }) {
const discordUrl = useSessionSelector((state) => { const discordUrl = useSessionSelector((state) => {
const socials = state.session?.socials || []; const socials = state.session?.socials || [];
@@ -72,6 +74,7 @@ export default function VideoTile({
const [restartToken, setRestartToken] = useState(0); const [restartToken, setRestartToken] = useState(0);
const [audioRestartToken, setAudioRestartToken] = useState(0); const [audioRestartToken, setAudioRestartToken] = useState(0);
const [muted, setMuted] = useState(true); const [muted, setMuted] = useState(true);
const [noticeFlashActive, setNoticeFlashActive] = useState(false);
const hasDedicatedAudio = Boolean(audioSessionInfo?.url); const hasDedicatedAudio = Boolean(audioSessionInfo?.url);
const usingSnapshot = videoMode === 'snapshot'; const usingSnapshot = videoMode === 'snapshot';
const sensors = telemetryFrame?.sensors; const sensors = telemetryFrame?.sensors;
@@ -298,6 +301,13 @@ export default function VideoTile({
[], [],
); );
useEffect(() => {
if (!showNotTurnNotice || !notTurnFlashAt) return undefined;
setNoticeFlashActive(true);
const timer = setTimeout(() => setNoticeFlashActive(false), 650);
return () => clearTimeout(timer);
}, [showNotTurnNotice, notTurnFlashAt]);
useEffect(() => { useEffect(() => {
if (status === 'playing') { if (status === 'playing') {
attemptUnmute(0); attemptUnmute(0);
@@ -634,6 +644,7 @@ export default function VideoTile({
mobileHud={mobileHud} mobileHud={mobileHud}
mapPosition={effectiveHudMapPosition} mapPosition={effectiveHudMapPosition}
turnTimerText={turnTimerText} turnTimerText={turnTimerText}
turnTimerFlashActive={noticeFlashActive}
labelScale={hudLabelScale} labelScale={hudLabelScale}
/> />
) : null} ) : null}