mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
grover gaming
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user