From 878c798bcded49a3def6597257f93f7eda407d37 Mon Sep 17 00:00:00 2001 From: Brian Beck Date: Sun, 23 Nov 2025 21:47:49 -0800 Subject: [PATCH] add debug labels and some missing shapes --- app/page.tsx | 4 +- app/style.css | 12 ++ docs/base/@vl2/shapes.vl2/shapes/borg5.glb | Bin 0 -> 6216 bytes docs/base/@vl2/shapes.vl2/shapes/porg2.glb | Bin 0 -> 2072 bytes .../shapes.vl2/shapes/turret_muzzlepoint.glb | Bin 0 -> 1264 bytes docs/base/@vl2/shapes.vl2/shapes/xorg3.glb | Bin 0 -> 9792 bytes docs/base/@vl2/shapes.vl2/shapes/xorg5.glb | Bin 0 -> 6336 bytes src/components/AudioEmitter.tsx | 8 +- src/components/DebugElements.tsx | 12 ++ src/components/FloatingLabel.tsx | 58 ++++++ src/components/GenericShape.tsx | 165 ++++++++++-------- src/components/Item.tsx | 33 ++-- src/components/SettingsProvider.tsx | 48 +++-- src/components/ShapeInfoProvider.tsx | 27 +++ src/components/StaticShape.tsx | 33 ++-- src/components/TSStatic.tsx | 29 +-- src/components/Turret.tsx | 47 +++-- src/components/useDistanceFromCamera.ts | 22 +++ src/components/useWorldPosition.ts | 18 ++ src/loaders.ts | 1 + src/manifest.ts | 15 +- 21 files changed, 385 insertions(+), 147 deletions(-) create mode 100644 docs/base/@vl2/shapes.vl2/shapes/borg5.glb create mode 100644 docs/base/@vl2/shapes.vl2/shapes/porg2.glb create mode 100644 docs/base/@vl2/shapes.vl2/shapes/turret_muzzlepoint.glb create mode 100644 docs/base/@vl2/shapes.vl2/shapes/xorg3.glb create mode 100644 docs/base/@vl2/shapes.vl2/shapes/xorg5.glb create mode 100644 src/components/DebugElements.tsx create mode 100644 src/components/FloatingLabel.tsx create mode 100644 src/components/ShapeInfoProvider.tsx create mode 100644 src/components/useDistanceFromCamera.ts create mode 100644 src/components/useWorldPosition.ts diff --git a/app/page.tsx b/app/page.tsx index 00e1b62d..345c9d70 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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() {
- + + diff --git a/app/style.css b/app/style.css index d5d0e6f4..6165fbfb 100644 --- a/app/style.css +++ b/app/style.css @@ -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; +} diff --git a/docs/base/@vl2/shapes.vl2/shapes/borg5.glb b/docs/base/@vl2/shapes.vl2/shapes/borg5.glb new file mode 100644 index 0000000000000000000000000000000000000000..39de358ba42aefef0644921f77daad079d0e036c GIT binary patch literal 6216 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 literal 0 HcmV?d00001 diff --git a/docs/base/@vl2/shapes.vl2/shapes/porg2.glb b/docs/base/@vl2/shapes.vl2/shapes/porg2.glb new file mode 100644 index 0000000000000000000000000000000000000000..808ad84fb337aef8fde5a6a114c2b439bf44b4b5 GIT binary patch literal 2072 zcmb7FeQXp}5a0GF2QAQ61X`@2Ss|A8dfo4j>vf-{P-%PZk@jf7w$Q!n_Ih&NZFaXn zt&I|bil~9aXrn|vR0t-GLBS|3yB0M3L6rCpAsR7?CMsx@NJ3Jv&RYu9kci&qy*F>( z%>3s4X7)Cjj@4!o1W_=HATG=xi29aj(;%r@mS&ToK{Bc7nyK1`NruRV9@Eebt2hZR z#VgX9-ldtvbrsR#&0LTVauOLJH*2PqGITIzf)qIvAgu%hAVbuYWRan^K~h(HK}WWD zX8}?-x-@)1bqsaf*$tAtn$_dEPTBVvW)d)LQ`N1sYI`_sX@eGunrg(v;Uz2MMfwrVEvyUud50F}SR z#l3yKVE@&d<{fySBc&5Ll6>E7%e7N!`Oc5V1CfW|E@Ug{%=6?IKS z=k5h#$1i{5d^}!XxqW#vnsL3Xs&@b0%8GXug-<_MR<%~FbN4;@2#Ob$Rc&iIPFwyt z?^}@r@|j2{`s3FTr5tQFY&@yFvGkSjReL*%%s=P)uYK3NvrcI5&&v3-4epMSBBUEW)N!NG?ewiiyPPFxTVXuBGf{+4Bnv${Qs7>_ zlt#;PPbnSu`@%d(g!C+zX;RGUYq!5%(uUYuXvWj#B3 z|FGxxJH#oP*o3~gGVIyEw39`rz<%^Bfw2DKLKdBY{rTgsd*_)%7JMBA VinGszjOW0d1tSCWb79UQ{ss~ZbJhR= literal 0 HcmV?d00001 diff --git a/docs/base/@vl2/shapes.vl2/shapes/turret_muzzlepoint.glb b/docs/base/@vl2/shapes.vl2/shapes/turret_muzzlepoint.glb new file mode 100644 index 0000000000000000000000000000000000000000..c5de993374fffdf0857399a2e793a962df75660b GIT binary patch literal 1264 zcmb7E>uTFD6!yCH1^NU=zdcWu9pCn&q-hJ;l6WMc3_>Z6qc{e~3bK-|m_T7~wb$tj z>;X2|k>WII3ri+!IJfUyJ{>#Fhu0^Xru}-SX*s-~22rme_==v(xOW zW!?3@&%5G$d@pE+7O8+Ww}Eb{N7JeL^k9!f%v1Rl+D_oY2$iXh#S-UvSq}(0 z7W<;(;6+(muKZ^Ddh)$-E$qNj>bgFIf6qm`r?IoQA3>DoVSe`GrxgC2K8A~0{(btW VY~VhZzRfu#Z~YJFC)yhY`3r7sZ5sdp literal 0 HcmV?d00001 diff --git a/docs/base/@vl2/shapes.vl2/shapes/xorg3.glb b/docs/base/@vl2/shapes.vl2/shapes/xorg3.glb new file mode 100644 index 0000000000000000000000000000000000000000..223b6d086fc456244e2b88c53ce8a2867c73f3be GIT binary patch literal 9792 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 @@ -204,12 +205,17 @@ export function AudioEmitter({ object }: { object: ConsoleObject }) { return debugMode ? ( - + + {fileName} + ) : null; } diff --git a/src/components/DebugElements.tsx b/src/components/DebugElements.tsx new file mode 100644 index 00000000..4a80dbab --- /dev/null +++ b/src/components/DebugElements.tsx @@ -0,0 +1,12 @@ +import { Stats } from "@react-three/drei"; +import { useSettings } from "./SettingsProvider"; + +export function DebugElements() { + const { debugMode } = useSettings(); + + return debugMode ? ( + <> + + + ) : null; +} diff --git a/src/components/FloatingLabel.tsx b/src/components/FloatingLabel.tsx new file mode 100644 index 00000000..23d304dd --- /dev/null +++ b/src/components/FloatingLabel.tsx @@ -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(null); + const distanceRef = useDistanceFromCamera(groupRef); + const [isVisible, setIsVisible] = useState(false); + const labelRef = useRef(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 ( + + {isVisible ? ( + +
+ {children} +
+ + ) : null} +
+ ); +} diff --git a/src/components/GenericShape.tsx b/src/components/GenericShape.tsx index 5e7044cd..107e00be 100644 --- a/src/components/GenericShape.tsx +++ b/src/components/GenericShape.tsx @@ -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 ; - } + 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 ; -} + // 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(); - 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 ( - - {node.material ? ( - - } - > - {Array.isArray(node.material) ? ( - node.material.map((mat, index) => ( - - )) - ) : ( - - )} - - ) : null} - - ); - })} - - ); + return ; } export function ShapePlaceholder({ color }: { color: string }) { @@ -120,3 +66,74 @@ export function ShapePlaceholder({ color }: { color: string }) { ); } + +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(); + }, [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 }) => ( + + {node.material ? ( + + } + > + {Array.isArray(node.material) ? ( + node.material.map((mat, index) => ( + + )) + ) : ( + + )} + + ) : null} + + ))} + {debugMode ? {shapeName} : null} + + ); +} diff --git a/src/components/Item.tsx b/src/components/Item.tsx index c8c3e25e..1804790a 100644 --- a/src/components/Item.tsx +++ b/src/components/Item.tsx @@ -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 ( - - {shapeName ? ( - }> - }> - - - - ) : ( - - )} - + + + {shapeName ? ( + }> + }> + + + + ) : ( + + )} + + ); } diff --git a/src/components/SettingsProvider.tsx b/src/components/SettingsProvider.tsx index 2599e8ea..db563fa7 100644 --- a/src/components/SettingsProvider.tsx +++ b/src/components/SettingsProvider.tsx @@ -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 | 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 ( diff --git a/src/components/ShapeInfoProvider.tsx b/src/components/ShapeInfoProvider.tsx new file mode 100644 index 00000000..a1410153 --- /dev/null +++ b/src/components/ShapeInfoProvider.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/components/StaticShape.tsx b/src/components/StaticShape.tsx index c4e5856a..2a2bc0ab 100644 --- a/src/components/StaticShape.tsx +++ b/src/components/StaticShape.tsx @@ -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 ( - - {shapeName ? ( - }> - }> - - - - ) : ( - - )} - + + + {shapeName ? ( + }> + }> + + + + ) : ( + + )} + + ); } diff --git a/src/components/TSStatic.tsx b/src/components/TSStatic.tsx index cee5108c..d2a64cb1 100644 --- a/src/components/TSStatic.tsx +++ b/src/components/TSStatic.tsx @@ -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(" missing shapeName for object", object); + } + return ( - - }> - }> - - - - + + + }> + }> + + + + + ); } diff --git a/src/components/Turret.tsx b/src/components/Turret.tsx index 09efb7fb..1615eeea 100644 --- a/src/components/Turret.tsx +++ b/src/components/Turret.tsx @@ -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; @@ -46,33 +48,42 @@ export function Turret({ object }: { object: ConsoleObject }) { if (!shapeName) { console.error(` missing shape for dataBlock: ${dataBlock}`); } + if (!barrelShapeName) { + console.error( + ` missing shape for initialBarrel dataBlock: ${initialBarrel}` + ); + } return ( - - {shapeName ? ( - }> - }> - - - - ) : ( - - )} - - {barrelShapeName ? ( + + + {shapeName ? ( }> }> - + ) : ( )} + + + {barrelShapeName ? ( + }> + }> + + + + ) : ( + + )} + + - + ); } diff --git a/src/components/useDistanceFromCamera.ts b/src/components/useDistanceFromCamera.ts new file mode 100644 index 00000000..5936a7c7 --- /dev/null +++ b/src/components/useDistanceFromCamera.ts @@ -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( + ref: RefObject +): RefObject { + const { camera } = useThree(); + const distanceRef = useRef(null); + const worldPosRef = useWorldPosition(ref); + + useFrame(() => { + if (!worldPosRef.current) { + distanceRef.current = null; + } else { + distanceRef.current = camera.position.distanceTo(worldPosRef.current); + } + }); + + return distanceRef; +} diff --git a/src/components/useWorldPosition.ts b/src/components/useWorldPosition.ts new file mode 100644 index 00000000..2a34582a --- /dev/null +++ b/src/components/useWorldPosition.ts @@ -0,0 +1,18 @@ +import { useFrame } from "@react-three/fiber"; +import { useRef, RefObject } from "react"; +import { Object3D, Vector3 } from "three"; + +export function useWorldPosition( + ref: RefObject +): RefObject { + const worldPositionRef = useRef(null); + + useFrame(() => { + if (ref.current) { + worldPositionRef.current ??= new Vector3(); + ref.current.getWorldPosition(worldPositionRef.current); + } + }); + + return worldPositionRef; +} diff --git a/src/loaders.ts b/src/loaders.ts index c594430a..61afab51 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -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); } diff --git a/src/manifest.ts b/src/manifest.ts index d3e7a631..fc9d48d4 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -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) {