"use client"; import { useState, useEffect, useCallback, Suspense, useRef } from "react"; import { Canvas, GLProps } from "@react-three/fiber"; import { NoToneMapping, SRGBColorSpace, PCFShadowMap, Camera } from "three"; import { Mission } from "@/src/components/Mission"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ObserverControls } from "@/src/components/ObserverControls"; import { TouchJoystick, TouchCameraMovement, type JoystickState, } from "@/src/components/TouchControls"; import { InspectorControls } from "@/src/components/InspectorControls"; import { useTouchDevice } from "@/src/components/useTouchDevice"; import { SettingsProvider } from "@/src/components/SettingsProvider"; import { ObserverCamera } from "@/src/components/ObserverCamera"; import { AudioProvider } from "@/src/components/AudioContext"; import { DebugElements } from "@/src/components/DebugElements"; import { CamerasProvider } from "@/src/components/CamerasProvider"; import { getMissionList, getMissionInfo } from "@/src/manifest"; import { createParser, useQueryState } from "nuqs"; // Three.js has its own loaders for textures and models, but we need to load other // stuff too, e.g. missions, terrains, and more. This client is used for those. const queryClient = new QueryClient(); // Renderer settings to match Tribes 2's simple rendering pipeline. // Tribes 2 (Torque engine, 2001) worked entirely in gamma/sRGB space with no HDR // or tone mapping. We disable tone mapping and ensure proper sRGB output. const glSettings: GLProps = { toneMapping: NoToneMapping, outputColorSpace: SRGBColorSpace, }; type CurrentMission = { missionName: string; missionType?: string; }; const defaultMission: CurrentMission = { missionName: "RiverDance", missionType: "CTF", }; const parseAsMissionWithType = createParser({ parse(query: string) { const [missionName, missionType] = query.split("~"); let selectedMissionType = missionType; const availableMissionTypes = getMissionInfo(missionName).missionTypes; if (!missionType || !availableMissionTypes.includes(missionType)) { selectedMissionType = availableMissionTypes[0]; } return { missionName, missionType: selectedMissionType }; }, serialize({ missionName, missionType }): string { const availableMissionTypes = getMissionInfo(missionName).missionTypes; if (availableMissionTypes.length === 1) { return missionName; } return `${missionName}~${missionType}`; }, eq(a, b) { return a.missionName === b.missionName && a.missionType === b.missionType; }, }).withDefault(defaultMission); function MapInspector() { const [currentMission, setCurrentMission] = useQueryState( "mission", parseAsMissionWithType, ); const changeMission = useCallback( (mission: CurrentMission) => { window.location.hash = ""; setCurrentMission(mission); }, [setCurrentMission], ); const isTouch = useTouchDevice(); const { missionName, missionType } = currentMission; const [loadingProgress, setLoadingProgress] = useState(0); const [showLoadingIndicator, setShowLoadingIndicator] = useState(true); const isLoading = loadingProgress < 1; // Keep the loading indicator visible briefly after reaching 100% useEffect(() => { if (isLoading) { setShowLoadingIndicator(true); } else { const timer = setTimeout(() => setShowLoadingIndicator(false), 500); return () => clearTimeout(timer); } }, [isLoading]); useEffect(() => { // For automation, like the t2-maps app! window.setMissionName = (missionName: string) => { const availableMissionTypes = getMissionInfo(missionName).missionTypes; changeMission({ missionName, missionType: availableMissionTypes[0], }); }; window.getMissionList = getMissionList; window.getMissionInfo = getMissionInfo; return () => { delete window.setMissionName; delete window.getMissionList; delete window.getMissionInfo; }; }, [changeMission]); const handleLoadingChange = useCallback( (_loading: boolean, progress: number = 0) => { setLoadingProgress(progress); }, [], ); const cameraRef = useRef(null); const joystickStateRef = useRef({ angle: 0, force: 0 }); const joystickZoneRef = useRef(null); const lookJoystickStateRef = useRef({ angle: 0, force: 0 }); const lookJoystickZoneRef = useRef(null); return (
{showLoadingIndicator && (
{Math.round(loadingProgress * 100)}%
)} {isTouch && ( )} { cameraRef.current = state.camera; }} > {isTouch === null ? null : isTouch ? ( ) : ( )}
); } export default function HomePage() { return ( ); }