diff --git a/public/models/map/blocking/model.glb b/public/models/map/blocking/model.glb deleted file mode 100644 index 98c45cc..0000000 --- a/public/models/map/blocking/model.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73f7be151055fc5e3cd51b6b2f0db717d442c8ce1645dae8400ac5644e8e4d45 -size 2067524 diff --git a/public/models/map/blocking/model.gltf b/public/models/map/blocking/model.gltf deleted file mode 100644 index 3070180..0000000 --- a/public/models/map/blocking/model.gltf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86174fe3ea442f7a13b86d40d96d315f1ae57d894daa69537f4266eb08fec513 -size 2839228 diff --git a/public/models/map/model.gltf b/public/models/map/model.gltf new file mode 100644 index 0000000..e1fd00c --- /dev/null +++ b/public/models/map/model.gltf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f11632d7daa81f186cf7e9a79631c0aac929c7a1e68ee10676e83837436652f +size 3220979 diff --git a/src/utils/debug/Debug.ts b/src/utils/debug/Debug.ts index a05ce06..aae6b86 100644 --- a/src/utils/debug/Debug.ts +++ b/src/utils/debug/Debug.ts @@ -7,6 +7,7 @@ export class Debug { public readonly active: boolean; private readonly gui: GUI | null; private readonly folders = new Map(); + private readonly registeredFolders = new Set(); private readonly listeners = new Set<() => void>(); private readonly controls: { cameraMode: CameraMode } = { cameraMode: "player", @@ -41,13 +42,22 @@ export class Debug { } } - createFolder(name: string): GUI; - createFolder(name: string): GUI | null; + /** + * 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. + */ createFolder(name: string): GUI | null { if (!this.gui) { return null; } + if (this.registeredFolders.has(name)) { + return null; + } + + this.registeredFolders.add(name); + const existingFolder = this.folders.get(name); if (existingFolder) { diff --git a/src/world/Lighting.tsx b/src/world/Lighting.tsx index c95c0cb..5ba14e6 100644 --- a/src/world/Lighting.tsx +++ b/src/world/Lighting.tsx @@ -32,6 +32,11 @@ export function Lighting(): React.JSX.Element { const folder = debug.createFolder("Lighting"); + // null = already registered (StrictMode double-mount), skip adding controls + if (!folder) { + return; + } + 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"); diff --git a/src/world/Map.tsx b/src/world/Map.tsx deleted file mode 100644 index 043181c..0000000 --- a/src/world/Map.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useMemo } from "react"; -import { useGLTF } from "@react-three/drei"; -import * as THREE from "three"; - -const MODEL_PATH = "/models/map/blocking/model.glb"; - -type CenteredModel = { - object: THREE.Object3D; - scale: number; -}; - -function centerModel(model: THREE.Object3D): number { - model.updateMatrixWorld(true); - - const bounds = new THREE.Box3().setFromObject(model); - const center = bounds.getCenter(new THREE.Vector3()); - const size = bounds.getSize(new THREE.Vector3()); - - model.position.set(-center.x, -bounds.min.y, -center.z); - - return size.length() > 0 && size.length() < 10 ? 5 : 1; -} - -export function Map(): React.JSX.Element { - const { scene } = useGLTF(MODEL_PATH); - const centeredModel = useMemo(() => { - const object = scene.clone(true); - const scale = centerModel(object); - - return { object, scale }; - }, [scene]); - - return ( - - - - ); -} - -useGLTF.preload(MODEL_PATH); diff --git a/src/world/World.tsx b/src/world/World.tsx index 3354220..81aea1a 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -1,10 +1,8 @@ -import { Suspense } from "react"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers"; import { Environment } from "@/world/Environment"; import { Lighting } from "@/world/Lighting"; -import { Map } from "@/world/Map"; import { PlayerComponent } from "@/world/player/PlayerComponent"; export function World(): React.JSX.Element { @@ -15,10 +13,8 @@ export function World(): React.JSX.Element { - {cameraMode === "debug" ? : } - - - + {cameraMode === "debug" ? : null} + {cameraMode === "debug" ? null : } ); } diff --git a/src/world/player/PlayerCamera.tsx b/src/world/player/PlayerCamera.tsx index ae7ce5d..1984f8a 100644 --- a/src/world/player/PlayerCamera.tsx +++ b/src/world/player/PlayerCamera.tsx @@ -1,25 +1,14 @@ import { useEffect } from "react"; import { PointerLockControls } from "@react-three/drei"; -import { useThree } from "@react-three/fiber"; -import * as THREE from "three"; export const PLAYER_EYE_HEIGHT = 1.75; -const PLAYER_SPAWN_POSITION = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 6); -const PLAYER_LOOK_AT = new THREE.Vector3(0, PLAYER_EYE_HEIGHT, 0); - export function PlayerCamera(): React.JSX.Element { - const camera = useThree((state) => state.camera); - useEffect(() => { - camera.position.copy(PLAYER_SPAWN_POSITION); - camera.lookAt(PLAYER_LOOK_AT); - camera.updateProjectionMatrix(); - return () => { document.exitPointerLock?.(); }; - }, [camera]); + }, []); return ; } diff --git a/src/world/player/PlayerComponent.tsx b/src/world/player/PlayerComponent.tsx index 8cd1f19..f3f941c 100644 --- a/src/world/player/PlayerComponent.tsx +++ b/src/world/player/PlayerComponent.tsx @@ -1,7 +1,17 @@ -import { PlayerCamera } from "@/world/player/PlayerCamera"; +import { useEffect } from "react"; +import { useThree } from "@react-three/fiber"; +import { PlayerCamera, PLAYER_EYE_HEIGHT } from "@/world/player/PlayerCamera"; import { PlayerController } from "@/world/player/PlayerController"; +const SPAWN_POSITION = { x: 0, y: PLAYER_EYE_HEIGHT, z: 0 }; + export function PlayerComponent(): React.JSX.Element { + const camera = useThree((state) => state.camera); + + useEffect(() => { + camera.position.set(SPAWN_POSITION.x, SPAWN_POSITION.y, SPAWN_POSITION.z); + }, [camera]); + return ( <> diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index 92a8206..4239bb7 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -3,30 +3,32 @@ import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { PLAYER_EYE_HEIGHT } from "@/world/player/PlayerCamera"; -const JUMP_HEIGHT = 1; -const GRAVITY = 18; -const JUMP_VELOCITY = Math.sqrt(2 * GRAVITY * JUMP_HEIGHT); const MOVE_SPEED = 5; +const GRAVITY = -20; +const JUMP_VELOCITY = 7; +const FLOOR_Y = 0; -type PlayerKeys = { +type Keys = { forward: boolean; backward: boolean; left: boolean; right: boolean; + jump: boolean; }; -const DEFAULT_KEYS: PlayerKeys = { +const DEFAULT_KEYS: Keys = { forward: false, backward: false, left: false, right: false, + jump: false, }; export function PlayerController(): null { const camera = useThree((state) => state.camera); - const keys = useRef({ ...DEFAULT_KEYS }); - const interact = useRef<() => void>(() => {}); - const verticalVelocity = useRef(0); + const keys = useRef({ ...DEFAULT_KEYS }); + const velocityY = useRef(0); + const isGrounded = useRef(false); const forward = useRef(new THREE.Vector3()); const right = useRef(new THREE.Vector3()); const movement = useRef(new THREE.Vector3()); @@ -49,15 +51,8 @@ export function PlayerController(): null { case "d": keys.current.right = pressed; break; - case "e": - if (pressed) { - interact.current(); - } - break; case " ": - if (pressed && camera.position.y <= PLAYER_EYE_HEIGHT) { - verticalVelocity.current = JUMP_VELOCITY; - } + if (pressed) keys.current.jump = true; break; default: return; @@ -77,15 +72,13 @@ export function PlayerController(): null { window.removeEventListener("keyup", handleKeyUp); keys.current = { ...DEFAULT_KEYS }; }; - }, [camera]); + }, []); useFrame((_, delta) => { const currentForward = forward.current; const currentRight = right.current; const currentMovement = movement.current; - currentMovement.set(0, 0, 0); - camera.getWorldDirection(currentForward); currentForward.setY(0); @@ -94,40 +87,35 @@ export function PlayerController(): null { currentRight.crossVectors(currentForward, up.current).normalize(); } - if (keys.current.forward) { - currentMovement.add(currentForward); - } + currentMovement.set(0, 0, 0); - if (keys.current.backward) { - currentMovement.sub(currentForward); - } - - if (keys.current.left) { - currentMovement.sub(currentRight); - } - - if (keys.current.right) { - currentMovement.add(currentRight); - } + if (keys.current.forward) currentMovement.add(currentForward); + if (keys.current.backward) currentMovement.sub(currentForward); + if (keys.current.left) currentMovement.sub(currentRight); + if (keys.current.right) currentMovement.add(currentRight); if (currentMovement.lengthSq() > 0) { currentMovement.normalize().multiplyScalar(MOVE_SPEED * delta); camera.position.add(currentMovement); } - verticalVelocity.current -= GRAVITY * delta; + const groundY = FLOOR_Y + PLAYER_EYE_HEIGHT; + isGrounded.current = camera.position.y <= groundY + 0.01; - const nextY = camera.position.y + verticalVelocity.current * delta; - camera.position.set(camera.position.x, nextY, camera.position.z); - - if (camera.position.y < PLAYER_EYE_HEIGHT) { - verticalVelocity.current = 0; - camera.position.set( - camera.position.x, - PLAYER_EYE_HEIGHT, - camera.position.z, - ); + if (keys.current.jump && isGrounded.current) { + velocityY.current = JUMP_VELOCITY; + keys.current.jump = false; } + + if (!isGrounded.current) { + velocityY.current += GRAVITY * delta; + } else if (velocityY.current < 0) { + velocityY.current = 0; + } + + camera.position.setY( + Math.max(groundY, camera.position.y + velocityY.current * delta), + ); }); return null;