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.
- The rovers enjoy chaos and destruction, not order and precision.
- It is your job to monitor them.
- tsk and !tskip are chat bot commands, ignore them.
Character:
- 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-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CH1QpIkJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C8rSwF1O.css">
<script type="module" crossorigin src="/assets/index-BjZM2uht.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CxKDohQn.css">
</head>
<body>
<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 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 roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const users = useSessionSelector((state) => state.session?.users ?? []);
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
@@ -23,7 +37,9 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const [now, setNow] = useState(() => Date.now());
const [turnCueVisible, setTurnCueVisible] = useState(false);
const [turnCueStartAt, setTurnCueStartAt] = useState(null);
const [notTurnFlashAt, setNotTurnFlashAt] = useState(0);
const lastTurnRef = useRef({ active: false, roverId: null });
const lastIntentRef = useRef(lastControlIntentAt || 0);
useEffect(() => {
if (mode !== 'turns') {
return undefined;
@@ -51,22 +67,34 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const idleDeadline = turnInfo?.idleDeadline || null;
const msUntilTurn = deadline ? deadline - now : null;
const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null;
const isPreSwitchWindow =
mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
const shouldShowVideo = mode !== 'turns' || isActiveDriver || isPreSwitchWindow;
const isTurnsMode = mode === 'turns';
const totalRovers = roster.length;
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 =
msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null;
const idleSkipSeconds =
msUntilIdleSkip != null && Number.isFinite(msUntilIdleSkip)
? Math.max(0, Math.ceil(msUntilIdleSkip / 1000))
: null;
const turnTimerText = isActiveDriver
? turnSeconds != null
? `${turnSeconds}s left`
: null
: isNextDriver && turnSeconds != null
? `Your turn in ${turnSeconds}s`
: null;
const turnTimerText = useMemo(() => {
if (!isTurnsMode) return null;
if (isActiveDriver) {
return turnSeconds != null ? `${turnSeconds}s left` : 'Your turn';
}
const parts = ['Not your turn'];
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
? [
...(shouldShowVideo ? [{ type: 'rover', id: roverId, key: roverId }] : []),
@@ -117,6 +145,16 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
}
}, [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 (
<section className="panel">
{roverId ? (
@@ -132,12 +170,14 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
layoutFormat={layoutFormat}
overcurrentLimiter={overcurrentLimiter}
songNote={song?.note}
qualityNotice={!shouldShowVideo ? 'Preview feed (low FPS) until your turn.' : null}
qualityNotice={null}
showTurnCue={turnCueVisible}
turnTimerText={turnTimerText}
turnSeconds={turnSeconds}
isActiveDriver={isActiveDriver}
idleSkipSeconds={idleSkipSeconds}
showNotTurnNotice={isNotYourTurn}
notTurnFlashAt={notTurnFlashAt}
/>
) : (
<div className="panel-muted content-center text-center text-sm text-slate-400 aspect-[4/3]">
@@ -19,6 +19,7 @@ function HudOverlay({
mobileHud = false,
mapPosition = 'top-center',
turnTimerText = null,
turnTimerFlashActive = false,
labelScale = 1,
}) {
const isMobile = mobileHud;
@@ -147,7 +148,9 @@ function HudOverlay({
</div>
{turnTimerText ? (
<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}
</div>
+11
View File
@@ -47,6 +47,8 @@ export default function VideoTile({
turnTimerText = null,
isActiveDriver = false,
idleSkipSeconds = null,
showNotTurnNotice = false,
notTurnFlashAt = 0,
}) {
const discordUrl = useSessionSelector((state) => {
const socials = state.session?.socials || [];
@@ -72,6 +74,7 @@ export default function VideoTile({
const [restartToken, setRestartToken] = useState(0);
const [audioRestartToken, setAudioRestartToken] = useState(0);
const [muted, setMuted] = useState(true);
const [noticeFlashActive, setNoticeFlashActive] = useState(false);
const hasDedicatedAudio = Boolean(audioSessionInfo?.url);
const usingSnapshot = videoMode === 'snapshot';
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(() => {
if (status === 'playing') {
attemptUnmute(0);
@@ -634,6 +644,7 @@ export default function VideoTile({
mobileHud={mobileHud}
mapPosition={effectiveHudMapPosition}
turnTimerText={turnTimerText}
turnTimerFlashActive={noticeFlashActive}
labelScale={hudLabelScale}
/>
) : null}