From 638022339e7c4d7486bf911ebe8482cfed0666d8 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 17 Apr 2026 15:42:10 +0200 Subject: [PATCH] update : put every constante in the data folder --- src/components/3d/GrabbableObject.tsx | 139 +++++++++++++++++++++++ src/components/3d/InteractableObject.tsx | 59 +++++----- src/components/3d/TriggerObject.tsx | 94 +++++++++++++++ src/data/debugConfig.ts | 5 + src/data/environmentConfig.ts | 11 ++ src/data/grabConfig.ts | 18 +++ src/data/keybindings.ts | 7 ++ src/data/lightingConfig.ts | 30 +++++ src/data/playerConfig.ts | 16 +++ src/data/testSceneConfig.ts | 22 ++++ src/data/triggerConfig.ts | 4 + src/world/Environment.tsx | 26 ++++- src/world/Lighting.tsx | 64 ++++++++--- src/world/Map.tsx | 3 +- src/world/World.tsx | 8 +- src/world/debug/TestScene.tsx | 68 +++++++++-- src/world/objects/GrabCube.tsx | 80 ------------- src/world/objects/TriggerSphere.tsx | 32 ------ src/world/player/PlayerCamera.tsx | 3 - src/world/player/PlayerComponent.tsx | 9 +- src/world/player/PlayerController.tsx | 81 +++++++------ 21 files changed, 570 insertions(+), 209 deletions(-) create mode 100644 src/components/3d/GrabbableObject.tsx create mode 100644 src/components/3d/TriggerObject.tsx create mode 100644 src/data/debugConfig.ts create mode 100644 src/data/environmentConfig.ts create mode 100644 src/data/grabConfig.ts create mode 100644 src/data/keybindings.ts create mode 100644 src/data/lightingConfig.ts create mode 100644 src/data/playerConfig.ts create mode 100644 src/data/testSceneConfig.ts create mode 100644 src/data/triggerConfig.ts delete mode 100644 src/world/objects/GrabCube.tsx delete mode 100644 src/world/objects/TriggerSphere.tsx diff --git a/src/components/3d/GrabbableObject.tsx b/src/components/3d/GrabbableObject.tsx new file mode 100644 index 0000000..7e42f32 --- /dev/null +++ b/src/components/3d/GrabbableObject.tsx @@ -0,0 +1,139 @@ +import { useRef } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { RigidBody } from "@react-three/rapier"; +import type { RapierRigidBody } from "@react-three/rapier"; +import * as THREE from "three"; +import { InteractableObject } from "@/components/3d/InteractableObject"; +import { + GRAB_DEFAULT_COLLIDERS, + GRAB_DEFAULT_LABEL, + GRAB_HOLD_DISTANCE_DEFAULT, + GRAB_HOLD_DISTANCE_MAX, + GRAB_HOLD_DISTANCE_MIN, + GRAB_HOLD_DISTANCE_STEP, + GRAB_STIFFNESS_DEFAULT, + GRAB_STIFFNESS_MAX, + GRAB_STIFFNESS_MIN, + GRAB_STIFFNESS_STEP, + GRAB_THROW_BOOST_DEFAULT, + GRAB_THROW_BOOST_MAX, + GRAB_THROW_BOOST_MIN, + GRAB_THROW_BOOST_STEP, +} from "@/data/grabConfig"; +import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; + +interface GrabbableObjectProps { + position: [number, number, number]; + children: React.ReactNode; + colliders?: "cuboid" | "ball" | "hull"; + label?: string; +} + +// Shared mutable params — one debug folder controls all instances. +const params = { + stiffness: GRAB_STIFFNESS_DEFAULT, + throwBoost: GRAB_THROW_BOOST_DEFAULT, + holdDistance: GRAB_HOLD_DISTANCE_DEFAULT, +}; + +const ZERO_ANGULAR_VELOCITY = { x: 0, y: 0, z: 0 }; + +const _holdTarget = new THREE.Vector3(); +const _currentPos = new THREE.Vector3(); +const _velocity = new THREE.Vector3(); + +export function GrabbableObject({ + position, + children, + colliders = GRAB_DEFAULT_COLLIDERS, + label = GRAB_DEFAULT_LABEL, +}: GrabbableObjectProps): React.JSX.Element { + const camera = useThree((state) => state.camera); + const rbRef = useRef(null); + const isHolding = useRef(false); + + useDebugFolder("GrabbableObject", (folder) => { + folder + .add( + params, + "stiffness", + GRAB_STIFFNESS_MIN, + GRAB_STIFFNESS_MAX, + GRAB_STIFFNESS_STEP, + ) + .name("Hold stiffness"); + folder + .add( + params, + "throwBoost", + GRAB_THROW_BOOST_MIN, + GRAB_THROW_BOOST_MAX, + GRAB_THROW_BOOST_STEP, + ) + .name("Throw boost"); + folder + .add( + params, + "holdDistance", + GRAB_HOLD_DISTANCE_MIN, + GRAB_HOLD_DISTANCE_MAX, + GRAB_HOLD_DISTANCE_STEP, + ) + .name("Hold distance"); + }); + + useFrame(() => { + if (!isHolding.current || !rbRef.current) return; + + camera.getWorldDirection(_holdTarget); + _holdTarget.multiplyScalar(params.holdDistance).add(camera.position); + + const t = rbRef.current.translation(); + _currentPos.set(t.x, t.y, t.z); + + _velocity + .subVectors(_holdTarget, _currentPos) + .multiplyScalar(params.stiffness); + + rbRef.current.setLinvel( + { x: _velocity.x, y: _velocity.y, z: _velocity.z }, + true, + ); + rbRef.current.setAngvel(ZERO_ANGULAR_VELOCITY, true); + }); + + return ( + + { + isHolding.current = true; + }} + onRelease={() => { + isHolding.current = false; + if (!rbRef.current || params.throwBoost === GRAB_THROW_BOOST_DEFAULT) + return; + const v = rbRef.current.linvel(); + rbRef.current.setLinvel( + { + x: v.x * params.throwBoost, + y: v.y * params.throwBoost, + z: v.z * params.throwBoost, + }, + true, + ); + }} + > + {children} + + + ); +} diff --git a/src/components/3d/InteractableObject.tsx b/src/components/3d/InteractableObject.tsx index c08845f..08f1e4c 100644 --- a/src/components/3d/InteractableObject.tsx +++ b/src/components/3d/InteractableObject.tsx @@ -1,9 +1,13 @@ import { useEffect, useRef } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import { RigidBody } from "@react-three/rapier"; import type { RapierRigidBody } from "@react-three/rapier"; import * as THREE from "three"; import type { RefObject } from "react"; +import { + INTERACTION_DEBUG_SPHERE_COLOR, + INTERACTION_DEBUG_SPHERE_OPACITY, + INTERACTION_DEBUG_SPHERE_SEGMENTS, +} from "@/data/debugConfig"; import { Debug } from "@/utils/debug/Debug"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; import { @@ -17,9 +21,7 @@ interface InteractableObjectProps { kind: InteractableKind; label: string; position: [number, number, number]; - rigidBodyType?: "dynamic" | "fixed"; - colliders?: "cuboid" | "ball" | "hull"; - rbRef?: RefObject; + bodyRef?: RefObject; onPress: () => void; onRelease?: () => void; children: React.ReactNode; @@ -34,16 +36,12 @@ export function InteractableObject({ kind, label, position, - rigidBodyType = "dynamic", - colliders = "cuboid", - rbRef, + bodyRef, onPress, onRelease = () => {}, children, }: InteractableObjectProps): React.JSX.Element { const camera = useThree((state) => state.camera); - const internalRef = useRef(null); - const bodyRef = rbRef ?? internalRef; const groupRef = useRef(null); const debugSphereRef = useRef(null); @@ -67,7 +65,6 @@ export function InteractableObject({ }); useFrame(() => { - const body = bodyRef.current; const group = groupRef.current; const debug = Debug.getInstance(); const manager = InteractionManager.getInstance(); @@ -77,8 +74,8 @@ export function InteractableObject({ debug.active && debug.getShowInteractionSpheres(); } - if (body) { - const t = body.translation(); + if (bodyRef?.current) { + const t = bodyRef.current.translation(); _objectPos.set(t.x, t.y, t.z); } else { _objectPos.set(...position); @@ -99,7 +96,6 @@ export function InteractableObject({ _raycaster.far = INTERACTION_RADIUS; const hits = group ? _raycaster.intersectObject(group, true) : []; - const validHit = hits.find((h) => h.object !== debugSphereRef.current); if (validHit) { @@ -110,24 +106,23 @@ export function InteractableObject({ }); return ( - - - {children} - - - - - - + + {children} + + + + + ); } diff --git a/src/components/3d/TriggerObject.tsx b/src/components/3d/TriggerObject.tsx new file mode 100644 index 0000000..51c919b --- /dev/null +++ b/src/components/3d/TriggerObject.tsx @@ -0,0 +1,94 @@ +import { useRef, useState } from "react"; +import { useGLTF } from "@react-three/drei"; +import { RigidBody } from "@react-three/rapier"; +import { InteractableObject } from "@/components/3d/InteractableObject"; +import { + TRIGGER_DEFAULT_COLLIDERS, + TRIGGER_DEFAULT_LABEL, + TRIGGER_DEFAULT_SOUND_VOLUME, + TRIGGER_DEFAULT_SPAWN_OFFSET, +} from "@/data/triggerConfig"; +import { AudioManager } from "@/stateManager/AudioManager"; + +interface SpawnedModel { + id: number; + position: [number, number, number]; +} + +interface TriggerObjectProps { + position: [number, number, number]; + children: React.ReactNode; + colliders?: "cuboid" | "ball" | "hull"; + label?: string; + soundPath?: string; + soundVolume?: number; + spawnModel?: string; + spawnOffset?: [number, number, number]; +} + +let _spawnCounter = 0; + +function SpawnedModelInstance({ + path, + position, +}: { + path: string; + position: [number, number, number]; +}): React.JSX.Element { + const { scene } = useGLTF(path); + return ; +} + +export function TriggerObject({ + position, + children, + colliders = TRIGGER_DEFAULT_COLLIDERS, + label = TRIGGER_DEFAULT_LABEL, + soundPath, + soundVolume = TRIGGER_DEFAULT_SOUND_VOLUME, + spawnModel, + spawnOffset = TRIGGER_DEFAULT_SPAWN_OFFSET, +}: TriggerObjectProps): React.JSX.Element { + const [spawned, setSpawned] = useState([]); + const positionRef = useRef(position); + + return ( + <> + + { + if (soundPath) { + AudioManager.getInstance().playSound(soundPath, soundVolume); + } + + if (spawnModel) { + const spawnPos: [number, number, number] = [ + positionRef.current[0] + spawnOffset[0], + positionRef.current[1] + spawnOffset[1], + positionRef.current[2] + spawnOffset[2], + ]; + setSpawned((prev) => [ + ...prev, + { id: ++_spawnCounter, position: spawnPos }, + ]); + } + }} + > + {children} + + + + {spawnModel && + spawned.map((s) => ( + + ))} + + ); +} diff --git a/src/data/debugConfig.ts b/src/data/debugConfig.ts new file mode 100644 index 0000000..946b8f6 --- /dev/null +++ b/src/data/debugConfig.ts @@ -0,0 +1,5 @@ +export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16; +export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15"; +export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; + +export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88; diff --git a/src/data/environmentConfig.ts b/src/data/environmentConfig.ts new file mode 100644 index 0000000..36c7dd9 --- /dev/null +++ b/src/data/environmentConfig.ts @@ -0,0 +1,11 @@ +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; diff --git a/src/data/grabConfig.ts b/src/data/grabConfig.ts new file mode 100644 index 0000000..774b515 --- /dev/null +++ b/src/data/grabConfig.ts @@ -0,0 +1,18 @@ +export const GRAB_DEFAULT_COLLIDERS = "cuboid"; +export const GRAB_DEFAULT_LABEL = "Prendre"; + +export const GRAB_STIFFNESS_DEFAULT = 15; +export const GRAB_THROW_BOOST_DEFAULT = 1.0; +export const GRAB_HOLD_DISTANCE_DEFAULT = 2; + +export const GRAB_STIFFNESS_MIN = 1; +export const GRAB_STIFFNESS_MAX = 50; +export const GRAB_STIFFNESS_STEP = 1; + +export const GRAB_THROW_BOOST_MIN = 0.5; +export const GRAB_THROW_BOOST_MAX = 3.0; +export const GRAB_THROW_BOOST_STEP = 0.1; + +export const GRAB_HOLD_DISTANCE_MIN = 0.5; +export const GRAB_HOLD_DISTANCE_MAX = 5.0; +export const GRAB_HOLD_DISTANCE_STEP = 0.1; diff --git a/src/data/keybindings.ts b/src/data/keybindings.ts new file mode 100644 index 0000000..04bd83b --- /dev/null +++ b/src/data/keybindings.ts @@ -0,0 +1,7 @@ +export const MOVE_FORWARD_KEY = "z"; +export const MOVE_BACKWARD_KEY = "s"; +export const MOVE_LEFT_KEY = "q"; +export const MOVE_RIGHT_KEY = "d"; +export const JUMP_KEY = " "; +export const INTERACT_KEY = "e"; +export const PRIMARY_INTERACT_MOUSE_BUTTON = 0; diff --git a/src/data/lightingConfig.ts b/src/data/lightingConfig.ts new file mode 100644 index 0000000..2b67660 --- /dev/null +++ b/src/data/lightingConfig.ts @@ -0,0 +1,30 @@ +export const AMBIENT_LIGHT_COLOR = "#dbeafe"; +export const SUN_LIGHT_COLOR = "#fff7ed"; + +export const LIGHTING_DEFAULTS = { + ambientIntensity: 1.8, + sunIntensity: 2.8, + sunX: 60, + sunY: 80, + sunZ: 30, +}; + +export const AMBIENT_INTENSITY_MIN = 0; +export const AMBIENT_INTENSITY_MAX = 5; +export const AMBIENT_INTENSITY_STEP = 0.1; + +export const SUN_INTENSITY_MIN = 0; +export const SUN_INTENSITY_MAX = 8; +export const SUN_INTENSITY_STEP = 0.1; + +export const SUN_X_MIN = -100; +export const SUN_X_MAX = 100; +export const SUN_X_STEP = 1; + +export const SUN_Y_MIN = 0; +export const SUN_Y_MAX = 150; +export const SUN_Y_STEP = 1; + +export const SUN_Z_MIN = -100; +export const SUN_Z_MAX = 100; +export const SUN_Z_STEP = 1; diff --git a/src/data/playerConfig.ts b/src/data/playerConfig.ts new file mode 100644 index 0000000..82ee55e --- /dev/null +++ b/src/data/playerConfig.ts @@ -0,0 +1,16 @@ +export const PLAYER_EYE_HEIGHT = 1.75; +export const PLAYER_CAPSULE_RADIUS = 0.35; + +export const PLAYER_WALK_SPEED = 11; +export const PLAYER_AIR_CONTROL_FACTOR = 0.35; +export const PLAYER_JUMP_SPEED = 9; +export const PLAYER_GRAVITY = 30; +export const PLAYER_MAX_DELTA = 0.05; +export const PLAYER_ACCELERATION_MULTIPLIER = 9; +export const PLAYER_XZ_DAMPING_FACTOR = 8; + +export const PLAYER_SPAWN_X = 0; +export const PLAYER_SPAWN_Z = 0; +export const PLAYER_SPAWN_Y_DEFAULT = 100; +export const PLAYER_SPAWN_Y_GAME = 100; +export const PLAYER_SPAWN_Y_PHYSICS = 3; diff --git a/src/data/testSceneConfig.ts b/src/data/testSceneConfig.ts new file mode 100644 index 0000000..bc12b02 --- /dev/null +++ b/src/data/testSceneConfig.ts @@ -0,0 +1,22 @@ +export const TEST_SCENE_FLOOR_POSITION: [number, number, number] = [0, -0.5, 0]; +export const TEST_SCENE_FLOOR_SIZE: [number, number, number] = [200, 1, 200]; +export const TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS: [number, number, number] = + [100, 0.5, 100]; + +export const TEST_SCENE_GRABBABLE_POSITION: [number, number, number] = [ + 0, 1, -3, +]; +export const TEST_SCENE_GRABBABLE_BOX_SIZE: [number, number, number] = [ + 0.5, 0.5, 0.5, +]; +export const TEST_SCENE_GRABBABLE_COLOR = "#e07b39"; +export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6; +export const TEST_SCENE_GRABBABLE_METALNESS = 0.1; + +export const TEST_SCENE_TRIGGER_POSITION: [number, number, number] = [3, 2, -3]; +export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3"; +export const TEST_SCENE_TRIGGER_RADIUS = 0.4; +export const TEST_SCENE_TRIGGER_SEGMENTS = 32; +export const TEST_SCENE_TRIGGER_COLOR = "#3b82f6"; +export const TEST_SCENE_TRIGGER_ROUGHNESS = 0.3; +export const TEST_SCENE_TRIGGER_METALNESS = 0.5; diff --git a/src/data/triggerConfig.ts b/src/data/triggerConfig.ts new file mode 100644 index 0000000..19ab3f5 --- /dev/null +++ b/src/data/triggerConfig.ts @@ -0,0 +1,4 @@ +export const TRIGGER_DEFAULT_COLLIDERS = "ball"; +export const TRIGGER_DEFAULT_LABEL = "Interagir"; +export const TRIGGER_DEFAULT_SOUND_VOLUME = 1; +export const TRIGGER_DEFAULT_SPAWN_OFFSET: [number, number, number] = [0, 0, 0]; diff --git a/src/world/Environment.tsx b/src/world/Environment.tsx index 7b8af01..b314bf4 100644 --- a/src/world/Environment.tsx +++ b/src/world/Environment.tsx @@ -1,3 +1,25 @@ -export function Environment(): React.JSX.Element { - return ; +import * as THREE from "three"; +import { useLoader } from "@react-three/fiber"; +import { + 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 ; +} + +export function Environment(): React.JSX.Element { + const sceneMode = useSceneMode(); + + if (sceneMode === "physics") { + return ( + + ); + } + + return ; } diff --git a/src/world/Lighting.tsx b/src/world/Lighting.tsx index 00c5940..37be86f 100644 --- a/src/world/Lighting.tsx +++ b/src/world/Lighting.tsx @@ -1,6 +1,26 @@ import { useRef } from "react"; import { useFrame } from "@react-three/fiber"; import type { AmbientLight, DirectionalLight } from "three"; +import { + AMBIENT_INTENSITY_MAX, + AMBIENT_INTENSITY_MIN, + AMBIENT_INTENSITY_STEP, + AMBIENT_LIGHT_COLOR, + LIGHTING_DEFAULTS, + SUN_INTENSITY_MAX, + SUN_INTENSITY_MIN, + SUN_INTENSITY_STEP, + SUN_LIGHT_COLOR, + SUN_X_MAX, + SUN_X_MIN, + SUN_X_STEP, + SUN_Y_MAX, + SUN_Y_MIN, + SUN_Y_STEP, + SUN_Z_MAX, + SUN_Z_MIN, + SUN_Z_STEP, +} from "@/data/lightingConfig"; import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; type LightingState = { @@ -11,24 +31,40 @@ type LightingState = { sunZ: number; }; -const LIGHTING_STATE: LightingState = { - ambientIntensity: 1.8, - sunIntensity: 2.8, - sunX: 60, - sunY: 80, - sunZ: 30, -}; +const LIGHTING_STATE: LightingState = { ...LIGHTING_DEFAULTS }; export function Lighting(): React.JSX.Element { const ambient = useRef(null); const sun = useRef(null); useDebugFolder("Lighting", (folder) => { - folder.add(LIGHTING_STATE, "ambientIntensity", 0, 5, 0.1).name("Ambient"); - folder.add(LIGHTING_STATE, "sunIntensity", 0, 8, 0.1).name("Sun Intensity"); - folder.add(LIGHTING_STATE, "sunX", -100, 100, 1).name("Sun X"); - folder.add(LIGHTING_STATE, "sunY", 0, 150, 1).name("Sun Y"); - folder.add(LIGHTING_STATE, "sunZ", -100, 100, 1).name("Sun Z"); + folder + .add( + LIGHTING_STATE, + "ambientIntensity", + AMBIENT_INTENSITY_MIN, + AMBIENT_INTENSITY_MAX, + AMBIENT_INTENSITY_STEP, + ) + .name("Ambient"); + folder + .add( + LIGHTING_STATE, + "sunIntensity", + SUN_INTENSITY_MIN, + SUN_INTENSITY_MAX, + SUN_INTENSITY_STEP, + ) + .name("Sun Intensity"); + folder + .add(LIGHTING_STATE, "sunX", SUN_X_MIN, SUN_X_MAX, SUN_X_STEP) + .name("Sun X"); + folder + .add(LIGHTING_STATE, "sunY", SUN_Y_MIN, SUN_Y_MAX, SUN_Y_STEP) + .name("Sun Y"); + folder + .add(LIGHTING_STATE, "sunZ", SUN_Z_MIN, SUN_Z_MAX, SUN_Z_STEP) + .name("Sun Z"); }); useFrame(() => { @@ -51,7 +87,7 @@ export function Lighting(): React.JSX.Element { diff --git a/src/world/Map.tsx b/src/world/Map.tsx index c811b62..ac6975e 100644 --- a/src/world/Map.tsx +++ b/src/world/Map.tsx @@ -3,6 +3,7 @@ import { useThree } from "@react-three/fiber"; import { useGLTF } from "@react-three/drei"; import * as THREE from "three"; import { Octree } from "three/addons/math/Octree.js"; +import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig"; import { Debug } from "@/utils/debug/Debug"; const MAP_PATH = "/models/map/model.gltf"; @@ -38,7 +39,7 @@ export function Map({ onOctreeReady }: MapProps): React.JSX.Element { groupRef.current.traverse((child) => { if (!(child instanceof THREE.Mesh)) return; - const helper = new THREE.BoxHelper(child, 0x00ff88); + const helper = new THREE.BoxHelper(child, MAP_DEBUG_BOX_HELPER_COLOR); scene.add(helper); helpers.push(helper); }); diff --git a/src/world/World.tsx b/src/world/World.tsx index 566ffb1..e0d3eee 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -1,5 +1,9 @@ import { useState, useCallback } from "react"; import type { Octree } from "three/addons/math/Octree.js"; +import { + PLAYER_SPAWN_Y_GAME, + PLAYER_SPAWN_Y_PHYSICS, +} from "@/data/playerConfig"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls"; @@ -32,7 +36,9 @@ export function World(): React.JSX.Element { {cameraMode !== "debug" ? ( ) : null} diff --git a/src/world/debug/TestScene.tsx b/src/world/debug/TestScene.tsx index 79de99f..d7cb3f5 100644 --- a/src/world/debug/TestScene.tsx +++ b/src/world/debug/TestScene.tsx @@ -2,8 +2,25 @@ import { useEffect, useRef } from "react"; import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier"; import * as THREE from "three"; import { Octree } from "three/addons/math/Octree.js"; -import { GrabCube } from "@/world/objects/GrabCube"; -import { TriggerSphere } from "@/world/objects/TriggerSphere"; +import { GrabbableObject } from "@/components/3d/GrabbableObject"; +import { TriggerObject } from "@/components/3d/TriggerObject"; +import { + TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS, + TEST_SCENE_FLOOR_POSITION, + TEST_SCENE_FLOOR_SIZE, + TEST_SCENE_GRABBABLE_BOX_SIZE, + TEST_SCENE_GRABBABLE_COLOR, + TEST_SCENE_GRABBABLE_METALNESS, + TEST_SCENE_GRABBABLE_POSITION, + TEST_SCENE_GRABBABLE_ROUGHNESS, + TEST_SCENE_TRIGGER_COLOR, + TEST_SCENE_TRIGGER_METALNESS, + TEST_SCENE_TRIGGER_POSITION, + TEST_SCENE_TRIGGER_RADIUS, + TEST_SCENE_TRIGGER_ROUGHNESS, + TEST_SCENE_TRIGGER_SEGMENTS, + TEST_SCENE_TRIGGER_SOUND_PATH, +} from "@/data/testSceneConfig"; interface TestSceneProps { onOctreeReady: (octree: Octree) => void; @@ -28,21 +45,54 @@ export function TestScene({ return ( <> - {/* Invisible floor mesh for Octree player collision */} - - + + - {/* Rapier physics for interactable objects */} - + - - + + + + + + + + + + + + + + ); diff --git a/src/world/objects/GrabCube.tsx b/src/world/objects/GrabCube.tsx deleted file mode 100644 index 22fb707..0000000 --- a/src/world/objects/GrabCube.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useRef } from "react"; -import { useFrame, useThree } from "@react-three/fiber"; -import type { RapierRigidBody } from "@react-three/rapier"; -import * as THREE from "three"; -import { InteractableObject } from "@/components/3d/InteractableObject"; -import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; - -const CUBE_SIZE = 0.5; -const HOLD_DISTANCE = 2; -const SPAWN_POSITION: [number, number, number] = [0, 1, -3]; - -const params = { stiffness: 15, throwBoost: 1.0 }; - -const _holdTarget = new THREE.Vector3(); -const _currentPos = new THREE.Vector3(); -const _velocity = new THREE.Vector3(); - -export function GrabCube(): React.JSX.Element { - const camera = useThree((state) => state.camera); - const rbRef = useRef(null); - const isHolding = useRef(false); - - useDebugFolder("GrabCube", (folder) => { - folder.add(params, "stiffness", 1, 50, 1).name("Hold stiffness"); - folder.add(params, "throwBoost", 0.5, 3.0, 0.1).name("Throw boost"); - }); - - useFrame(() => { - if (!isHolding.current || !rbRef.current) return; - - camera.getWorldDirection(_holdTarget); - _holdTarget.multiplyScalar(HOLD_DISTANCE).add(camera.position); - - const t = rbRef.current.translation(); - _currentPos.set(t.x, t.y, t.z); - - _velocity - .subVectors(_holdTarget, _currentPos) - .multiplyScalar(params.stiffness); - - rbRef.current.setLinvel( - { x: _velocity.x, y: _velocity.y, z: _velocity.z }, - true, - ); - rbRef.current.setAngvel({ x: 0, y: 0, z: 0 }, true); - }); - - return ( - { - isHolding.current = true; - }} - onRelease={() => { - isHolding.current = false; - if (rbRef.current && params.throwBoost !== 1.0) { - const v = rbRef.current.linvel(); - rbRef.current.setLinvel( - { - x: v.x * params.throwBoost, - y: v.y * params.throwBoost, - z: v.z * params.throwBoost, - }, - true, - ); - } - }} - > - - - - - - ); -} diff --git a/src/world/objects/TriggerSphere.tsx b/src/world/objects/TriggerSphere.tsx deleted file mode 100644 index 8119dd7..0000000 --- a/src/world/objects/TriggerSphere.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { AudioManager } from "@/stateManager/AudioManager"; -import { InteractableObject } from "@/components/3d/InteractableObject"; - -const SPHERE_RADIUS = 0.4; -const SPAWN_POSITION: [number, number, number] = [3, 2, -3]; -const SOUND_PATH = "/sounds/fa.mp3"; - -interface TriggerSphereProps { - soundPath?: string; -} - -export function TriggerSphere({ - soundPath = SOUND_PATH, -}: TriggerSphereProps): React.JSX.Element { - return ( - { - AudioManager.getInstance().playSound(soundPath); - }} - > - - - - - - ); -} diff --git a/src/world/player/PlayerCamera.tsx b/src/world/player/PlayerCamera.tsx index e22d43c..483eed8 100644 --- a/src/world/player/PlayerCamera.tsx +++ b/src/world/player/PlayerCamera.tsx @@ -1,9 +1,6 @@ import { useEffect } from "react"; import { PointerLockControls } from "@react-three/drei"; -export const PLAYER_EYE_HEIGHT = 1.75; -export const PLAYER_CAPSULE_RADIUS = 0.35; - export function PlayerCamera(): React.JSX.Element { useEffect(() => { return () => { diff --git a/src/world/player/PlayerComponent.tsx b/src/world/player/PlayerComponent.tsx index 28b914b..dc206c5 100644 --- a/src/world/player/PlayerComponent.tsx +++ b/src/world/player/PlayerComponent.tsx @@ -1,6 +1,11 @@ import { useEffect } from "react"; import { useThree } from "@react-three/fiber"; import type { Octree } from "three/addons/math/Octree.js"; +import { + PLAYER_SPAWN_X, + PLAYER_SPAWN_Y_DEFAULT, + PLAYER_SPAWN_Z, +} from "@/data/playerConfig"; import { PlayerCamera } from "@/world/player/PlayerCamera"; import { PlayerController } from "@/world/player/PlayerController"; @@ -11,12 +16,12 @@ interface PlayerComponentProps { export function PlayerComponent({ octree = null, - spawnY = 100, + spawnY = PLAYER_SPAWN_Y_DEFAULT, }: PlayerComponentProps): React.JSX.Element { const camera = useThree((state) => state.camera); useEffect(() => { - camera.position.set(0, spawnY, 0); + camera.position.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z); }, [camera, spawnY]); return ( diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index e21a797..fa33ba3 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -3,16 +3,29 @@ import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { Capsule } from "three/addons/math/Capsule.js"; import type { Octree } from "three/addons/math/Octree.js"; -import { InteractionManager } from "@/stateManager/InteractionManager"; import { - PLAYER_EYE_HEIGHT, + INTERACT_KEY, + JUMP_KEY, + MOVE_BACKWARD_KEY, + MOVE_FORWARD_KEY, + MOVE_LEFT_KEY, + MOVE_RIGHT_KEY, + PRIMARY_INTERACT_MOUSE_BUTTON, +} from "@/data/keybindings"; +import { + PLAYER_ACCELERATION_MULTIPLIER, + PLAYER_AIR_CONTROL_FACTOR, PLAYER_CAPSULE_RADIUS, -} from "@/world/player/PlayerCamera"; - -const WALK_SPEED = 11; -const AIR_CONTROL = 0.35; -const JUMP_SPEED = 9; -const GRAVITY = 30; + PLAYER_EYE_HEIGHT, + PLAYER_GRAVITY, + PLAYER_JUMP_SPEED, + PLAYER_MAX_DELTA, + PLAYER_SPAWN_X, + PLAYER_SPAWN_Z, + PLAYER_WALK_SPEED, + PLAYER_XZ_DAMPING_FACTOR, +} from "@/data/playerConfig"; +import { InteractionManager } from "@/stateManager/InteractionManager"; type Keys = { forward: boolean; @@ -60,11 +73,11 @@ export function PlayerController({ octree }: PlayerControllerProps): null { useEffect(() => { const spawnY = camera.position.y; capsule.current.start.set( - 0, + PLAYER_SPAWN_X, spawnY - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, - 0, + PLAYER_SPAWN_Z, ); - capsule.current.end.set(0, spawnY, 0); + capsule.current.end.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -73,22 +86,22 @@ export function PlayerController({ octree }: PlayerControllerProps): null { const handleKeyDown = (event: KeyboardEvent): void => { switch (event.key.toLowerCase()) { - case "z": + case MOVE_FORWARD_KEY: keys.current.forward = true; break; - case "s": + case MOVE_BACKWARD_KEY: keys.current.backward = true; break; - case "q": + case MOVE_LEFT_KEY: keys.current.left = true; break; - case "d": + case MOVE_RIGHT_KEY: keys.current.right = true; break; - case " ": + case JUMP_KEY: wantsJump.current = true; break; - case "e": + case INTERACT_KEY: if (interaction.getState().focused?.kind === "trigger") { interaction.pressInteract(); } @@ -101,19 +114,19 @@ export function PlayerController({ octree }: PlayerControllerProps): null { const handleKeyUp = (event: KeyboardEvent): void => { switch (event.key.toLowerCase()) { - case "z": + case MOVE_FORWARD_KEY: keys.current.forward = false; break; - case "s": + case MOVE_BACKWARD_KEY: keys.current.backward = false; break; - case "q": + case MOVE_LEFT_KEY: keys.current.left = false; break; - case "d": + case MOVE_RIGHT_KEY: keys.current.right = false; break; - case "e": + case INTERACT_KEY: if (interaction.getState().focused?.kind === "trigger") { interaction.releaseInteract(); } @@ -125,14 +138,14 @@ export function PlayerController({ octree }: PlayerControllerProps): null { }; const handleMouseDown = (event: MouseEvent): void => { - if (event.button !== 0) return; + if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return; if (interaction.getState().focused?.kind === "grab") { interaction.pressInteract(); } }; const handleMouseUp = (event: MouseEvent): void => { - if (event.button !== 0) return; + if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return; if (interaction.getState().holding) { interaction.releaseInteract(); } @@ -153,8 +166,7 @@ export function PlayerController({ octree }: PlayerControllerProps): null { }, []); useFrame((_, delta) => { - // Clamp delta so physics don't explode on tab focus regain - const dt = Math.min(delta, 0.05); + const dt = Math.min(delta, PLAYER_MAX_DELTA); // Compute wish direction from camera yaw (XZ only) camera.getWorldDirection(_forward); @@ -172,12 +184,15 @@ export function PlayerController({ octree }: PlayerControllerProps): null { if (_wishDir.lengthSq() > 0) _wishDir.normalize(); // Accelerate horizontally - const accel = onFloor.current ? WALK_SPEED : WALK_SPEED * AIR_CONTROL; - velocity.current.x += _wishDir.x * accel * dt * 9; - velocity.current.z += _wishDir.z * accel * dt * 9; + const accel = onFloor.current + ? PLAYER_WALK_SPEED + : PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR; + velocity.current.x += + _wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER; + velocity.current.z += + _wishDir.z * accel * dt * PLAYER_ACCELERATION_MULTIPLIER; - // Exponential damping on XZ - const damping = Math.exp(-8 * dt); + const damping = Math.exp(-PLAYER_XZ_DAMPING_FACTOR * dt); velocity.current.x *= damping; velocity.current.z *= damping; @@ -185,11 +200,11 @@ export function PlayerController({ octree }: PlayerControllerProps): null { if (onFloor.current) { velocity.current.y = Math.max(0, velocity.current.y); if (wantsJump.current) { - velocity.current.y = JUMP_SPEED; + velocity.current.y = PLAYER_JUMP_SPEED; onFloor.current = false; } } else { - velocity.current.y -= GRAVITY * dt; + velocity.current.y -= PLAYER_GRAVITY * dt; } wantsJump.current = false;