t2-mapper/app/page.tsx

151 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-09-11 16:48:23 -07:00
"use client";
2025-12-14 11:06:57 -08:00
import { useState, useEffect, useCallback, Suspense, useMemo } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { Canvas, GLProps } from "@react-three/fiber";
import { NoToneMapping, SRGBColorSpace, PCFShadowMap } from "three";
import { Mission } from "@/src/components/Mission";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ObserverControls } from "@/src/components/ObserverControls";
import { InspectorControls } from "@/src/components/InspectorControls";
import { SettingsProvider } from "@/src/components/SettingsProvider";
import { ObserverCamera } from "@/src/components/ObserverCamera";
2025-11-15 16:33:18 -08:00
import { AudioProvider } from "@/src/components/AudioContext";
import { DebugElements } from "@/src/components/DebugElements";
2025-11-26 14:37:49 -08:00
import { CamerasProvider } from "@/src/components/CamerasProvider";
import { getMissionList, getMissionInfo } from "@/src/manifest";
2025-11-13 22:55:58 -08:00
// 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();
2025-09-11 16:48:23 -07:00
// 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,
};
2025-11-15 12:55:08 -08:00
function MapInspector() {
2025-11-14 03:30:33 -08:00
const searchParams = useSearchParams();
const router = useRouter();
2025-12-14 11:06:57 -08:00
const [initialMissionName, initialMissionType] = useMemo(
() => (searchParams.get("mission") || "RiverDance:CTF").split("~"),
[],
);
const [missionName, setMissionName] = useState(initialMissionName);
const availableMissionTypes = getMissionInfo(missionName).missionTypes;
const [missionType, setMissionType] = useState(() =>
initialMissionType && availableMissionTypes.includes(initialMissionType)
? initialMissionType
: availableMissionTypes[0],
2025-11-14 03:30:33 -08:00
);
2025-12-14 11:06:57 -08:00
const isOnlyMissionType = availableMissionTypes.length === 1;
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]);
2025-11-14 03:30:33 -08:00
useEffect(() => {
// For automation, like the t2-maps app!
window.setMissionName = setMissionName;
window.getMissionList = getMissionList;
window.getMissionInfo = getMissionInfo;
return () => {
delete window.setMissionName;
delete window.getMissionList;
delete window.getMissionInfo;
};
}, []);
2025-11-14 03:30:33 -08:00
// Update query params when state changes
useEffect(() => {
const params = new URLSearchParams();
2025-12-14 11:06:57 -08:00
const value = isOnlyMissionType
? missionName
: `${missionName}~${missionType}`;
params.set("mission", value);
2025-11-14 03:30:33 -08:00
router.replace(`?${params.toString()}`, { scroll: false });
2025-12-14 11:06:57 -08:00
}, [missionName, missionType, isOnlyMissionType, router]);
2025-09-12 10:30:40 -07:00
const handleLoadingChange = useCallback(
(_loading: boolean, progress: number = 0) => {
setLoadingProgress(progress);
},
[],
);
2025-12-02 22:16:40 -08:00
2025-09-11 16:48:23 -07:00
return (
<QueryClientProvider client={queryClient}>
<main>
<SettingsProvider>
2025-12-02 16:58:35 -08:00
<div id="canvasContainer">
{showLoadingIndicator && (
<div id="loadingIndicator" data-complete={!isLoading}>
<div className="LoadingSpinner" />
<div className="LoadingProgress">
<div
className="LoadingProgress-bar"
style={{ width: `${loadingProgress * 100}%` }}
/>
</div>
<div className="LoadingProgress-text">
{Math.round(loadingProgress * 100)}%
</div>
</div>
)}
2025-12-14 11:06:57 -08:00
<Canvas
frameloop="always"
gl={glSettings}
shadows={{ type: PCFShadowMap }}
>
2025-12-02 16:58:35 -08:00
<CamerasProvider>
<AudioProvider>
2025-12-02 22:16:40 -08:00
<Mission
2025-12-14 11:06:57 -08:00
key={`${missionName}~${missionType}`}
2025-12-02 22:16:40 -08:00
name={missionName}
2025-12-14 11:06:57 -08:00
missionType={missionType}
2025-12-02 22:16:40 -08:00
onLoadingChange={handleLoadingChange}
2025-12-14 11:06:57 -08:00
setMissionType={setMissionType}
2025-12-02 22:16:40 -08:00
/>
2025-12-02 16:58:35 -08:00
<ObserverCamera />
<DebugElements />
<ObserverControls />
</AudioProvider>
</CamerasProvider>
</Canvas>
</div>
2025-11-13 22:55:58 -08:00
<InspectorControls
missionName={missionName}
2025-12-14 11:06:57 -08:00
missionType={missionType}
onChangeMission={({ missionName, missionType }) => {
setMissionName(missionName);
setMissionType(missionType);
}}
2025-09-12 10:30:40 -07:00
/>
</SettingsProvider>
</main>
</QueryClientProvider>
2025-09-11 16:48:23 -07:00
);
}
2025-11-15 12:55:08 -08:00
export default function HomePage() {
return (
<Suspense>
<MapInspector />
</Suspense>
);
}