mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-15 16:34:49 +00:00
fix title and meta tags, remove more Next.js stuff
This commit is contained in:
parent
5025065188
commit
842ddb7df7
13 changed files with 17 additions and 774 deletions
11
app/global.d.ts
vendored
11
app/global.d.ts
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
import type { getMissionList, getMissionInfo } from "@/src/manifest";
|
|
||||||
import type { DemoRecording } from "@/src/demo/types";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
setMissionName?: (missionName: string) => void;
|
|
||||||
getMissionList?: typeof getMissionList;
|
|
||||||
getMissionInfo?: typeof getMissionInfo;
|
|
||||||
loadDemoRecording?: (recording: DemoRecording) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
|
||||||
import "./style.css";
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "MapGenius – Explore maps for Tribes 2",
|
|
||||||
description: "Tribes 2 forever.",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const viewport = {
|
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 1,
|
|
||||||
userScalable: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<NuqsAdapter defaultOptions={{ clearOnDefault: false }}>
|
|
||||||
{children}
|
|
||||||
</NuqsAdapter>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
24
app/page.tsx
24
app/page.tsx
|
|
@ -1,24 +0,0 @@
|
||||||
"use client";
|
|
||||||
import { Suspense } from "react";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { FeaturesProvider } from "@/src/components/FeaturesProvider";
|
|
||||||
import { MapInspector } from "@/src/components/MapInspector";
|
|
||||||
import { SettingsProvider } from "@/src/components/SettingsProvider";
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
return (
|
|
||||||
<Suspense>
|
|
||||||
<FeaturesProvider>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<SettingsProvider>
|
|
||||||
<MapInspector />
|
|
||||||
</SettingsProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</FeaturesProvider>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
.CanvasContainer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LoadingIndicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.LoadingIndicator[data-complete="true"] {
|
|
||||||
animation: loadingComplete 0.3s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Spinner {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
|
||||||
border-top-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loadingComplete {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Sidebar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 260px;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SidebarSection {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.SidebarSection:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SectionLabel {
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: rgba(255, 255, 255, 0.4);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationList {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0 12px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationItem:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationItem[data-active="true"] {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayButton {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PlayButton:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationItem[data-active="true"] .PlayButton {
|
|
||||||
background: rgba(100, 180, 255, 0.3);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AnimationName {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ClipName {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CyclicIcon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.3);
|
|
||||||
title: "Cyclic (looping)";
|
|
||||||
}
|
|
||||||
|
|
||||||
.CheckboxField {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,441 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useEffectEvent,
|
|
||||||
useCallback,
|
|
||||||
Suspense,
|
|
||||||
useMemo,
|
|
||||||
} from "react";
|
|
||||||
import { Canvas, GLProps } from "@react-three/fiber";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { NoToneMapping, SRGBColorSpace, PCFShadowMap } from "three";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { OrbitControls, Center, Bounds, useBounds } from "@react-three/drei";
|
|
||||||
import { SettingsProvider, useDebug } from "@/src/components/SettingsProvider";
|
|
||||||
import { ShapeRenderer, useStaticShape } from "@/src/components/GenericShape";
|
|
||||||
import { ShapeInfoProvider } from "@/src/components/ShapeInfoProvider";
|
|
||||||
import { DebugElements } from "@/src/components/DebugElements";
|
|
||||||
import { TickProvider } from "@/src/components/TickProvider";
|
|
||||||
import { ShapeSelect } from "@/src/components/ShapeSelect";
|
|
||||||
import { engineStore, useEngineSelector } from "@/src/state/engineStore";
|
|
||||||
import {
|
|
||||||
getResourceList,
|
|
||||||
getResourceMap,
|
|
||||||
getResourceKey,
|
|
||||||
getSourceAndPath,
|
|
||||||
} from "@/src/manifest";
|
|
||||||
import { createParser, useQueryState } from "nuqs";
|
|
||||||
import { createScriptLoader } from "@/src/torqueScript/scriptLoader.browser";
|
|
||||||
import picomatch from "picomatch";
|
|
||||||
import {
|
|
||||||
createScriptCache,
|
|
||||||
type FileSystemHandler,
|
|
||||||
runServer,
|
|
||||||
type TorqueObject,
|
|
||||||
type TorqueRuntime,
|
|
||||||
} from "@/src/torqueScript";
|
|
||||||
import styles from "./page.module.css";
|
|
||||||
import { ignoreScripts } from "@/src/torqueScript/ignoreScripts";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
const sceneBg = new THREE.Color(0.1, 0.1, 0.1);
|
|
||||||
|
|
||||||
const glSettings: GLProps = {
|
|
||||||
toneMapping: NoToneMapping,
|
|
||||||
outputColorSpace: SRGBColorSpace,
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadScript = createScriptLoader();
|
|
||||||
const scriptCache = createScriptCache();
|
|
||||||
const fileSystem: FileSystemHandler = {
|
|
||||||
findFiles: (pattern) => {
|
|
||||||
const isMatch = picomatch(pattern, { nocase: true });
|
|
||||||
return getResourceList()
|
|
||||||
.filter((path) => isMatch(path))
|
|
||||||
.map((resourceKey) => {
|
|
||||||
const [, actualPath] = getSourceAndPath(resourceKey);
|
|
||||||
return actualPath;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isFile: (resourcePath) => {
|
|
||||||
const resourceKeys = getResourceMap();
|
|
||||||
const resourceKey = getResourceKey(resourcePath);
|
|
||||||
return resourceKeys[resourceKey] != null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultShape = "deploy_inventory.dts";
|
|
||||||
|
|
||||||
const parseAsShape = createParser<string>({
|
|
||||||
parse: (query: string) => query,
|
|
||||||
serialize: (value: string) => value,
|
|
||||||
eq: (a, b) => a === b,
|
|
||||||
}).withDefault(defaultShape);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to run the TorqueScript runtime once (hardcoded to SC_Normal/CTF)
|
|
||||||
* so deploy animations and other script-driven behaviors work.
|
|
||||||
*/
|
|
||||||
function useShapeRuntime(): TorqueRuntime | null {
|
|
||||||
const [runtime, setRuntime] = useState<TorqueRuntime | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
let isDisposed = false;
|
|
||||||
|
|
||||||
const { runtime, ready } = runServer({
|
|
||||||
missionName: "SC_Normal",
|
|
||||||
missionType: "CTF",
|
|
||||||
runtimeOptions: {
|
|
||||||
loadScript,
|
|
||||||
fileSystem,
|
|
||||||
cache: scriptCache,
|
|
||||||
signal: controller.signal,
|
|
||||||
ignoreScripts,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
void ready
|
|
||||||
.then(() => {
|
|
||||||
if (isDisposed || controller.signal.aborted) return;
|
|
||||||
engineStore.getState().setRuntime(runtime);
|
|
||||||
setRuntime(runtime);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err instanceof Error && err.name === "AbortError") return;
|
|
||||||
console.error("Shape runtime failed:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Seed store immediately
|
|
||||||
engineStore.getState().setRuntime(runtime);
|
|
||||||
|
|
||||||
const unsubscribe = runtime.subscribeRuntimeEvents((event) => {
|
|
||||||
if (event.type !== "batch.flushed") return;
|
|
||||||
engineStore.getState().applyRuntimeBatch(event.events, {
|
|
||||||
tick: event.tick,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isDisposed = true;
|
|
||||||
controller.abort();
|
|
||||||
unsubscribe();
|
|
||||||
engineStore.getState().clearRuntime();
|
|
||||||
runtime.destroy();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return runtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create a minimal TorqueObject for the shape viewer. */
|
|
||||||
function createFakeObject(
|
|
||||||
runtime: TorqueRuntime | null,
|
|
||||||
shapeName: string,
|
|
||||||
): TorqueObject {
|
|
||||||
// Try to find a matching datablock for this shape so deploy animations work.
|
|
||||||
let datablockName: string | undefined;
|
|
||||||
if (runtime) {
|
|
||||||
for (const obj of runtime.state.objectsById.values()) {
|
|
||||||
if (
|
|
||||||
obj.shapeFile &&
|
|
||||||
String(obj.shapeFile).toLowerCase() === shapeName.toLowerCase()
|
|
||||||
) {
|
|
||||||
datablockName = obj._name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
_id: 99999,
|
|
||||||
_class: "StaticShapeData",
|
|
||||||
_className: "StaticShape",
|
|
||||||
...(datablockName ? { datablock: datablockName } : {}),
|
|
||||||
} as TorqueObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FitOnLoad() {
|
|
||||||
const bounds = useBounds();
|
|
||||||
useEffect(() => {
|
|
||||||
bounds.refresh().fit();
|
|
||||||
}, [bounds]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnimationInfo {
|
|
||||||
name: string;
|
|
||||||
alias: string | null;
|
|
||||||
cyclic: boolean | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reports available animations (with cyclic and alias info when available). */
|
|
||||||
function AnimationReporter({
|
|
||||||
shapeName,
|
|
||||||
onAnimations,
|
|
||||||
}: {
|
|
||||||
shapeName: string;
|
|
||||||
onAnimations: (anims: AnimationInfo[]) => void;
|
|
||||||
}) {
|
|
||||||
const gltf = useStaticShape(shapeName);
|
|
||||||
const shapeAliases = useEngineSelector((state) =>
|
|
||||||
state.runtime.sequenceAliases.get(shapeName.toLowerCase()),
|
|
||||||
);
|
|
||||||
const anims = useMemo(() => {
|
|
||||||
// Collect cyclic info from vis_sequence nodes on the scene
|
|
||||||
const visCyclic = new Map<string, boolean>();
|
|
||||||
gltf.scene.traverse((node: any) => {
|
|
||||||
const ud = node.userData;
|
|
||||||
if (ud?.vis_sequence && ud.vis_cyclic != null) {
|
|
||||||
visCyclic.set(ud.vis_sequence.toLowerCase(), !!ud.vis_cyclic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Build reverse alias map: clip name -> alias
|
|
||||||
let reverseAliases: Map<string, string> | undefined;
|
|
||||||
if (shapeAliases) {
|
|
||||||
reverseAliases = new Map();
|
|
||||||
for (const [alias, clipName] of shapeAliases) {
|
|
||||||
reverseAliases.set(clipName, alias);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gltf.animations.map((clip) => ({
|
|
||||||
name: clip.name,
|
|
||||||
alias: reverseAliases?.get(clip.name.toLowerCase()) ?? null,
|
|
||||||
cyclic: visCyclic.get(clip.name.toLowerCase()) ?? null,
|
|
||||||
}));
|
|
||||||
}, [gltf, shapeAliases]);
|
|
||||||
const reportAnimations = useEffectEvent(onAnimations);
|
|
||||||
useEffect(() => {
|
|
||||||
reportAnimations(anims);
|
|
||||||
}, [anims]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Plays the selected animation via the TorqueScript runtime. */
|
|
||||||
function AnimationPlayer({
|
|
||||||
object,
|
|
||||||
runtime,
|
|
||||||
animation,
|
|
||||||
}: {
|
|
||||||
object: TorqueObject;
|
|
||||||
runtime: TorqueRuntime | null;
|
|
||||||
animation: string;
|
|
||||||
}) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (!runtime || !animation) return;
|
|
||||||
// Use nsCall to dispatch directly on the ShapeBase namespace, bypassing
|
|
||||||
// class chain resolution (the fake object's _className won't resolve
|
|
||||||
// to ShapeBase through the namespace parent chain).
|
|
||||||
for (let slot = 0; slot < 4; slot++) {
|
|
||||||
runtime.$.nsCall("ShapeBase", "stopThread", object, slot);
|
|
||||||
}
|
|
||||||
runtime.$.nsCall("ShapeBase", "playThread", object, 0, animation);
|
|
||||||
return () => {
|
|
||||||
for (let slot = 0; slot < 4; slot++) {
|
|
||||||
runtime.$.nsCall("ShapeBase", "stopThread", object, slot);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [runtime, object, animation]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShapeViewer({
|
|
||||||
shapeName,
|
|
||||||
runtime,
|
|
||||||
onAnimations,
|
|
||||||
selectedAnimation,
|
|
||||||
}: {
|
|
||||||
shapeName: string;
|
|
||||||
runtime: TorqueRuntime | null;
|
|
||||||
onAnimations: (anims: AnimationInfo[]) => void;
|
|
||||||
selectedAnimation: string;
|
|
||||||
}) {
|
|
||||||
const object = useMemo(
|
|
||||||
() => createFakeObject(runtime, shapeName),
|
|
||||||
[runtime, shapeName],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ShapeInfoProvider type="StaticShape" object={object} shapeName={shapeName}>
|
|
||||||
<Center>
|
|
||||||
<ShapeRenderer />
|
|
||||||
<AnimationReporter shapeName={shapeName} onAnimations={onAnimations} />
|
|
||||||
<AnimationPlayer
|
|
||||||
object={object}
|
|
||||||
runtime={runtime}
|
|
||||||
animation={selectedAnimation}
|
|
||||||
/>
|
|
||||||
<FitOnLoad />
|
|
||||||
</Center>
|
|
||||||
</ShapeInfoProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SceneLighting() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ambientLight intensity={0.6} />
|
|
||||||
<directionalLight position={[50, 80, 30]} intensity={1.2} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShapeInspector() {
|
|
||||||
const [currentShape, setCurrentShape] = useQueryState("shape", parseAsShape);
|
|
||||||
const runtime = useShapeRuntime();
|
|
||||||
const [availableAnimations, setAvailableAnimations] = useState<
|
|
||||||
AnimationInfo[]
|
|
||||||
>([]);
|
|
||||||
const [selectedAnimation, setSelectedAnimation] = useState<string>("");
|
|
||||||
|
|
||||||
const handleAnimations = useCallback((anims: AnimationInfo[]) => {
|
|
||||||
setAvailableAnimations(anims);
|
|
||||||
setSelectedAnimation("");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [showLoading, setShowLoading] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
if (runtime) {
|
|
||||||
const timer = setTimeout(() => setShowLoading(false), 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [runtime]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<main>
|
|
||||||
<SettingsProvider>
|
|
||||||
<div className={styles.CanvasContainer}>
|
|
||||||
{showLoading && (
|
|
||||||
<div
|
|
||||||
className={styles.LoadingIndicator}
|
|
||||||
data-complete={!!runtime}
|
|
||||||
>
|
|
||||||
<div className={styles.Spinner} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Canvas
|
|
||||||
frameloop="always"
|
|
||||||
gl={glSettings}
|
|
||||||
shadows={{ type: PCFShadowMap }}
|
|
||||||
scene={{ background: sceneBg }}
|
|
||||||
camera={{ position: [5, 3, 5], fov: 90 }}
|
|
||||||
>
|
|
||||||
<TickProvider>
|
|
||||||
<SceneLighting />
|
|
||||||
<Bounds fit clip observe margin={1.5}>
|
|
||||||
<Suspense>
|
|
||||||
<ShapeViewer
|
|
||||||
key={currentShape}
|
|
||||||
shapeName={currentShape}
|
|
||||||
runtime={runtime}
|
|
||||||
onAnimations={handleAnimations}
|
|
||||||
selectedAnimation={selectedAnimation}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</Bounds>
|
|
||||||
<DebugElements />
|
|
||||||
<OrbitControls makeDefault />
|
|
||||||
</TickProvider>
|
|
||||||
</Canvas>
|
|
||||||
</div>
|
|
||||||
<ShapeControls
|
|
||||||
currentShape={currentShape}
|
|
||||||
onChangeShape={setCurrentShape}
|
|
||||||
animations={availableAnimations}
|
|
||||||
selectedAnimation={selectedAnimation}
|
|
||||||
onChangeAnimation={setSelectedAnimation}
|
|
||||||
/>
|
|
||||||
</SettingsProvider>
|
|
||||||
</main>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShapeControls({
|
|
||||||
currentShape,
|
|
||||||
onChangeShape,
|
|
||||||
animations,
|
|
||||||
selectedAnimation,
|
|
||||||
onChangeAnimation,
|
|
||||||
}: {
|
|
||||||
currentShape: string;
|
|
||||||
onChangeShape: (shape: string) => void;
|
|
||||||
animations: AnimationInfo[];
|
|
||||||
selectedAnimation: string;
|
|
||||||
onChangeAnimation: (name: string) => void;
|
|
||||||
}) {
|
|
||||||
const { debugMode, setDebugMode } = useDebug();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.Sidebar}>
|
|
||||||
<div className={styles.SidebarSection}>
|
|
||||||
<ShapeSelect value={currentShape} onChange={onChangeShape} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.SidebarSection}>
|
|
||||||
<div className={styles.CheckboxField}>
|
|
||||||
<input
|
|
||||||
id="debugInput"
|
|
||||||
type="checkbox"
|
|
||||||
checked={debugMode}
|
|
||||||
onChange={(e) => setDebugMode(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="debugInput">Debug</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{animations.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className={styles.SidebarSection}>
|
|
||||||
<div className={styles.SectionLabel}>Animations</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.AnimationList}>
|
|
||||||
{animations.map((anim) => (
|
|
||||||
<div
|
|
||||||
key={anim.name}
|
|
||||||
className={styles.AnimationItem}
|
|
||||||
data-active={selectedAnimation === anim.name}
|
|
||||||
onClick={() =>
|
|
||||||
onChangeAnimation(
|
|
||||||
selectedAnimation === anim.name ? "" : anim.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className={styles.PlayButton}
|
|
||||||
title={`Play ${anim.alias ?? anim.name}`}
|
|
||||||
>
|
|
||||||
{selectedAnimation === anim.name ? "\u25A0" : "\u25B6"}
|
|
||||||
</button>
|
|
||||||
<span className={styles.AnimationName}>
|
|
||||||
{anim.alias ?? anim.name}
|
|
||||||
</span>
|
|
||||||
{anim.alias && (
|
|
||||||
<span
|
|
||||||
className={styles.ClipName}
|
|
||||||
title={`GLB clip: ${anim.name}`}
|
|
||||||
>
|
|
||||||
{anim.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{anim.cyclic === true && (
|
|
||||||
<span className={styles.CyclicIcon} title="Cyclic (looping)">
|
|
||||||
{"\u221E"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ShapesPage() {
|
|
||||||
return (
|
|
||||||
<Suspense>
|
|
||||||
<ShapeInspector />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: black;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*:before,
|
|
||||||
*:after {
|
|
||||||
box-sizing: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
"Segoe UI",
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
"Open Sans",
|
|
||||||
"Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="range"] {
|
|
||||||
max-width: 80px;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,10 +1,14 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<title>MapGenius – Explore maps for Tribes 2</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta
|
||||||
<title>t2-mapper</title>
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="description" content="Tribes 2 forever." />
|
||||||
|
<link rel="icon" type="image/png" href="/t2-mapper/icon.png" />
|
||||||
<script type="module" crossorigin src="/t2-mapper/assets/index-ClGJzuqQ.js"></script>
|
<script type="module" crossorigin src="/t2-mapper/assets/index-ClGJzuqQ.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/chunk-DECur_0Z.js">
|
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/chunk-DECur_0Z.js">
|
||||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/logger-DePRU8Hm.js">
|
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/logger-DePRU8Hm.js">
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -1,10 +1,14 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<title>MapGenius – Explore maps for Tribes 2</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta
|
||||||
<title>t2-mapper</title>
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<meta name="description" content="Tribes 2 forever." />
|
||||||
|
<link rel="icon" type="image/png" href="/icon.png" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
6
next-env.d.ts
vendored
6
next-env.d.ts
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
import "./.next/types/routes.d.ts";
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import { NextConfig } from "next";
|
|
||||||
import { PHASE_DEVELOPMENT_SERVER } from "next/constants";
|
|
||||||
|
|
||||||
const nextConfig = (phase, { defaultConfig }): NextConfig => {
|
|
||||||
return {
|
|
||||||
// Suppress static export config warnings in dev mode as they are not relevant.
|
|
||||||
output: phase === PHASE_DEVELOPMENT_SERVER ? undefined : "export",
|
|
||||||
distDir: "./docs",
|
|
||||||
basePath: "/t2-mapper",
|
|
||||||
assetPrefix: "/t2-mapper/",
|
|
||||||
trailingSlash: true,
|
|
||||||
reactCompiler: true,
|
|
||||||
experimental: { viewTransition: true },
|
|
||||||
headers:
|
|
||||||
// TorqueScript files should be served as text. This won't affect what
|
|
||||||
// GitHub Pages does with the static export, but it'll at least improve
|
|
||||||
// the dev server. Otherwise, the responses can't be easily inspected in
|
|
||||||
// the Network tab.
|
|
||||||
phase === PHASE_DEVELOPMENT_SERVER
|
|
||||||
? async () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/:path*.cs",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: "Content-Type",
|
|
||||||
value: "text/plain; charset=utf-8",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
// For the dev server, redirect / to the `basePath` for convenience, so you
|
|
||||||
// can just open localhost:3000.
|
|
||||||
redirects:
|
|
||||||
phase === PHASE_DEVELOPMENT_SERVER
|
|
||||||
? async () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/",
|
|
||||||
destination: "/t2-mapper/",
|
|
||||||
basePath: false,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -5,6 +5,7 @@ import babel from "@rolldown/plugin-babel";
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: "/t2-mapper/",
|
base: "/t2-mapper/",
|
||||||
|
server: { port: 3000 },
|
||||||
build: {
|
build: {
|
||||||
outDir: "docs",
|
outDir: "docs",
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue