grover gaming

This commit is contained in:
legop3
2026-05-13 20:10:47 -04:00
parent 36a6ee012c
commit 3c9400f488
7 changed files with 189 additions and 125 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<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-BZ5Amllx.js"></script> <script type="module" crossorigin src="/assets/index-0u9jLPMr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ByJnZYMA.css"> <link rel="stylesheet" crossorigin href="/assets/index-ByJnZYMA.css">
</head> </head>
<body> <body>
@@ -1,4 +1,5 @@
import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx'; import RoverMediaPlayer from '../RoverMediaPlayer/index.jsx';
import { useControlSystem } from '../../controls/index.js';
import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx'; import TurnsOverlay from '../HudOverlays/TurnsOverlay/index.jsx';
import HudOverlay from '../HudOverlays/HudOverlay/index.jsx'; import HudOverlay from '../HudOverlays/HudOverlay/index.jsx';
import RoverDescriptionOverlay from '../HudOverlays/RoverDescriptionOverlay/index.jsx'; import RoverDescriptionOverlay from '../HudOverlays/RoverDescriptionOverlay/index.jsx';
@@ -8,6 +9,9 @@ import DriverBottomStrip from '../HudOverlays/DriverBottomStrip/index.jsx';
import HudChatInput from '../HudOverlays/HudChatInput/index.jsx'; import HudChatInput from '../HudOverlays/HudChatInput/index.jsx';
export default function DriverVideo({ layoutFormat = 'desktop' }) { export default function DriverVideo({ layoutFormat = 'desktop' }) {
const {
state: { lastControlIntentAt },
} = useControlSystem();
const mobileHud = layoutFormat !== 'desktop'; const mobileHud = layoutFormat !== 'desktop';
return ( return (
<section className="panel"> <section className="panel">
@@ -18,6 +22,7 @@ export default function DriverVideo({ layoutFormat = 'desktop' }) {
<RoverDescriptionOverlay <RoverDescriptionOverlay
variant="default" variant="default"
mobileHud={mobileHud} mobileHud={mobileHud}
controlIntentAt={lastControlIntentAt}
/> />
<HudOverlay <HudOverlay
layoutFormat={layoutFormat} layoutFormat={layoutFormat}
@@ -0,0 +1,38 @@
import TopDownMap from '../../TopDownMap/index.jsx';
export default function HudMapOverlay({
sensors,
show = true,
mapPosition = 'top-center',
layoutFormat = 'desktop',
mobileHud = false,
}) {
if (!show) return null;
const portraitMobile = layoutFormat === 'mobile-portrait';
const mapSize = '240px';
const mapScale = portraitMobile ? 0.3 : mobileHud ? 0.33 : 0.7;
const mapOpacity = mobileHud ? 0.6 : 0.7;
const mapStyle = {
width: mapSize,
height: mapSize,
opacity: mapOpacity,
transform: mapPosition === 'top-center' ? `translateX(-50%) scale(${mapScale})` : `scale(${mapScale})`,
transformOrigin:
mapPosition === 'bottom-left'
? 'bottom left'
: mapPosition === 'top-center'
? 'top center'
: 'top right',
...(mapPosition === 'bottom-left'
? { left: '0.25rem', bottom: '0.25rem' }
: mapPosition === 'top-center'
? { left: '50%', top: '0.25rem' }
: { right: '0.25rem', top: '0.25rem' }),
};
return (
<div className="pointer-events-none absolute rounded" style={mapStyle}>
<TopDownMap sensors={sensors} size={240} overlay />
</div>
);
}
@@ -0,0 +1,50 @@
import { roverNameChromeStyle } from '../../../lib/roverColor.js';
export default function RoverLabelOverlay({
variant = 'default',
label,
roverColor = null,
driverLabel = null,
mobileHud = false,
labelScale = 1,
}) {
const labelPadClass = mobileHud ? 'px-0.25 py-0.25' : 'px-0.5 py-0.5';
const labelTextClass = mobileHud ? 'text-[0.55rem]' : 'text-[0.8rem]';
const labelPosClass = 'bottom-0.5';
const labelWrapperStyle = {
transform: `translateX(-50%) scale(${labelScale})`,
transformOrigin: 'center bottom',
};
if (variant === 'spectator') {
return (
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}>
<div className={`flex items-center gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}>
<span
className="font-semibold text-white rounded px-1 py-[1px] border border-transparent"
style={roverNameChromeStyle(roverColor, 0.18)}
>
{label || 'Unnamed Rover'}
</span>
{driverLabel ? <span className="text-slate-300"> {driverLabel}</span> : null}
</div>
</div>
);
}
return (
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}>
<div className={`flex gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}>
<span>
Rover:{' '}
<span
className="rounded px-1 py-[1px] border border-transparent"
style={roverNameChromeStyle(roverColor, 0.18)}
>
"{label || 'Unnamed Rover'}"
</span>
</span>
</div>
</div>
);
}
@@ -0,0 +1,52 @@
export default function SpectatorTelemetryOverlay({ sensors, mobileHud = false }) {
const statusPadClass = mobileHud ? 'px-0.25 py-0.25' : 'px-1 py-0.5';
const telemetryPosClass = mobileHud ? 'left-0.5 top-1/2' : 'left-1 top-1/2';
const telemetryTextClass = mobileHud ? 'text-[0.45rem]' : 'text-[0.65rem]';
const telemetryEntries = [
['Voltage', sensors?.voltageMv != null ? `${(sensors.voltageMv / 1000).toFixed(2)} V` : '--'],
['Current', sensors?.currentMa != null ? `${sensors.currentMa} mA` : '--'],
['Charge', sensors?.batteryChargeMah != null ? `${sensors.batteryChargeMah}` : '--'],
['OI', sensors?.oiMode?.label || '--'],
];
const docked = Boolean(sensors?.chargingSources?.homeBase);
const chargingLabel = sensors?.chargingState?.label || '';
const charging = Boolean(chargingLabel && chargingLabel.toLowerCase() !== 'not charging');
const oiLabel = sensors?.oiMode?.label || 'Unknown';
const oiNormalized = oiLabel.toLowerCase();
const oiTone =
oiNormalized === 'full'
? 'bg-emerald-500/80 text-emerald-50'
: oiNormalized === 'safe'
? 'bg-amber-400/80 text-amber-950'
: oiNormalized === 'passive'
? 'bg-slate-700/80 text-slate-100'
: 'bg-slate-700/60 text-slate-200';
const dockTone = docked ? 'bg-emerald-500/80 text-emerald-50' : 'bg-slate-700/70 text-slate-200';
const chargingTone = charging
? 'bg-emerald-500/80 text-emerald-50'
: docked
? 'bg-amber-400/80 text-amber-950'
: 'bg-slate-700/70 text-slate-200';
return (
<div
className={`absolute ${telemetryPosClass} flex -translate-y-1/2 flex-col gap-0.5 bg-black/70 text-slate-100 ${telemetryTextClass} ${statusPadClass}`}
>
<div className="space-y-0.5 leading-tight">
<div className="flex flex-col gap-0.5 text-[0.75rem] font-semibold uppercase tracking-wide">
<span className={`rounded px-1.5 py-0.5 ${dockTone}`}>{docked ? 'Docked' : 'Undocked'}</span>
<span className={`rounded px-1.5 py-0.5 ${chargingTone}`}>
{charging ? 'Charging' : docked ? 'Not charging' : 'Not charging'}
</span>
<span className={`rounded px-1.5 py-0.5 ${oiTone}`}>OI: {oiLabel}</span>
</div>
{telemetryEntries.map(([labelText, value]) => (
<span key={labelText} className="flex items-center justify-between gap-0.5">
<span className="text-slate-400">{labelText}</span>
<span className="font-semibold text-white">{value}</span>
</span>
))}
</div>
</div>
);
}
@@ -5,8 +5,9 @@ import React from 'react';
import { useHudMapSetting } from '../../../hooks/useHudMapSetting.js'; import { useHudMapSetting } from '../../../hooks/useHudMapSetting.js';
import { useSessionSelector } from '../../../context/SessionContext.jsx'; import { useSessionSelector } from '../../../context/SessionContext.jsx';
import { useTelemetryFrame } from '../../../context/TelemetryContext.jsx'; import { useTelemetryFrame } from '../../../context/TelemetryContext.jsx';
import TopDownMap from '../../TopDownMap/index.jsx'; import RoverLabelOverlay from './RoverLabelOverlay.jsx';
import { roverNameChromeStyle } from '../../../lib/roverColor.js'; import SpectatorTelemetryOverlay from './SpectatorTelemetryOverlay.jsx';
import HudMapOverlay from './HudMapOverlay.jsx';
function HudOverlay({ function HudOverlay({
roverId = null, roverId = null,
@@ -56,134 +57,52 @@ function HudOverlay({
: showHudMapDesktop; : showHudMapDesktop;
const resolvedMapPosition = const resolvedMapPosition =
mapPosition || (variant === 'spectator' ? 'top-center' : isMobile ? 'top-right' : 'top-center'); mapPosition || (variant === 'spectator' ? 'top-center' : isMobile ? 'top-right' : 'top-center');
const portraitMobile = layoutFormat === 'mobile-portrait';
const statusPadClass = isMobile ? 'px-0.25 py-0.25' : 'px-1 py-0.5';
const labelPadClass = isMobile ? 'px-0.25 py-0.25' : 'px-0.5 py-0.5';
const labelTextClass = isMobile ? 'text-[0.55rem]' : 'text-[0.8rem]';
const telemetryPosClass = isMobile ? 'left-0.5 top-1/2' : 'left-1 top-1/2';
const labelPosClass = isMobile ? 'bottom-0.5' : 'bottom-0.5';
const labelWrapperStyle = {
transform: `translateX(-50%) scale(${labelScale})`,
transformOrigin: 'center bottom',
};
const mapSize = '240px';
const mapScale = portraitMobile ? 0.3 : isMobile ? 0.33 : 0.7;
const mapOpacity = isMobile ? 0.6 : 0.7;
const mapStyle = {
width: mapSize,
height: mapSize,
opacity: mapOpacity,
transform: resolvedMapPosition === 'top-center' ? `translateX(-50%) scale(${mapScale})` : `scale(${mapScale})`,
transformOrigin:
resolvedMapPosition === 'bottom-left'
? 'bottom left'
: resolvedMapPosition === 'top-center'
? 'top center'
: 'top right',
...(resolvedMapPosition === 'bottom-left'
? { left: '0.25rem', bottom: '0.25rem' }
: resolvedMapPosition === 'top-center'
? { left: '50%', top: '0.25rem' }
: { right: '0.25rem', top: '0.25rem' }),
};
if (variant === 'none') { if (variant === 'none') {
return null; return null;
} }
if (variant === 'spectator') { if (variant === 'spectator') {
const telemetryEntries = [
['Voltage', resolvedSensors?.voltageMv != null ? `${(resolvedSensors.voltageMv / 1000).toFixed(2)} V` : '--'],
['Current', resolvedSensors?.currentMa != null ? `${resolvedSensors.currentMa} mA` : '--'],
['Charge', resolvedSensors?.batteryChargeMah != null ? `${resolvedSensors.batteryChargeMah}` : '--'],
['OI', resolvedSensors?.oiMode?.label || '--'],
];
const docked = Boolean(resolvedSensors?.chargingSources?.homeBase);
const chargingLabel = resolvedSensors?.chargingState?.label || '';
const charging = Boolean(chargingLabel && chargingLabel.toLowerCase() !== 'not charging');
const oiLabel = resolvedSensors?.oiMode?.label || 'Unknown';
const oiNormalized = oiLabel.toLowerCase();
const oiTone =
oiNormalized === 'full'
? 'bg-emerald-500/80 text-emerald-50'
: oiNormalized === 'safe'
? 'bg-amber-400/80 text-amber-950'
: oiNormalized === 'passive'
? 'bg-slate-700/80 text-slate-100'
: 'bg-slate-700/60 text-slate-200';
const dockTone = docked ? 'bg-emerald-500/80 text-emerald-50' : 'bg-slate-700/70 text-slate-200';
const chargingTone = charging
? 'bg-emerald-500/80 text-emerald-50'
: docked
? 'bg-amber-400/80 text-amber-950'
: 'bg-slate-700/70 text-slate-200';
return ( return (
<> <>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div <SpectatorTelemetryOverlay sensors={resolvedSensors} mobileHud={isMobile} />
className={`absolute ${telemetryPosClass} flex -translate-y-1/2 flex-col gap-0.5 bg-black/70 text-slate-100 ${isMobile ? 'text-[0.45rem]' : 'text-[0.65rem]'} ${statusPadClass}`} <RoverLabelOverlay
> variant="spectator"
<div className="space-y-0.5 leading-tight"> label={resolvedLabel}
<div className="flex flex-col gap-0.5 text-[0.75rem] font-semibold uppercase tracking-wide"> roverColor={resolvedRoverColor}
<span className={`rounded px-1.5 py-0.5 ${dockTone}`}>{docked ? 'Docked' : 'Undocked'}</span> driverLabel={resolvedDriverLabel}
<span className={`rounded px-1.5 py-0.5 ${chargingTone}`}> mobileHud={isMobile}
{charging ? 'Charging' : docked ? 'Not charging' : 'Not charging'} labelScale={labelScale}
</span> />
<span className={`rounded px-1.5 py-0.5 ${oiTone}`}>OI: {oiLabel}</span>
</div> </div>
{telemetryEntries.map(([labelText, value]) => ( <HudMapOverlay
<span key={labelText} className="flex items-center justify-between gap-0.5"> sensors={resolvedSensors}
<span className="text-slate-400">{labelText}</span> show={resolvedShowTopDown}
<span className="font-semibold text-white">{value}</span> mapPosition={resolvedMapPosition}
</span> layoutFormat={layoutFormat}
))} mobileHud={isMobile}
</div> />
</div>
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}>
<div
className={`flex items-center gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}
>
<span
className="font-semibold text-white rounded px-1 py-[1px] border border-transparent"
style={roverNameChromeStyle(resolvedRoverColor, 0.18)}
>
{resolvedLabel || 'Unnamed Rover'}
</span>
{resolvedDriverLabel ? <span className="text-slate-300"> {resolvedDriverLabel}</span> : null}
</div>
</div>
</div>
{resolvedShowTopDown ? (
<div className="pointer-events-none absolute rounded" style={{ ...mapStyle }}>
<TopDownMap sensors={resolvedSensors} size={240} overlay />
</div>
) : null}
</> </>
); );
} }
return ( return (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center"> <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className={`absolute ${labelPosClass} left-1/2`} style={labelWrapperStyle}> <RoverLabelOverlay
<div className={`flex gap-0.5 bg-black/80 text-slate-100 ${labelPadClass} ${labelTextClass}`}> variant="default"
<span> label={resolvedLabel}
Rover:{' '} roverColor={resolvedRoverColor}
<span mobileHud={isMobile}
className="rounded px-1 py-[1px] border border-transparent" labelScale={labelScale}
style={roverNameChromeStyle(resolvedRoverColor, 0.18)} />
> <HudMapOverlay
"{resolvedLabel || 'Unnamed Rover'}" sensors={resolvedSensors}
</span> show={resolvedShowTopDown && variant !== 'spectator'}
</span> mapPosition={resolvedMapPosition}
</div> layoutFormat={layoutFormat}
</div> mobileHud={isMobile}
/>
{resolvedShowTopDown && variant !== 'spectator' ? (
<div className="pointer-events-none absolute rounded" style={{ ...mapStyle }}>
<TopDownMap sensors={resolvedSensors} size={240} overlay />
</div>
) : null}
</div> </div>
); );
} }