mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-15 08:24:51 +00:00
add debug labels and some missing shapes
This commit is contained in:
parent
7557d2c48e
commit
878c798bcd
21 changed files with 385 additions and 147 deletions
|
|
@ -10,6 +10,7 @@ import { InspectorControls } from "@/src/components/InspectorControls";
|
||||||
import { SettingsProvider } from "@/src/components/SettingsProvider";
|
import { SettingsProvider } from "@/src/components/SettingsProvider";
|
||||||
import { ObserverCamera } from "@/src/components/ObserverCamera";
|
import { ObserverCamera } from "@/src/components/ObserverCamera";
|
||||||
import { AudioProvider } from "@/src/components/AudioContext";
|
import { AudioProvider } from "@/src/components/AudioContext";
|
||||||
|
import { DebugElements } from "@/src/components/DebugElements";
|
||||||
|
|
||||||
// three.js has its own loaders for textures and models, but we need to load other
|
// 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.
|
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||||
|
|
@ -35,11 +36,12 @@ function MapInspector() {
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<main>
|
<main>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<Canvas shadows>
|
<Canvas shadows frameloop="always">
|
||||||
<AudioProvider>
|
<AudioProvider>
|
||||||
<ObserverControls />
|
<ObserverControls />
|
||||||
<Mission key={missionName} name={missionName} />
|
<Mission key={missionName} name={missionName} />
|
||||||
<ObserverCamera />
|
<ObserverCamera />
|
||||||
|
<DebugElements />
|
||||||
</AudioProvider>
|
</AudioProvider>
|
||||||
<EffectComposer>
|
<EffectComposer>
|
||||||
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,15 @@ main {
|
||||||
#speedInput {
|
#speedInput {
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.StaticShapeLabel {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StatsPanel {
|
||||||
|
left: auto !important;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
docs/base/@vl2/shapes.vl2/shapes/borg5.glb
Normal file
BIN
docs/base/@vl2/shapes.vl2/shapes/borg5.glb
Normal file
Binary file not shown.
BIN
docs/base/@vl2/shapes.vl2/shapes/porg2.glb
Normal file
BIN
docs/base/@vl2/shapes.vl2/shapes/porg2.glb
Normal file
Binary file not shown.
BIN
docs/base/@vl2/shapes.vl2/shapes/turret_muzzlepoint.glb
Normal file
BIN
docs/base/@vl2/shapes.vl2/shapes/turret_muzzlepoint.glb
Normal file
Binary file not shown.
BIN
docs/base/@vl2/shapes.vl2/shapes/xorg3.glb
Normal file
BIN
docs/base/@vl2/shapes.vl2/shapes/xorg3.glb
Normal file
Binary file not shown.
BIN
docs/base/@vl2/shapes.vl2/shapes/xorg5.glb
Normal file
BIN
docs/base/@vl2/shapes.vl2/shapes/xorg5.glb
Normal file
Binary file not shown.
|
|
@ -5,6 +5,7 @@ import { ConsoleObject, getPosition, getProperty } from "../mission";
|
||||||
import { audioToUrl } from "../loaders";
|
import { audioToUrl } from "../loaders";
|
||||||
import { useAudio } from "./AudioContext";
|
import { useAudio } from "./AudioContext";
|
||||||
import { useSettings } from "./SettingsProvider";
|
import { useSettings } from "./SettingsProvider";
|
||||||
|
import { FloatingLabel } from "./FloatingLabel";
|
||||||
|
|
||||||
// Global audio buffer cache
|
// Global audio buffer cache
|
||||||
const audioBufferCache = new Map<string, AudioBuffer>();
|
const audioBufferCache = new Map<string, AudioBuffer>();
|
||||||
|
|
@ -204,12 +205,17 @@ export function AudioEmitter({ object }: { object: ConsoleObject }) {
|
||||||
return debugMode ? (
|
return debugMode ? (
|
||||||
<mesh position={emitterPosRef.current}>
|
<mesh position={emitterPosRef.current}>
|
||||||
<sphereGeometry args={[minDistance, 12, 12]} />
|
<sphereGeometry args={[minDistance, 12, 12]} />
|
||||||
<meshStandardMaterial
|
<meshBasicMaterial
|
||||||
color="#00ff00"
|
color="#00ff00"
|
||||||
wireframe
|
wireframe
|
||||||
|
opacity={0.2}
|
||||||
|
transparent
|
||||||
toneMapped={false}
|
toneMapped={false}
|
||||||
fog={false}
|
fog={false}
|
||||||
/>
|
/>
|
||||||
|
<FloatingLabel color="#00ff00" position={[0, minDistance + 1, 0]}>
|
||||||
|
{fileName}
|
||||||
|
</FloatingLabel>
|
||||||
</mesh>
|
</mesh>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
src/components/DebugElements.tsx
Normal file
12
src/components/DebugElements.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Stats } from "@react-three/drei";
|
||||||
|
import { useSettings } from "./SettingsProvider";
|
||||||
|
|
||||||
|
export function DebugElements() {
|
||||||
|
const { debugMode } = useSettings();
|
||||||
|
|
||||||
|
return debugMode ? (
|
||||||
|
<>
|
||||||
|
<Stats className="StatsPanel" />
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
58
src/components/FloatingLabel.tsx
Normal file
58
src/components/FloatingLabel.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import { Object3D } from "three";
|
||||||
|
import { useDistanceFromCamera } from "./useDistanceFromCamera";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { Html } from "@react-three/drei";
|
||||||
|
|
||||||
|
const DEFAULT_POSITION = [0, 0, 0] as [x: number, y: number, z: number];
|
||||||
|
|
||||||
|
export function FloatingLabel({
|
||||||
|
children,
|
||||||
|
color = "white",
|
||||||
|
position = DEFAULT_POSITION,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
color?: string;
|
||||||
|
position?: [x: number, y: number, z: number];
|
||||||
|
}) {
|
||||||
|
const groupRef = useRef<Object3D>(null);
|
||||||
|
const distanceRef = useDistanceFromCamera(groupRef);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const labelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Initialize opacity when label ref is attached
|
||||||
|
useEffect(() => {
|
||||||
|
if (labelRef.current && distanceRef.current != null) {
|
||||||
|
const opacity = Math.max(0, Math.min(1, 1 - distanceRef.current / 200));
|
||||||
|
labelRef.current.style.opacity = opacity.toString();
|
||||||
|
}
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
const distance = distanceRef.current;
|
||||||
|
const shouldBeVisible = distance != null && distance < 200;
|
||||||
|
|
||||||
|
// Update visibility state only when crossing threshold
|
||||||
|
if (isVisible !== shouldBeVisible) {
|
||||||
|
setIsVisible(shouldBeVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update opacity directly on DOM element (no re-render)
|
||||||
|
if (labelRef.current && shouldBeVisible) {
|
||||||
|
const opacity = Math.max(0, Math.min(1, 1 - distance / 200));
|
||||||
|
labelRef.current.style.opacity = opacity.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
{isVisible ? (
|
||||||
|
<Html position={position} center>
|
||||||
|
<div ref={labelRef} className="StaticShapeLabel" style={{ color }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Html>
|
||||||
|
) : null}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Suspense } from "react";
|
import { Suspense, useMemo } from "react";
|
||||||
import { useGLTF, useTexture } from "@react-three/drei";
|
import { useGLTF, useTexture } from "@react-three/drei";
|
||||||
import { BASE_URL, shapeTextureToUrl, shapeToUrl } from "../loaders";
|
import { BASE_URL, shapeTextureToUrl, shapeToUrl } from "../loaders";
|
||||||
import { filterGeometryByVertexGroups, getHullBoneIndices } from "../meshUtils";
|
import { filterGeometryByVertexGroups, getHullBoneIndices } from "../meshUtils";
|
||||||
|
|
@ -8,6 +8,9 @@ import {
|
||||||
} from "../shaderMaterials";
|
} from "../shaderMaterials";
|
||||||
import { MeshStandardMaterial } from "three";
|
import { MeshStandardMaterial } from "three";
|
||||||
import { setupColor } from "../textureUtils";
|
import { setupColor } from "../textureUtils";
|
||||||
|
import { useSettings } from "./SettingsProvider";
|
||||||
|
import { useShapeInfo } from "./ShapeInfoProvider";
|
||||||
|
import { FloatingLabel } from "./FloatingLabel";
|
||||||
|
|
||||||
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||||
|
|
||||||
|
|
@ -27,7 +30,7 @@ export function ShapeTexture({
|
||||||
shapeName?: string;
|
shapeName?: string;
|
||||||
}) {
|
}) {
|
||||||
const url = shapeTextureToUrl(material.name, FALLBACK_URL);
|
const url = shapeTextureToUrl(material.name, FALLBACK_URL);
|
||||||
const isOrganic = shapeName && /borg|xorg/i.test(shapeName);
|
const isOrganic = shapeName && /borg|xorg|porg/i.test(shapeName);
|
||||||
|
|
||||||
const texture = useTexture(url, (texture) => {
|
const texture = useTexture(url, (texture) => {
|
||||||
if (!isOrganic) {
|
if (!isOrganic) {
|
||||||
|
|
@ -36,80 +39,23 @@ export function ShapeTexture({
|
||||||
return setupColor(texture);
|
return setupColor(texture);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only use alpha-as-roughness material for borg shapes
|
const customMaterial = useMemo(() => {
|
||||||
if (!isOrganic) {
|
// Only use alpha-as-roughness material for borg shapes
|
||||||
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
if (!isOrganic) {
|
||||||
shaderMaterial.map = texture;
|
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
||||||
return <primitive object={shaderMaterial} attach="material" />;
|
shaderMaterial.map = texture;
|
||||||
}
|
return shaderMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
// For non-borg shapes, use the original GLTF material with updated texture
|
// For non-borg shapes, use the original GLTF material with updated texture
|
||||||
const clonedMaterial = material.clone();
|
const clonedMaterial = material.clone();
|
||||||
clonedMaterial.map = texture;
|
clonedMaterial.map = texture;
|
||||||
clonedMaterial.transparent = true;
|
clonedMaterial.transparent = true;
|
||||||
clonedMaterial.alphaTest = 0.9;
|
clonedMaterial.alphaTest = 0.9;
|
||||||
return <primitive object={clonedMaterial} attach="material" />;
|
return clonedMaterial;
|
||||||
}
|
}, [material, texture, isOrganic]);
|
||||||
|
|
||||||
export function ShapeModel({ shapeName }: { shapeName: string }) {
|
return <primitive object={customMaterial} attach="material" />;
|
||||||
const { nodes } = useStaticShape(shapeName);
|
|
||||||
|
|
||||||
let hullBoneIndices = new Set<number>();
|
|
||||||
const skeletonsFound = Object.values(nodes).filter(
|
|
||||||
(node: any) => node.skeleton
|
|
||||||
);
|
|
||||||
|
|
||||||
if (skeletonsFound.length > 0) {
|
|
||||||
const skeleton = (skeletonsFound[0] as any).skeleton;
|
|
||||||
hullBoneIndices = getHullBoneIndices(skeleton);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Object.entries(nodes)
|
|
||||||
.filter(
|
|
||||||
([name, node]: [string, any]) =>
|
|
||||||
node.material &&
|
|
||||||
node.material.name !== "Unassigned" &&
|
|
||||||
!node.name.match(/^Hulk/i)
|
|
||||||
)
|
|
||||||
.map(([name, node]: [string, any]) => {
|
|
||||||
const geometry = filterGeometryByVertexGroups(
|
|
||||||
node.geometry,
|
|
||||||
hullBoneIndices
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mesh key={node.id} geometry={geometry} castShadow receiveShadow>
|
|
||||||
{node.material ? (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
// Allow the mesh to render while the texture is still loading;
|
|
||||||
// show a wireframe placeholder.
|
|
||||||
<meshStandardMaterial color="gray" wireframe />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Array.isArray(node.material) ? (
|
|
||||||
node.material.map((mat, index) => (
|
|
||||||
<ShapeTexture
|
|
||||||
key={index}
|
|
||||||
material={mat as MeshStandardMaterial}
|
|
||||||
shapeName={shapeName}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<ShapeTexture
|
|
||||||
material={node.material as MeshStandardMaterial}
|
|
||||||
shapeName={shapeName}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
) : null}
|
|
||||||
</mesh>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShapePlaceholder({ color }: { color: string }) {
|
export function ShapePlaceholder({ color }: { color: string }) {
|
||||||
|
|
@ -120,3 +66,74 @@ export function ShapePlaceholder({ color }: { color: string }) {
|
||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StaticShapeType = "StaticShape" | "TSStatic" | "Item" | "Turret";
|
||||||
|
|
||||||
|
export function ShapeModel() {
|
||||||
|
const { shapeName } = useShapeInfo();
|
||||||
|
const { debugMode } = useSettings();
|
||||||
|
const { nodes } = useStaticShape(shapeName);
|
||||||
|
|
||||||
|
const hullBoneIndices = useMemo(() => {
|
||||||
|
const skeletonsFound = Object.values(nodes).filter(
|
||||||
|
(node: any) => node.skeleton
|
||||||
|
);
|
||||||
|
|
||||||
|
if (skeletonsFound.length > 0) {
|
||||||
|
const skeleton = (skeletonsFound[0] as any).skeleton;
|
||||||
|
return getHullBoneIndices(skeleton);
|
||||||
|
}
|
||||||
|
return new Set<number>();
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
|
const processedNodes = useMemo(() => {
|
||||||
|
return Object.entries(nodes)
|
||||||
|
.filter(
|
||||||
|
([name, node]: [string, any]) =>
|
||||||
|
node.material &&
|
||||||
|
node.material.name !== "Unassigned" &&
|
||||||
|
!node.name.match(/^Hulk/i)
|
||||||
|
)
|
||||||
|
.map(([name, node]: [string, any]) => {
|
||||||
|
const geometry = filterGeometryByVertexGroups(
|
||||||
|
node.geometry,
|
||||||
|
hullBoneIndices
|
||||||
|
);
|
||||||
|
return { node, geometry };
|
||||||
|
});
|
||||||
|
}, [nodes, hullBoneIndices]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{processedNodes.map(({ node, geometry }) => (
|
||||||
|
<mesh key={node.id} geometry={geometry} castShadow receiveShadow>
|
||||||
|
{node.material ? (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
// Allow the mesh to render while the texture is still loading;
|
||||||
|
// show a wireframe placeholder.
|
||||||
|
<meshStandardMaterial color="gray" wireframe />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Array.isArray(node.material) ? (
|
||||||
|
node.material.map((mat, index) => (
|
||||||
|
<ShapeTexture
|
||||||
|
key={index}
|
||||||
|
material={mat as MeshStandardMaterial}
|
||||||
|
shapeName={shapeName}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<ShapeTexture
|
||||||
|
material={node.material as MeshStandardMaterial}
|
||||||
|
shapeName={shapeName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{debugMode ? <FloatingLabel>{shapeName}</FloatingLabel> : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getScale,
|
getScale,
|
||||||
} from "../mission";
|
} from "../mission";
|
||||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||||
|
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||||
|
|
||||||
const dataBlockToShapeName = {
|
const dataBlockToShapeName = {
|
||||||
AmmoPack: "pack_upgrade_ammo.dts",
|
AmmoPack: "pack_upgrade_ammo.dts",
|
||||||
|
|
@ -67,20 +68,22 @@ export function Item({ object }: { object: ConsoleObject }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<ShapeInfoProvider shapeName={shapeName} type="Item">
|
||||||
quaternion={q}
|
<group
|
||||||
position={[x - 1024, y, z - 1024]}
|
quaternion={q}
|
||||||
scale={[scaleX, scaleY, scaleZ]}
|
position={[x - 1024, y, z - 1024]}
|
||||||
>
|
scale={[scaleX, scaleY, scaleZ]}
|
||||||
{shapeName ? (
|
>
|
||||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
{shapeName ? (
|
||||||
<Suspense fallback={<ShapePlaceholder color="pink" />}>
|
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||||
<ShapeModel shapeName={shapeName} />
|
<Suspense fallback={<ShapePlaceholder color="pink" />}>
|
||||||
</Suspense>
|
<ShapeModel />
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
) : (
|
</ErrorBoundary>
|
||||||
<ShapePlaceholder color="orange" />
|
) : (
|
||||||
)}
|
<ShapePlaceholder color="orange" />
|
||||||
</group>
|
)}
|
||||||
|
</group>
|
||||||
|
</ShapeInfoProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ type PersistedSettings = {
|
||||||
speedMultiplier?: number;
|
speedMultiplier?: number;
|
||||||
fov?: number;
|
fov?: number;
|
||||||
audioEnabled?: boolean;
|
audioEnabled?: boolean;
|
||||||
|
debugMode?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
|
|
@ -51,6 +53,12 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore.
|
// Ignore.
|
||||||
}
|
}
|
||||||
|
if (savedSettings.debugMode != null) {
|
||||||
|
setDebugMode(savedSettings.debugMode);
|
||||||
|
}
|
||||||
|
if (savedSettings.audioEnabled != null) {
|
||||||
|
setAudioEnabled(savedSettings.audioEnabled);
|
||||||
|
}
|
||||||
if (savedSettings.fogEnabled != null) {
|
if (savedSettings.fogEnabled != null) {
|
||||||
setFogEnabled(savedSettings.fogEnabled);
|
setFogEnabled(savedSettings.fogEnabled);
|
||||||
}
|
}
|
||||||
|
|
@ -62,19 +70,37 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Persist settings to localStoarge.
|
// Persist settings to localStorage with debouncing to avoid excessive writes
|
||||||
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const settingsToSave: PersistedSettings = {
|
// Clear any pending save
|
||||||
fogEnabled,
|
if (saveTimerRef.current) {
|
||||||
speedMultiplier,
|
clearTimeout(saveTimerRef.current);
|
||||||
fov,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
localStorage.setItem("settings", JSON.stringify(settingsToSave));
|
|
||||||
} catch (err) {
|
|
||||||
// Probably forbidden by browser settings.
|
|
||||||
}
|
}
|
||||||
}, [fogEnabled, speedMultiplier, fov]);
|
|
||||||
|
// Debounce localStorage writes (wait 300ms after last change)
|
||||||
|
saveTimerRef.current = setTimeout(() => {
|
||||||
|
const settingsToSave: PersistedSettings = {
|
||||||
|
fogEnabled,
|
||||||
|
speedMultiplier,
|
||||||
|
fov,
|
||||||
|
audioEnabled,
|
||||||
|
debugMode,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
localStorage.setItem("settings", JSON.stringify(settingsToSave));
|
||||||
|
} catch (err) {
|
||||||
|
// Probably forbidden by browser settings.
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (saveTimerRef.current) {
|
||||||
|
clearTimeout(saveTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fogEnabled, speedMultiplier, fov, audioEnabled, debugMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider value={value}>
|
<SettingsContext.Provider value={value}>
|
||||||
|
|
|
||||||
27
src/components/ShapeInfoProvider.tsx
Normal file
27
src/components/ShapeInfoProvider.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||||
|
|
||||||
|
export type StaticShapeType = "TSStatic" | "StaticShape" | "Item" | "Turret";
|
||||||
|
|
||||||
|
const ShapeInfoContext = createContext(null);
|
||||||
|
|
||||||
|
export function useShapeInfo() {
|
||||||
|
return useContext(ShapeInfoContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShapeInfoProvider({
|
||||||
|
children,
|
||||||
|
shapeName,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
shapeName: string;
|
||||||
|
type: StaticShapeType;
|
||||||
|
}) {
|
||||||
|
const context = useMemo(() => ({ shapeName, type }), [shapeName, type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShapeInfoContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</ShapeInfoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getScale,
|
getScale,
|
||||||
} from "../mission";
|
} from "../mission";
|
||||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||||
|
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||||
|
|
||||||
const dataBlockToShapeName = {
|
const dataBlockToShapeName = {
|
||||||
Banner_Honor: "banner_honor.dts",
|
Banner_Honor: "banner_honor.dts",
|
||||||
|
|
@ -57,20 +58,22 @@ export function StaticShape({ object }: { object: ConsoleObject }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<ShapeInfoProvider shapeName={shapeName} type="StaticShape">
|
||||||
quaternion={q}
|
<group
|
||||||
position={[x - 1024, y, z - 1024]}
|
quaternion={q}
|
||||||
scale={[scaleX, scaleY, scaleZ]}
|
position={[x - 1024, y, z - 1024]}
|
||||||
>
|
scale={[scaleX, scaleY, scaleZ]}
|
||||||
{shapeName ? (
|
>
|
||||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
{shapeName ? (
|
||||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||||
<ShapeModel shapeName={shapeName} />
|
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||||
</Suspense>
|
<ShapeModel />
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
) : (
|
</ErrorBoundary>
|
||||||
<ShapePlaceholder color="orange" />
|
) : (
|
||||||
)}
|
<ShapePlaceholder color="orange" />
|
||||||
</group>
|
)}
|
||||||
|
</group>
|
||||||
|
</ShapeInfoProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getScale,
|
getScale,
|
||||||
} from "../mission";
|
} from "../mission";
|
||||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||||
|
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||||
|
|
||||||
export function TSStatic({ object }: { object: ConsoleObject }) {
|
export function TSStatic({ object }: { object: ConsoleObject }) {
|
||||||
const shapeName = getProperty(object, "shapeName").value;
|
const shapeName = getProperty(object, "shapeName").value;
|
||||||
|
|
@ -16,17 +17,23 @@ export function TSStatic({ object }: { object: ConsoleObject }) {
|
||||||
const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
|
const [scaleX, scaleY, scaleZ] = useMemo(() => getScale(object), [object]);
|
||||||
const q = useMemo(() => getRotation(object, true), [object]);
|
const q = useMemo(() => getRotation(object, true), [object]);
|
||||||
|
|
||||||
|
if (!shapeName) {
|
||||||
|
console.error("<TSStatic> missing shapeName for object", object);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<ShapeInfoProvider shapeName={shapeName} type="TSStatic">
|
||||||
quaternion={q}
|
<group
|
||||||
position={[x - 1024, y, z - 1024]}
|
quaternion={q}
|
||||||
scale={[scaleX, scaleY, scaleZ]}
|
position={[x - 1024, y, z - 1024]}
|
||||||
>
|
scale={[scaleX, scaleY, scaleZ]}
|
||||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
>
|
||||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||||
<ShapeModel shapeName={shapeName} />
|
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||||
</Suspense>
|
<ShapeModel />
|
||||||
</ErrorBoundary>
|
</Suspense>
|
||||||
</group>
|
</ErrorBoundary>
|
||||||
|
</group>
|
||||||
|
</ShapeInfoProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getScale,
|
getScale,
|
||||||
} from "../mission";
|
} from "../mission";
|
||||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||||
|
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||||
|
|
||||||
const dataBlockToShapeName = {
|
const dataBlockToShapeName = {
|
||||||
AABarrelLarge: "turret_aa_large.dts",
|
AABarrelLarge: "turret_aa_large.dts",
|
||||||
|
|
@ -17,6 +18,7 @@ const dataBlockToShapeName = {
|
||||||
PlasmaBarrelLarge: "turret_fusion_large.dts",
|
PlasmaBarrelLarge: "turret_fusion_large.dts",
|
||||||
SentryTurret: "turret_sentry.dts",
|
SentryTurret: "turret_sentry.dts",
|
||||||
TurretBaseLarge: "turret_base_large.dts",
|
TurretBaseLarge: "turret_base_large.dts",
|
||||||
|
SentryTurretBarrel: "turret_muzzlepoint.dts",
|
||||||
};
|
};
|
||||||
|
|
||||||
let _caseInsensitiveLookup: Record<string, string>;
|
let _caseInsensitiveLookup: Record<string, string>;
|
||||||
|
|
@ -46,33 +48,42 @@ export function Turret({ object }: { object: ConsoleObject }) {
|
||||||
if (!shapeName) {
|
if (!shapeName) {
|
||||||
console.error(`<Turret> missing shape for dataBlock: ${dataBlock}`);
|
console.error(`<Turret> missing shape for dataBlock: ${dataBlock}`);
|
||||||
}
|
}
|
||||||
|
if (!barrelShapeName) {
|
||||||
|
console.error(
|
||||||
|
`<Turret> missing shape for initialBarrel dataBlock: ${initialBarrel}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<ShapeInfoProvider shapeName={shapeName} type="Turret">
|
||||||
quaternion={q}
|
<group
|
||||||
position={[x - 1024, y, z - 1024]}
|
quaternion={q}
|
||||||
scale={[-scaleX, scaleY, scaleZ]}
|
position={[x - 1024, y, z - 1024]}
|
||||||
>
|
scale={[-scaleX, scaleY, scaleZ]}
|
||||||
{shapeName ? (
|
>
|
||||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
{shapeName ? (
|
||||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
|
||||||
<ShapeModel shapeName={shapeName} />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
) : (
|
|
||||||
<ShapePlaceholder color="orange" />
|
|
||||||
)}
|
|
||||||
<group position={[0, 1.5, 0]}>
|
|
||||||
{barrelShapeName ? (
|
|
||||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||||
<ShapeModel shapeName={barrelShapeName} />
|
<ShapeModel />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : (
|
) : (
|
||||||
<ShapePlaceholder color="orange" />
|
<ShapePlaceholder color="orange" />
|
||||||
)}
|
)}
|
||||||
|
<ShapeInfoProvider shapeName={barrelShapeName} type="Turret">
|
||||||
|
<group position={[0, 1.5, 0]}>
|
||||||
|
{barrelShapeName ? (
|
||||||
|
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||||
|
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||||
|
<ShapeModel />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
) : (
|
||||||
|
<ShapePlaceholder color="orange" />
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
</ShapeInfoProvider>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</ShapeInfoProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
src/components/useDistanceFromCamera.ts
Normal file
22
src/components/useDistanceFromCamera.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { RefObject, useRef } from "react";
|
||||||
|
import { Object3D } from "three";
|
||||||
|
import { useWorldPosition } from "./useWorldPosition";
|
||||||
|
|
||||||
|
export function useDistanceFromCamera<T extends Object3D>(
|
||||||
|
ref: RefObject<T>
|
||||||
|
): RefObject<number> {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const distanceRef = useRef<number>(null);
|
||||||
|
const worldPosRef = useWorldPosition(ref);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!worldPosRef.current) {
|
||||||
|
distanceRef.current = null;
|
||||||
|
} else {
|
||||||
|
distanceRef.current = camera.position.distanceTo(worldPosRef.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return distanceRef;
|
||||||
|
}
|
||||||
18
src/components/useWorldPosition.ts
Normal file
18
src/components/useWorldPosition.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import { useRef, RefObject } from "react";
|
||||||
|
import { Object3D, Vector3 } from "three";
|
||||||
|
|
||||||
|
export function useWorldPosition<T extends Object3D>(
|
||||||
|
ref: RefObject<T>
|
||||||
|
): RefObject<Vector3 | null> {
|
||||||
|
const worldPositionRef = useRef<Vector3 | null>(null);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
worldPositionRef.current ??= new Vector3();
|
||||||
|
ref.current.getWorldPosition(worldPositionRef.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return worldPositionRef;
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ export function textureFrameToUrl(fileName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shapeTextureToUrl(name: string, fallbackUrl?: string) {
|
export function shapeTextureToUrl(name: string, fallbackUrl?: string) {
|
||||||
|
name = name.replace(/^skins\\/, "");
|
||||||
name = name.replace(/\.\d+$/, "");
|
name = name.replace(/\.\d+$/, "");
|
||||||
return getUrlForPath(`textures/skins/${name}.png`, fallbackUrl);
|
return getUrlForPath(`textures/skins/${name}.png`, fallbackUrl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,18 @@ export function getSource(resourcePath: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _resourcePathCache = new Map();
|
||||||
|
|
||||||
export function getActualResourcePath(resourcePath: string) {
|
export function getActualResourcePath(resourcePath: string) {
|
||||||
|
if (_resourcePathCache.has(resourcePath)) {
|
||||||
|
return _resourcePathCache.get(resourcePath);
|
||||||
|
}
|
||||||
|
const actualResourcePath = getActualResourcePathUncached(resourcePath);
|
||||||
|
_resourcePathCache.set(resourcePath, actualResourcePath);
|
||||||
|
return actualResourcePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActualResourcePathUncached(resourcePath: string) {
|
||||||
if (manifest[resourcePath]) {
|
if (manifest[resourcePath]) {
|
||||||
return resourcePath;
|
return resourcePath;
|
||||||
}
|
}
|
||||||
|
|
@ -57,8 +68,10 @@ export function getActualResourcePath(resourcePath: string) {
|
||||||
return resourcePath;
|
return resourcePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _cachedResourceList = Object.keys(manifest).sort();
|
||||||
|
|
||||||
export function getResourceList() {
|
export function getResourceList() {
|
||||||
return Object.keys(manifest).sort();
|
return _cachedResourceList;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilePath(resourcePath: string) {
|
export function getFilePath(resourcePath: string) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue