refacto: cleanning the codebase
This commit is contained in:
Generated
+152
-2
@@ -10,7 +10,10 @@
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.6.0",
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"@react-three/rapier": "^2.2.0",
|
||||
"gsap": "^3.15.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"r3f-perf": "^7.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -23,10 +26,11 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"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-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"madge": "^8.0.0",
|
||||
"prettier": "^3.8.2",
|
||||
"typescript": "~6.0.2",
|
||||
@@ -643,6 +647,19 @@
|
||||
"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": {
|
||||
"version": "1.3.2",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
|
||||
@@ -2689,6 +2779,13 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
@@ -2939,6 +3036,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -3564,7 +3667,6 @@
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz",
|
||||
"integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
@@ -3788,6 +3890,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -4059,6 +4171,15 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||
@@ -4145,6 +4266,19 @@
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz",
|
||||
@@ -4907,6 +5041,22 @@
|
||||
"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": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
|
||||
|
||||
+1
-1
@@ -19,6 +19,7 @@
|
||||
"@react-three/postprocessing": "^3.0.4",
|
||||
"@react-three/rapier": "^2.2.0",
|
||||
"gsap": "^3.15.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"r3f-perf": "^7.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -36,7 +37,6 @@
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"lil-gui": "^0.21.0",
|
||||
"madge": "^8.0.0",
|
||||
"prettier": "^3.8.2",
|
||||
"typescript": "~6.0.2",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:904b303c98f865526b9524b955f440e630f29d8e18a57bb7bf443fcd9715add1
|
||||
size 83079911
|
||||
+5
-2
@@ -1,3 +1,4 @@
|
||||
import { Suspense } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Crosshair } from "@/components/ui/Crosshair";
|
||||
import { InteractPrompt } from "@/components/ui/InteractPrompt";
|
||||
@@ -8,8 +9,10 @@ function App(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Canvas camera={{ position: [85, 60, 85], fov: 42 }} shadows>
|
||||
<World />
|
||||
<DebugPerf />
|
||||
<Suspense fallback={null}>
|
||||
<World />
|
||||
<DebugPerf />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
<Crosshair />
|
||||
<InteractPrompt />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { RigidBody } from "@react-three/rapier";
|
||||
import { InteractableObject } from "@/components/3d/InteractableObject";
|
||||
@@ -50,7 +50,6 @@ export function TriggerObject({
|
||||
spawnOffset = TRIGGER_DEFAULT_SPAWN_OFFSET,
|
||||
}: TriggerObjectProps): React.JSX.Element {
|
||||
const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
|
||||
const positionRef = useRef(position);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -66,9 +65,9 @@ export function TriggerObject({
|
||||
|
||||
if (spawnModel) {
|
||||
const spawnPos: [number, number, number] = [
|
||||
positionRef.current[0] + spawnOffset[0],
|
||||
positionRef.current[1] + spawnOffset[1],
|
||||
positionRef.current[2] + spawnOffset[2],
|
||||
position[0] + spawnOffset[0],
|
||||
position[1] + spawnOffset[1],
|
||||
position[2] + spawnOffset[2],
|
||||
];
|
||||
setSpawned((prev) => [
|
||||
...prev,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useInteraction } from "@/hooks/useInteraction";
|
||||
import { useInteractionSelector } from "@/hooks/useInteraction";
|
||||
|
||||
export function Crosshair(): React.JSX.Element | null {
|
||||
const cameraMode = useCameraMode();
|
||||
const { focused } = useInteraction();
|
||||
const focused = useInteractionSelector((state) => state.focused);
|
||||
|
||||
if (cameraMode !== "player") return null;
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { INTERACT_KEY } from "@/data/keybindings";
|
||||
import { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||
import { useInteraction } from "@/hooks/useInteraction";
|
||||
import { useInteractionSelector } from "@/hooks/useInteraction";
|
||||
|
||||
export function InteractPrompt(): React.JSX.Element | null {
|
||||
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 (!focused || holding || focused.kind !== "trigger") return null;
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,3 +3,14 @@ export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
|
||||
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25;
|
||||
|
||||
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,11 +1,2 @@
|
||||
export const GAME_SCENE_SKYBOX_PATH = "/skybox/sky.exr";
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type GUI from "lil-gui";
|
||||
import { Debug } from "@/utils/debug/Debug";
|
||||
|
||||
@@ -6,12 +6,23 @@ export function useDebugFolder(
|
||||
name: string,
|
||||
setup: (folder: GUI) => void,
|
||||
): void {
|
||||
const setupRef = useRef(setup);
|
||||
|
||||
useEffect(() => {
|
||||
setupRef.current = setup;
|
||||
}, [setup]);
|
||||
|
||||
useEffect(() => {
|
||||
const debug = Debug.getInstance();
|
||||
if (!debug.active) return;
|
||||
|
||||
const folder = debug.createFolder(name);
|
||||
if (!folder) return;
|
||||
setup(folder);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
if (folder) {
|
||||
setupRef.current(folder);
|
||||
}
|
||||
|
||||
return () => {
|
||||
debug.destroyFolder(name);
|
||||
};
|
||||
}, [name]);
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,18 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import {
|
||||
InteractionManager,
|
||||
type InteractionSnapshot,
|
||||
} from "@/stateManager/InteractionManager";
|
||||
|
||||
const manager = InteractionManager.getInstance();
|
||||
|
||||
export function useInteraction(): InteractionSnapshot {
|
||||
const manager = InteractionManager.getInstance();
|
||||
const [state, setState] = useState<InteractionSnapshot>(manager.getState());
|
||||
|
||||
useEffect(() => {
|
||||
return manager.subscribe(() => {
|
||||
setState({ ...manager.getState() });
|
||||
});
|
||||
}, [manager]);
|
||||
|
||||
return state;
|
||||
return useSyncExternalStore(
|
||||
manager.subscribe.bind(manager),
|
||||
manager.getState.bind(manager),
|
||||
);
|
||||
}
|
||||
|
||||
export function useInteractionSelector<T>(
|
||||
selector: (state: InteractionSnapshot) => T,
|
||||
): T {
|
||||
return useSyncExternalStore(manager.subscribe.bind(manager), () =>
|
||||
selector(manager.getState()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export class AudioManager {
|
||||
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 {
|
||||
if (!AudioManager._instance) {
|
||||
@@ -12,12 +15,47 @@ export class AudioManager {
|
||||
private constructor() {}
|
||||
|
||||
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));
|
||||
void audio.play();
|
||||
audio.currentTime = 0;
|
||||
|
||||
void audio.play().catch(() => {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
});
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this._audioPools.forEach((pool) => {
|
||||
pool.forEach((audio) => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
});
|
||||
});
|
||||
this._audioPools.clear();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,8 @@ export class InteractionManager {
|
||||
|
||||
setFocused(handle: InteractableHandle | null): void {
|
||||
if (this._focused === handle) return;
|
||||
// Never interrupt an active grab via focus change
|
||||
if (this._holding) {
|
||||
this._focused = handle;
|
||||
this._emit();
|
||||
return;
|
||||
}
|
||||
if (this._holding) return;
|
||||
|
||||
this._focused = handle;
|
||||
this._emit();
|
||||
}
|
||||
@@ -59,7 +55,7 @@ export class InteractionManager {
|
||||
}
|
||||
|
||||
releaseInteract(): void {
|
||||
const handle = this._holdingHandle ?? this._focused;
|
||||
const handle = this._holding ? this._holdingHandle : null;
|
||||
if (!handle) return;
|
||||
|
||||
handle.onRelease();
|
||||
@@ -77,6 +73,9 @@ export class InteractionManager {
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this._focused = null;
|
||||
this._holding = false;
|
||||
this._holdingHandle = null;
|
||||
this._listeners.clear();
|
||||
InteractionManager._instance = null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export class Debug {
|
||||
public readonly active: boolean;
|
||||
private readonly gui: GUI | null;
|
||||
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 controls: {
|
||||
cameraMode: CameraMode;
|
||||
@@ -63,27 +63,41 @@ export class Debug {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a named GUI folder. Returns the folder on first call, null on
|
||||
* subsequent calls with the same name — callers should skip `.add()` when
|
||||
* null is returned to avoid duplicating controls under StrictMode double-mount.
|
||||
* Acquires a named GUI folder. Returns the folder on first acquisition and null
|
||||
* on subsequent acquisitions so callers only register controls once.
|
||||
*/
|
||||
createFolder(name: string): GUI | null {
|
||||
if (!this.gui) return null;
|
||||
|
||||
if (this.registeredFolders.has(name)) return null;
|
||||
|
||||
this.registeredFolders.add(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);
|
||||
this.folders.set(name, folder);
|
||||
this.folderRefCounts.set(name, 1);
|
||||
|
||||
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 {
|
||||
this.listeners.add(listener);
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
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 {
|
||||
return (
|
||||
<OrbitControls
|
||||
enableDamping
|
||||
dampingFactor={0.05}
|
||||
minDistance={100}
|
||||
maxDistance={1000}
|
||||
target={[0, 1.75, 0]}
|
||||
dampingFactor={DEBUG_CAMERA_DAMPING_FACTOR}
|
||||
minDistance={DEBUG_CAMERA_MIN_DISTANCE}
|
||||
maxDistance={DEBUG_CAMERA_MAX_DISTANCE}
|
||||
target={[PLAYER_SPAWN_X, PLAYER_EYE_HEIGHT, PLAYER_SPAWN_Z]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
export function DebugHelpers(): React.JSX.Element | null {
|
||||
@@ -10,10 +18,15 @@ export function DebugHelpers(): React.JSX.Element | null {
|
||||
return (
|
||||
<>
|
||||
<gridHelper
|
||||
args={[180, 36, "#1d4ed8", "#1e293b"]}
|
||||
position={[0, 0.01, 0]}
|
||||
args={[
|
||||
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]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import * as THREE from "three";
|
||||
import { useLoader } from "@react-three/fiber";
|
||||
import { Environment as DreiEnvironment } from "@react-three/drei";
|
||||
import {
|
||||
GAME_SCENE_SKYBOX_PATH,
|
||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||
SKYBOX_FACES,
|
||||
} from "@/data/environmentConfig";
|
||||
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 {
|
||||
const sceneMode = useSceneMode();
|
||||
|
||||
@@ -21,5 +14,5 @@ export function Environment(): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
return <SkyBox />;
|
||||
return <DreiEnvironment background files={GAME_SCENE_SKYBOX_PATH} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user