diff --git a/app/global.d.ts b/app/global.d.ts
deleted file mode 100644
index 6061bbfa..00000000
--- a/app/global.d.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/layout.tsx b/app/layout.tsx
deleted file mode 100644
index c7f9f897..00000000
--- a/app/layout.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {children}
-
-
-
- );
-}
diff --git a/app/page.tsx b/app/page.tsx
deleted file mode 100644
index de5c8ddf..00000000
--- a/app/page.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/shapes/page.module.css b/app/shapes/page.module.css
deleted file mode 100644
index e889f522..00000000
--- a/app/shapes/page.module.css
+++ /dev/null
@@ -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;
-}
diff --git a/app/shapes/page.tsx b/app/shapes/page.tsx
deleted file mode 100644
index 8c7a3dec..00000000
--- a/app/shapes/page.tsx
+++ /dev/null
@@ -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({
- 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(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();
- 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 | 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 (
-
-
-
-
-
-
-
-
- );
-}
-
-function SceneLighting() {
- return (
- <>
-
-
- >
- );
-}
-
-function ShapeInspector() {
- const [currentShape, setCurrentShape] = useQueryState("shape", parseAsShape);
- const runtime = useShapeRuntime();
- const [availableAnimations, setAvailableAnimations] = useState<
- AnimationInfo[]
- >([]);
- const [selectedAnimation, setSelectedAnimation] = useState("");
-
- 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 (
-
-
-
-
- {showLoading && (
-
- )}
-
-
-
-
-
-
- );
-}
-
-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 (
-
-
-
-
-
-
- setDebugMode(e.target.checked)}
- />
-
-
-
- {animations.length > 0 && (
- <>
-
-
- {animations.map((anim) => (
-
- onChangeAnimation(
- selectedAnimation === anim.name ? "" : anim.name,
- )
- }
- >
-
-
- {anim.alias ?? anim.name}
-
- {anim.alias && (
-
- {anim.name}
-
- )}
- {anim.cyclic === true && (
-
- {"\u221E"}
-
- )}
-
- ))}
-
- >
- )}
-
- );
-}
-
-export default function ShapesPage() {
- return (
-
-
-
- );
-}
diff --git a/app/style.css b/app/style.css
deleted file mode 100644
index 4c9aaf83..00000000
--- a/app/style.css
+++ /dev/null
@@ -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;
-}
diff --git a/app/icon.png b/docs/icon.png
similarity index 100%
rename from app/icon.png
rename to docs/icon.png
diff --git a/docs/index.html b/docs/index.html
index 4ca04b7a..cb668d9e 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,10 +1,14 @@
-
-
-
- t2-mapper
+
+ MapGenius – Explore maps for Tribes 2
+
+
+
diff --git a/index.html b/index.html
index 53096a18..0d6cc55d 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,14 @@
-
-
-
- t2-mapper
+
+ MapGenius – Explore maps for Tribes 2
+
+
+
diff --git a/next-env.d.ts b/next-env.d.ts
deleted file mode 100644
index 9edff1c7..00000000
--- a/next-env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-///
-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.
diff --git a/next.config.ts b/next.config.ts
deleted file mode 100644
index dc957e10..00000000
--- a/next.config.ts
+++ /dev/null
@@ -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;
diff --git a/public/icon.png b/public/icon.png
new file mode 100644
index 00000000..3b2cc797
Binary files /dev/null and b/public/icon.png differ
diff --git a/vite.config.ts b/vite.config.ts
index 119ae29a..ce5e3234 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,6 +5,7 @@ import babel from "@rolldown/plugin-babel";
// https://vite.dev/config/
export default defineConfig({
base: "/t2-mapper/",
+ server: { port: 3000 },
build: {
outDir: "docs",
emptyOutDir: false,