import { useEffect, useState, useRef, RefObject } from "react"; import { RiLandscapeFill } from "react-icons/ri"; import { FaRotateRight } from "react-icons/fa6"; import { LuClipboardList } from "react-icons/lu"; import { Camera } from "three"; import { useControls, useDebug, useSettings, type TouchMode, } from "./SettingsProvider"; import { CopyCoordinatesButton } from "./CopyCoordinatesButton"; import { LoadDemoButton } from "./LoadDemoButton"; import { JoinServerButton } from "./JoinServerButton"; import { Accordion, AccordionGroup } from "./Accordion"; import styles from "./InspectorControls.module.css"; import { useTouchDevice } from "./useTouchDevice"; import { useRecording } from "./RecordingProvider"; import { useDataSource, useMissionName } from "../state/gameEntityStore"; import { useLiveSelector } from "../state/liveConnectionStore"; import { hasMission } from "../manifest"; const DEFAULT_PANELS = ["controls", "preferences", "audio"]; export function InspectorControls({ missionName, missionType, onOpenMapInfo, onOpenServerBrowser, onChooseMap, onCancelChoosingMap, choosingMap, cameraRef, invalidateRef, }: { missionName: string; missionType: string; onOpenMapInfo: () => void; onOpenServerBrowser?: () => void; onChooseMap?: () => void; onCancelChoosingMap?: () => void; choosingMap?: boolean; cameraRef: RefObject; invalidateRef: RefObject<() => void>; }) { const isTouch = useTouchDevice(); const dataSource = useDataSource(); const recording = useRecording(); const storeMissionName = useMissionName(); const hasStreamData = dataSource === "demo" || dataSource === "live"; // When streaming, the URL query param may not reflect the actual map. // Use the store's mission name (from the server) for the manifest check. const effectiveMissionName = hasStreamData ? storeMissionName : missionName; const missionInManifest = effectiveMissionName ? hasMission(effectiveMissionName) : false; const isLiveConnected = useLiveSelector( (s) => s.gameStatus === "connected" || s.gameStatus === "authenticating", ); const { fogEnabled, setFogEnabled, fov, setFov, audioEnabled, setAudioEnabled, audioVolume, setAudioVolume, animationEnabled, setAnimationEnabled, } = useSettings(); const { speedMultiplier, setSpeedMultiplier, touchMode, setTouchMode, invertScroll, setInvertScroll, invertDrag, setInvertDrag, invertJoystick, setInvertJoystick, } = useControls(); const { debugMode, setDebugMode, renderOnDemand, setRenderOnDemand } = useDebug(); const [settingsOpen, setSettingsOpen] = useState(false); const dropdownRef = useRef(null); const buttonRef = useRef(null); const focusAreaRef = useRef(null); // Focus the panel when it opens. useEffect(() => { if (settingsOpen) { dropdownRef.current?.focus(); } }, [settingsOpen]); const handleDropdownBlur = (e: React.FocusEvent) => { const relatedTarget = e.relatedTarget as Node | null; if (relatedTarget && focusAreaRef.current?.contains(relatedTarget)) { return; } setSettingsOpen(false); }; // Close on Escape and return focus to the gear button. const handlePanelKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { setSettingsOpen(false); buttonRef.current?.focus(); } }; return (
{onOpenServerBrowser && ( )}
setSpeedMultiplier(parseFloat(event.target.value) / 100) } />

How fast you move in free-flying mode. {isTouch === false ? " Use your scroll wheel or trackpad to adjust while flying." : ""}

{isTouch ? (
{" "}

Single stick has a unified move + look control. Dual stick has independent move + look.

) : null} {isTouch === false ? (
{ setInvertScroll(event.target.checked); }} />

Reverse which scroll direction increases and decreases fly speed.

) : null} {isTouch ? (
{ setInvertJoystick(event.target.checked); }} />

Reverse joystick look direction.

) : null}
{ setInvertDrag(event.target.checked); }} />

Reverse how dragging the viewport aims the camera.

{fov}° setFov(parseInt(event.target.value))} />
{ setAudioEnabled(event.target.checked); }} />
{Math.round(audioVolume * 100)}% setAudioVolume(parseFloat(event.target.value)) } />
{ setFogEnabled(event.target.checked); }} />
{ setAnimationEnabled(event.target.checked); }} />
{ setDebugMode(event.target.checked); }} />
{ setRenderOnDemand(event.target.checked); }} />

Significantly decreases CPU and GPU usage by only rendering frames when requested. Helpful when developing parts of the app unrelated to rendering.

); }