mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-01-19 20:25:01 +00:00
add debug labels and some missing shapes
This commit is contained in:
parent
7557d2c48e
commit
878c798bcd
|
|
@ -10,6 +10,7 @@ import { InspectorControls } from "@/src/components/InspectorControls";
|
|||
import { SettingsProvider } from "@/src/components/SettingsProvider";
|
||||
import { ObserverCamera } from "@/src/components/ObserverCamera";
|
||||
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
|
||||
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||
|
|
@ -35,11 +36,12 @@ function MapInspector() {
|
|||
<QueryClientProvider client={queryClient}>
|
||||
<main>
|
||||
<SettingsProvider>
|
||||
<Canvas shadows>
|
||||
<Canvas shadows frameloop="always">
|
||||
<AudioProvider>
|
||||
<ObserverControls />
|
||||
<Mission key={missionName} name={missionName} />
|
||||
<ObserverCamera />
|
||||
<DebugElements />
|
||||
</AudioProvider>
|
||||
<EffectComposer>
|
||||
<N8AO intensity={3} aoRadius={3} quality="performance" />
|
||||
|
|
|
|||
|
|
@ -45,3 +45,15 @@ main {
|
|||
#speedInput {
|
||||
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 { useAudio } from "./AudioContext";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
|
||||
// Global audio buffer cache
|
||||
const audioBufferCache = new Map<string, AudioBuffer>();
|
||||
|
|
@ -204,12 +205,17 @@ export function AudioEmitter({ object }: { object: ConsoleObject }) {
|
|||
return debugMode ? (
|
||||
<mesh position={emitterPosRef.current}>
|
||||
<sphereGeometry args={[minDistance, 12, 12]} />
|
||||
<meshStandardMaterial
|
||||
<meshBasicMaterial
|
||||
color="#00ff00"
|
||||
wireframe
|
||||
opacity={0.2}
|
||||
transparent
|
||||
toneMapped={false}
|
||||
fog={false}
|
||||
/>
|
||||
<FloatingLabel color="#00ff00" position={[0, minDistance + 1, 0]}>
|
||||
{fileName}
|
||||
</FloatingLabel>
|
||||
</mesh>
|
||||
) : 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 { BASE_URL, shapeTextureToUrl, shapeToUrl } from "../loaders";
|
||||
import { filterGeometryByVertexGroups, getHullBoneIndices } from "../meshUtils";
|
||||
|
|
@ -8,6 +8,9 @@ import {
|
|||
} from "../shaderMaterials";
|
||||
import { MeshStandardMaterial } from "three";
|
||||
import { setupColor } from "../textureUtils";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { useShapeInfo } from "./ShapeInfoProvider";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
|
||||
const FALLBACK_URL = `${BASE_URL}/black.png`;
|
||||
|
||||
|
|
@ -27,7 +30,7 @@ export function ShapeTexture({
|
|||
shapeName?: string;
|
||||
}) {
|
||||
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) => {
|
||||
if (!isOrganic) {
|
||||
|
|
@ -36,80 +39,23 @@ export function ShapeTexture({
|
|||
return setupColor(texture);
|
||||
});
|
||||
|
||||
// Only use alpha-as-roughness material for borg shapes
|
||||
if (!isOrganic) {
|
||||
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
||||
shaderMaterial.map = texture;
|
||||
return <primitive object={shaderMaterial} attach="material" />;
|
||||
}
|
||||
const customMaterial = useMemo(() => {
|
||||
// Only use alpha-as-roughness material for borg shapes
|
||||
if (!isOrganic) {
|
||||
const shaderMaterial = createAlphaAsRoughnessMaterial();
|
||||
shaderMaterial.map = texture;
|
||||
return shaderMaterial;
|
||||
}
|
||||
|
||||
// For non-borg shapes, use the original GLTF material with updated texture
|
||||
const clonedMaterial = material.clone();
|
||||
clonedMaterial.map = texture;
|
||||
clonedMaterial.transparent = true;
|
||||
clonedMaterial.alphaTest = 0.9;
|
||||
return <primitive object={clonedMaterial} attach="material" />;
|
||||
}
|
||||
// For non-borg shapes, use the original GLTF material with updated texture
|
||||
const clonedMaterial = material.clone();
|
||||
clonedMaterial.map = texture;
|
||||
clonedMaterial.transparent = true;
|
||||
clonedMaterial.alphaTest = 0.9;
|
||||
return clonedMaterial;
|
||||
}, [material, texture, isOrganic]);
|
||||
|
||||
export function ShapeModel({ shapeName }: { shapeName: string }) {
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
return <primitive object={customMaterial} attach="material" />;
|
||||
}
|
||||
|
||||
export function ShapePlaceholder({ color }: { color: string }) {
|
||||
|
|
@ -120,3 +66,74 @@ export function ShapePlaceholder({ color }: { color: string }) {
|
|||
</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,
|
||||
} from "../mission";
|
||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
|
||||
const dataBlockToShapeName = {
|
||||
AmmoPack: "pack_upgrade_ammo.dts",
|
||||
|
|
@ -67,20 +68,22 @@ export function Item({ object }: { object: ConsoleObject }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="pink" />}>
|
||||
<ShapeModel shapeName={shapeName} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<ShapePlaceholder color="orange" />
|
||||
)}
|
||||
</group>
|
||||
<ShapeInfoProvider shapeName={shapeName} type="Item">
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="pink" />}>
|
||||
<ShapeModel />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<ShapePlaceholder color="orange" />
|
||||
)}
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ type PersistedSettings = {
|
|||
speedMultiplier?: number;
|
||||
fov?: number;
|
||||
audioEnabled?: boolean;
|
||||
debugMode?: boolean;
|
||||
};
|
||||
|
||||
export function useSettings() {
|
||||
|
|
@ -51,6 +53,12 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|||
} catch (err) {
|
||||
// Ignore.
|
||||
}
|
||||
if (savedSettings.debugMode != null) {
|
||||
setDebugMode(savedSettings.debugMode);
|
||||
}
|
||||
if (savedSettings.audioEnabled != null) {
|
||||
setAudioEnabled(savedSettings.audioEnabled);
|
||||
}
|
||||
if (savedSettings.fogEnabled != null) {
|
||||
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(() => {
|
||||
const settingsToSave: PersistedSettings = {
|
||||
fogEnabled,
|
||||
speedMultiplier,
|
||||
fov,
|
||||
};
|
||||
try {
|
||||
localStorage.setItem("settings", JSON.stringify(settingsToSave));
|
||||
} catch (err) {
|
||||
// Probably forbidden by browser settings.
|
||||
// Clear any pending save
|
||||
if (saveTimerRef.current) {
|
||||
clearTimeout(saveTimerRef.current);
|
||||
}
|
||||
}, [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 (
|
||||
<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,
|
||||
} from "../mission";
|
||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
|
||||
const dataBlockToShapeName = {
|
||||
Banner_Honor: "banner_honor.dts",
|
||||
|
|
@ -57,20 +58,22 @@ export function StaticShape({ object }: { object: ConsoleObject }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel shapeName={shapeName} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<ShapePlaceholder color="orange" />
|
||||
)}
|
||||
</group>
|
||||
<ShapeInfoProvider shapeName={shapeName} type="StaticShape">
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<ShapePlaceholder color="orange" />
|
||||
)}
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getScale,
|
||||
} from "../mission";
|
||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
|
||||
export function TSStatic({ object }: { object: ConsoleObject }) {
|
||||
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 q = useMemo(() => getRotation(object, true), [object]);
|
||||
|
||||
if (!shapeName) {
|
||||
console.error("<TSStatic> missing shapeName for object", object);
|
||||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel shapeName={shapeName} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</group>
|
||||
<ShapeInfoProvider shapeName={shapeName} type="TSStatic">
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getScale,
|
||||
} from "../mission";
|
||||
import { ShapeModel, ShapePlaceholder } from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
|
||||
const dataBlockToShapeName = {
|
||||
AABarrelLarge: "turret_aa_large.dts",
|
||||
|
|
@ -17,6 +18,7 @@ const dataBlockToShapeName = {
|
|||
PlasmaBarrelLarge: "turret_fusion_large.dts",
|
||||
SentryTurret: "turret_sentry.dts",
|
||||
TurretBaseLarge: "turret_base_large.dts",
|
||||
SentryTurretBarrel: "turret_muzzlepoint.dts",
|
||||
};
|
||||
|
||||
let _caseInsensitiveLookup: Record<string, string>;
|
||||
|
|
@ -46,33 +48,42 @@ export function Turret({ object }: { object: ConsoleObject }) {
|
|||
if (!shapeName) {
|
||||
console.error(`<Turret> missing shape for dataBlock: ${dataBlock}`);
|
||||
}
|
||||
if (!barrelShapeName) {
|
||||
console.error(
|
||||
`<Turret> missing shape for initialBarrel dataBlock: ${initialBarrel}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[-scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel shapeName={shapeName} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<ShapePlaceholder color="orange" />
|
||||
)}
|
||||
<group position={[0, 1.5, 0]}>
|
||||
{barrelShapeName ? (
|
||||
<ShapeInfoProvider shapeName={shapeName} type="Turret">
|
||||
<group
|
||||
quaternion={q}
|
||||
position={[x - 1024, y, z - 1024]}
|
||||
scale={[-scaleX, scaleY, scaleZ]}
|
||||
>
|
||||
{shapeName ? (
|
||||
<ErrorBoundary fallback={<ShapePlaceholder color="red" />}>
|
||||
<Suspense fallback={<ShapePlaceholder color="yellow" />}>
|
||||
<ShapeModel shapeName={barrelShapeName} />
|
||||
<ShapeModel />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<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>
|
||||
</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) {
|
||||
name = name.replace(/^skins\\/, "");
|
||||
name = name.replace(/\.\d+$/, "");
|
||||
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) {
|
||||
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]) {
|
||||
return resourcePath;
|
||||
}
|
||||
|
|
@ -57,8 +68,10 @@ export function getActualResourcePath(resourcePath: string) {
|
|||
return resourcePath;
|
||||
}
|
||||
|
||||
const _cachedResourceList = Object.keys(manifest).sort();
|
||||
|
||||
export function getResourceList() {
|
||||
return Object.keys(manifest).sort();
|
||||
return _cachedResourceList;
|
||||
}
|
||||
|
||||
export function getFilePath(resourcePath: string) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue