refacto: cleanning the codebase

This commit is contained in:
2026-04-17 16:03:29 +02:00
parent 638022339e
commit f9c4495610
17 changed files with 317 additions and 76 deletions
+152 -2
View File
@@ -10,7 +10,10 @@
"dependencies": { "dependencies": {
"@react-three/drei": "^10.7.7", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.0", "@react-three/fiber": "^9.6.0",
"@react-three/postprocessing": "^3.0.4",
"@react-three/rapier": "^2.2.0", "@react-three/rapier": "^2.2.0",
"gsap": "^3.15.0",
"lil-gui": "^0.21.0",
"r3f-perf": "^7.2.3", "r3f-perf": "^7.2.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
@@ -23,10 +26,11 @@
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4", "eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0", "globals": "^17.4.0",
"lil-gui": "^0.21.0",
"madge": "^8.0.0", "madge": "^8.0.0",
"prettier": "^3.8.2", "prettier": "^3.8.2",
"typescript": "~6.0.2", "typescript": "~6.0.2",
@@ -643,6 +647,19 @@
"url": "https://github.com/sponsors/Boshen" "url": "https://github.com/sponsors/Boshen"
} }
}, },
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@radix-ui/react-icons": { "node_modules/@radix-ui/react-icons": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
@@ -752,6 +769,32 @@
} }
} }
}, },
"node_modules/@react-three/postprocessing": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz",
"integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==",
"license": "MIT",
"dependencies": {
"maath": "^0.6.0",
"n8ao": "^1.9.4",
"postprocessing": "^6.36.6"
},
"peerDependencies": {
"@react-three/fiber": "^9.0.0",
"react": "^19.0",
"three": ">= 0.156.0"
}
},
"node_modules/@react-three/postprocessing/node_modules/maath": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz",
"integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==",
"license": "MIT",
"peerDependencies": {
"@types/three": ">=0.144.0",
"three": ">=0.144.0"
}
},
"node_modules/@react-three/rapier": { "node_modules/@react-three/rapier": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-2.2.0.tgz",
@@ -2531,6 +2574,53 @@
} }
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "10.1.8",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"funding": {
"url": "https://opencollective.com/eslint-config-prettier"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.5.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
"integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"prettier-linter-helpers": "^1.0.1",
"synckit": "^0.11.12"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint-plugin-prettier"
},
"peerDependencies": {
"@types/eslint": ">=8.0.0",
"eslint": ">=8.0.0",
"eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
"prettier": ">=3.0.0"
},
"peerDependenciesMeta": {
"@types/eslint": {
"optional": true
},
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react-hooks": { "node_modules/eslint-plugin-react-hooks": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
@@ -2689,6 +2779,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-diff": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
"integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2939,6 +3036,12 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/gsap": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -3564,7 +3667,6 @@
"version": "0.21.0", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz", "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
"integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==", "integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
@@ -3788,6 +3890,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/n8ao": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz",
"integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==",
"license": "ISC",
"peerDependencies": {
"postprocessing": ">=6.30.0",
"three": ">=0.137"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4059,6 +4171,15 @@
"postcss": "^8.2.9" "postcss": "^8.2.9"
} }
}, },
"node_modules/postprocessing": {
"version": "6.39.0",
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz",
"integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==",
"license": "Zlib",
"peerDependencies": {
"three": ">= 0.168.0 < 0.184.0"
}
},
"node_modules/potpack": { "node_modules/potpack": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
@@ -4145,6 +4266,19 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-linter-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
"integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/pretty-ms": { "node_modules/pretty-ms": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
@@ -4907,6 +5041,22 @@
"react": ">=17.0" "react": ">=17.0"
} }
}, },
"node_modules/synckit": {
"version": "0.11.12",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
"integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
+1 -1
View File
@@ -19,6 +19,7 @@
"@react-three/postprocessing": "^3.0.4", "@react-three/postprocessing": "^3.0.4",
"@react-three/rapier": "^2.2.0", "@react-three/rapier": "^2.2.0",
"gsap": "^3.15.0", "gsap": "^3.15.0",
"lil-gui": "^0.21.0",
"r3f-perf": "^7.2.3", "r3f-perf": "^7.2.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
@@ -36,7 +37,6 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0", "globals": "^17.4.0",
"lil-gui": "^0.21.0",
"madge": "^8.0.0", "madge": "^8.0.0",
"prettier": "^3.8.2", "prettier": "^3.8.2",
"typescript": "~6.0.2", "typescript": "~6.0.2",
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:904b303c98f865526b9524b955f440e630f29d8e18a57bb7bf443fcd9715add1
size 83079911
+3
View File
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { Crosshair } from "@/components/ui/Crosshair"; import { Crosshair } from "@/components/ui/Crosshair";
import { InteractPrompt } from "@/components/ui/InteractPrompt"; import { InteractPrompt } from "@/components/ui/InteractPrompt";
@@ -8,8 +9,10 @@ function App(): React.JSX.Element {
return ( return (
<> <>
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows> <Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
<Suspense fallback={null}>
<World /> <World />
<DebugPerf /> <DebugPerf />
</Suspense>
</Canvas> </Canvas>
<Crosshair /> <Crosshair />
<InteractPrompt /> <InteractPrompt />
+4 -5
View File
@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useState } from "react";
import { useGLTF } from "@react-three/drei"; import { useGLTF } from "@react-three/drei";
import { RigidBody } from "@react-three/rapier"; import { RigidBody } from "@react-three/rapier";
import { InteractableObject } from "@/components/3d/InteractableObject"; import { InteractableObject } from "@/components/3d/InteractableObject";
@@ -50,7 +50,6 @@ export function TriggerObject({
spawnOffset = TRIGGER_DEFAULT_SPAWN_OFFSET, spawnOffset = TRIGGER_DEFAULT_SPAWN_OFFSET,
}: TriggerObjectProps): React.JSX.Element { }: TriggerObjectProps): React.JSX.Element {
const [spawned, setSpawned] = useState<SpawnedModel[]>([]); const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
const positionRef = useRef(position);
return ( return (
<> <>
@@ -66,9 +65,9 @@ export function TriggerObject({
if (spawnModel) { if (spawnModel) {
const spawnPos: [number, number, number] = [ const spawnPos: [number, number, number] = [
positionRef.current[0] + spawnOffset[0], position[0] + spawnOffset[0],
positionRef.current[1] + spawnOffset[1], position[1] + spawnOffset[1],
positionRef.current[2] + spawnOffset[2], position[2] + spawnOffset[2],
]; ];
setSpawned((prev) => [ setSpawned((prev) => [
...prev, ...prev,
+2 -2
View File
@@ -1,9 +1,9 @@
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useInteraction } from "@/hooks/useInteraction"; import { useInteractionSelector } from "@/hooks/useInteraction";
export function Crosshair(): React.JSX.Element | null { export function Crosshair(): React.JSX.Element | null {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const { focused } = useInteraction(); const focused = useInteractionSelector((state) => state.focused);
if (cameraMode !== "player") return null; if (cameraMode !== "player") return null;
+5 -3
View File
@@ -1,16 +1,18 @@
import { INTERACT_KEY } from "@/data/keybindings";
import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useInteraction } from "@/hooks/useInteraction"; import { useInteractionSelector } from "@/hooks/useInteraction";
export function InteractPrompt(): React.JSX.Element | null { export function InteractPrompt(): React.JSX.Element | null {
const cameraMode = useCameraMode(); const cameraMode = useCameraMode();
const { focused, holding } = useInteraction(); const focused = useInteractionSelector((state) => state.focused);
const holding = useInteractionSelector((state) => state.holding);
if (cameraMode !== "player") return null; if (cameraMode !== "player") return null;
if (!focused || holding || focused.kind !== "trigger") return null; if (!focused || holding || focused.kind !== "trigger") return null;
return ( return (
<div className="interact-prompt" aria-live="polite"> <div className="interact-prompt" aria-live="polite">
<kbd className="interact-prompt__key">E</kbd> <kbd className="interact-prompt__key">{INTERACT_KEY.toUpperCase()}</kbd>
<span className="interact-prompt__label">{focused.label}</span> <span className="interact-prompt__label">{focused.label}</span>
</div> </div>
); );
+11
View File
@@ -3,3 +3,14 @@ export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88; export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88;
export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05;
export const DEBUG_CAMERA_MIN_DISTANCE = 100;
export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
export const DEBUG_GRID_SIZE = 180;
export const DEBUG_GRID_DIVISIONS = 36;
export const DEBUG_GRID_PRIMARY_COLOR = "#1d4ed8";
export const DEBUG_GRID_SECONDARY_COLOR = "#1e293b";
export const DEBUG_GRID_Y = 0.01;
export const DEBUG_AXES_SIZE = 10;
+1 -10
View File
@@ -1,11 +1,2 @@
export const GAME_SCENE_SKYBOX_PATH = "/skybox/sky.exr";
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018"; export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
// CubeTextureLoader face order: +X, -X, +Y, -Y, +Z, -Z
export const SKYBOX_FACES = [
"/skybox/px.jpg",
"/skybox/nx.jpg",
"/skybox/py.jpg",
"/skybox/ny.jpg",
"/skybox/pz.jpg",
"/skybox/nz.jpg",
] as const;
+16 -5
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useRef } from "react";
import type GUI from "lil-gui"; import type GUI from "lil-gui";
import { Debug } from "@/utils/debug/Debug"; import { Debug } from "@/utils/debug/Debug";
@@ -6,12 +6,23 @@ export function useDebugFolder(
name: string, name: string,
setup: (folder: GUI) => void, setup: (folder: GUI) => void,
): void { ): void {
const setupRef = useRef(setup);
useEffect(() => {
setupRef.current = setup;
}, [setup]);
useEffect(() => { useEffect(() => {
const debug = Debug.getInstance(); const debug = Debug.getInstance();
if (!debug.active) return; if (!debug.active) return;
const folder = debug.createFolder(name); const folder = debug.createFolder(name);
if (!folder) return; if (folder) {
setup(folder); setupRef.current(folder);
// eslint-disable-next-line react-hooks/exhaustive-deps }
}, []);
return () => {
debug.destroyFolder(name);
};
}, [name]);
} }
+15 -11
View File
@@ -1,18 +1,22 @@
import { useEffect, useState } from "react"; import { useSyncExternalStore } from "react";
import { import {
InteractionManager, InteractionManager,
type InteractionSnapshot, type InteractionSnapshot,
} from "@/stateManager/InteractionManager"; } from "@/stateManager/InteractionManager";
const manager = InteractionManager.getInstance();
export function useInteraction(): InteractionSnapshot { export function useInteraction(): InteractionSnapshot {
const manager = InteractionManager.getInstance(); return useSyncExternalStore(
const [state, setState] = useState<InteractionSnapshot>(manager.getState()); manager.subscribe.bind(manager),
manager.getState.bind(manager),
useEffect(() => { );
return manager.subscribe(() => { }
setState({ ...manager.getState() });
}); export function useInteractionSelector<T>(
}, [manager]); selector: (state: InteractionSnapshot) => T,
): T {
return state; return useSyncExternalStore(manager.subscribe.bind(manager), () =>
selector(manager.getState()),
);
} }
+40 -2
View File
@@ -1,5 +1,8 @@
export class AudioManager { export class AudioManager {
private static _instance: AudioManager | null = null; private static _instance: AudioManager | null = null;
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
private static readonly MAX_POOL_SIZE_PER_SOUND = 6;
static getInstance(): AudioManager { static getInstance(): AudioManager {
if (!AudioManager._instance) { if (!AudioManager._instance) {
@@ -12,12 +15,47 @@ export class AudioManager {
private constructor() {} private constructor() {}
playSound(path: string, volume = 1): void { playSound(path: string, volume = 1): void {
const audio = new Audio(path); const audio = this._acquireAudio(path);
audio.volume = Math.max(0, Math.min(1, volume)); audio.volume = Math.max(0, Math.min(1, volume));
void audio.play(); audio.currentTime = 0;
void audio.play().catch(() => {
audio.pause();
audio.currentTime = 0;
});
} }
destroy(): void { destroy(): void {
this._audioPools.forEach((pool) => {
pool.forEach((audio) => {
audio.pause();
audio.src = "";
});
});
this._audioPools.clear();
AudioManager._instance = null; AudioManager._instance = null;
} }
private _acquireAudio(path: string): HTMLAudioElement {
const existingPool = this._audioPools.get(path);
if (existingPool) {
const availableAudio = existingPool.find(
(audio) => audio.paused || audio.ended,
);
if (availableAudio) return availableAudio;
if (existingPool.length < AudioManager.MAX_POOL_SIZE_PER_SOUND) {
const pooledAudio = new Audio(path);
existingPool.push(pooledAudio);
return pooledAudio;
}
return existingPool[0]!;
}
const initialAudio = new Audio(path);
this._audioPools.set(path, [initialAudio]);
return initialAudio;
}
} }
+6 -7
View File
@@ -39,12 +39,8 @@ export class InteractionManager {
setFocused(handle: InteractableHandle | null): void { setFocused(handle: InteractableHandle | null): void {
if (this._focused === handle) return; if (this._focused === handle) return;
// Never interrupt an active grab via focus change if (this._holding) return;
if (this._holding) {
this._focused = handle;
this._emit();
return;
}
this._focused = handle; this._focused = handle;
this._emit(); this._emit();
} }
@@ -59,7 +55,7 @@ export class InteractionManager {
} }
releaseInteract(): void { releaseInteract(): void {
const handle = this._holdingHandle ?? this._focused; const handle = this._holding ? this._holdingHandle : null;
if (!handle) return; if (!handle) return;
handle.onRelease(); handle.onRelease();
@@ -77,6 +73,9 @@ export class InteractionManager {
} }
destroy(): void { destroy(): void {
this._focused = null;
this._holding = false;
this._holdingHandle = null;
this._listeners.clear(); this._listeners.clear();
InteractionManager._instance = null; InteractionManager._instance = null;
} }
+23 -9
View File
@@ -7,7 +7,7 @@ export class Debug {
public readonly active: boolean; public readonly active: boolean;
private readonly gui: GUI | null; private readonly gui: GUI | null;
private readonly folders = new Map<string, GUI>(); private readonly folders = new Map<string, GUI>();
private readonly registeredFolders = new Set<string>(); private readonly folderRefCounts = new Map<string, number>();
private readonly listeners = new Set<() => void>(); private readonly listeners = new Set<() => void>();
private readonly controls: { private readonly controls: {
cameraMode: CameraMode; cameraMode: CameraMode;
@@ -63,27 +63,41 @@ export class Debug {
} }
/** /**
* Creates a named GUI folder. Returns the folder on first call, null on * Acquires a named GUI folder. Returns the folder on first acquisition and null
* subsequent calls with the same name — callers should skip `.add()` when * on subsequent acquisitions so callers only register controls once.
* null is returned to avoid duplicating controls under StrictMode double-mount.
*/ */
createFolder(name: string): GUI | null { createFolder(name: string): GUI | null {
if (!this.gui) return null; if (!this.gui) return null;
if (this.registeredFolders.has(name)) return null;
this.registeredFolders.add(name);
const existing = this.folders.get(name); const existing = this.folders.get(name);
if (existing) return existing; if (existing) {
this.folderRefCounts.set(name, (this.folderRefCounts.get(name) ?? 0) + 1);
return null;
}
const folder = this.gui.addFolder(name); const folder = this.gui.addFolder(name);
this.folders.set(name, folder); this.folders.set(name, folder);
this.folderRefCounts.set(name, 1);
return folder; return folder;
} }
destroyFolder(name: string): void {
const folder = this.folders.get(name);
const refCount = this.folderRefCounts.get(name);
if (!folder || refCount === undefined) return;
if (refCount > 1) {
this.folderRefCounts.set(name, refCount - 1);
return;
}
folder.destroy();
this.folders.delete(name);
this.folderRefCounts.delete(name);
}
subscribe(listener: () => void): () => void { subscribe(listener: () => void): () => void {
this.listeners.add(listener); this.listeners.add(listener);
+14 -4
View File
@@ -1,13 +1,23 @@
import { OrbitControls } from "@react-three/drei"; import { OrbitControls } from "@react-three/drei";
import {
DEBUG_CAMERA_DAMPING_FACTOR,
DEBUG_CAMERA_MAX_DISTANCE,
DEBUG_CAMERA_MIN_DISTANCE,
} from "@/data/debugConfig";
import {
PLAYER_EYE_HEIGHT,
PLAYER_SPAWN_X,
PLAYER_SPAWN_Z,
} from "@/data/playerConfig";
export function DebugCameraControls(): React.JSX.Element { export function DebugCameraControls(): React.JSX.Element {
return ( return (
<OrbitControls <OrbitControls
enableDamping enableDamping
dampingFactor={0.05} dampingFactor={DEBUG_CAMERA_DAMPING_FACTOR}
minDistance={100} minDistance={DEBUG_CAMERA_MIN_DISTANCE}
maxDistance={1000} maxDistance={DEBUG_CAMERA_MAX_DISTANCE}
target={[0, 1.75, 0]} target={[PLAYER_SPAWN_X, PLAYER_EYE_HEIGHT, PLAYER_SPAWN_Z]}
/> />
); );
} }
+16 -3
View File
@@ -1,3 +1,11 @@
import {
DEBUG_AXES_SIZE,
DEBUG_GRID_DIVISIONS,
DEBUG_GRID_PRIMARY_COLOR,
DEBUG_GRID_SECONDARY_COLOR,
DEBUG_GRID_SIZE,
DEBUG_GRID_Y,
} from "@/data/debugConfig";
import { Debug } from "@/utils/debug/Debug"; import { Debug } from "@/utils/debug/Debug";
export function DebugHelpers(): React.JSX.Element | null { export function DebugHelpers(): React.JSX.Element | null {
@@ -10,10 +18,15 @@ export function DebugHelpers(): React.JSX.Element | null {
return ( return (
<> <>
<gridHelper <gridHelper
args={[180, 36, "#1d4ed8", "#1e293b"]} args={[
position={[0, 0.01, 0]} DEBUG_GRID_SIZE,
DEBUG_GRID_DIVISIONS,
DEBUG_GRID_PRIMARY_COLOR,
DEBUG_GRID_SECONDARY_COLOR,
]}
position={[0, DEBUG_GRID_Y, 0]}
/> />
<axesHelper args={[10]} /> <axesHelper args={[DEBUG_AXES_SIZE]} />
</> </>
); );
} }
+3 -10
View File
@@ -1,17 +1,10 @@
import * as THREE from "three"; import { Environment as DreiEnvironment } from "@react-three/drei";
import { useLoader } from "@react-three/fiber";
import { import {
GAME_SCENE_SKYBOX_PATH,
PHYSICS_SCENE_BACKGROUND_COLOR, PHYSICS_SCENE_BACKGROUND_COLOR,
SKYBOX_FACES,
} from "@/data/environmentConfig"; } from "@/data/environmentConfig";
import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode";
function SkyBox(): React.JSX.Element {
const texture = useLoader(THREE.CubeTextureLoader, [...SKYBOX_FACES]);
return <primitive attach="background" object={texture} />;
}
export function Environment(): React.JSX.Element { export function Environment(): React.JSX.Element {
const sceneMode = useSceneMode(); const sceneMode = useSceneMode();
@@ -21,5 +14,5 @@ export function Environment(): React.JSX.Element {
); );
} }
return <SkyBox />; return <DreiEnvironment background files={GAME_SCENE_SKYBOX_PATH} />;
} }