From 665d9f97026fe2629e4ba3c688a3b1d2568302c6 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 26 May 2026 23:52:12 +0200 Subject: [PATCH] fix(map): add world plane collision and respawn --- src/components/debug/scene/DebugHelpers.tsx | 4 +- src/data/player/playerConfig.ts | 2 + src/data/world/terrainBoundaryConfig.ts | 10 ---- src/data/world/waterConfig.ts | 4 +- src/data/world/worldBoundsConfig.ts | 13 +++++ src/world/GameMap.tsx | 2 + src/world/GameMapCollision.tsx | 4 +- src/world/WorldPlane.tsx | 20 +++++++ .../collision/TerrainBoundaryCollision.tsx | 36 ------------ src/world/collision/WorldBoundsCollision.tsx | 11 ++++ src/world/collision/WorldPlaneCollision.tsx | 20 +++++++ src/world/collision/WorldWallsCollision.tsx | 38 +++++++++++++ src/world/player/PlayerController.tsx | 55 ++++++++++++++++--- 13 files changed, 159 insertions(+), 60 deletions(-) delete mode 100644 src/data/world/terrainBoundaryConfig.ts create mode 100644 src/data/world/worldBoundsConfig.ts create mode 100644 src/world/WorldPlane.tsx delete mode 100644 src/world/collision/TerrainBoundaryCollision.tsx create mode 100644 src/world/collision/WorldBoundsCollision.tsx create mode 100644 src/world/collision/WorldPlaneCollision.tsx create mode 100644 src/world/collision/WorldWallsCollision.tsx diff --git a/src/components/debug/scene/DebugHelpers.tsx b/src/components/debug/scene/DebugHelpers.tsx index 17b50ac..c94be16 100644 --- a/src/components/debug/scene/DebugHelpers.tsx +++ b/src/components/debug/scene/DebugHelpers.tsx @@ -6,12 +6,14 @@ import { DEBUG_GRID_SIZE, DEBUG_GRID_Y, } from "@/data/debug/debugConfig"; +import { useSceneMode } from "@/hooks/debug/useSceneMode"; import { Debug } from "@/utils/debug/Debug"; export function DebugHelpers(): React.JSX.Element | null { const debug = Debug.getInstance(); + const sceneMode = useSceneMode(); - if (!debug.active) { + if (!debug.active || sceneMode === "game") { return null; } diff --git a/src/data/player/playerConfig.ts b/src/data/player/playerConfig.ts index 360cebe..a947caa 100644 --- a/src/data/player/playerConfig.ts +++ b/src/data/player/playerConfig.ts @@ -10,6 +10,8 @@ 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_FALL_RESPAWN_Y = -20; +export const PLAYER_FALL_RESPAWN_DELAY = 3; export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0]; export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0]; diff --git a/src/data/world/terrainBoundaryConfig.ts b/src/data/world/terrainBoundaryConfig.ts deleted file mode 100644 index c61d66d..0000000 --- a/src/data/world/terrainBoundaryConfig.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Vector3Tuple } from "@/types/three/three"; - -export const TERRAIN_BOUNDARY_CONFIG = { - enabled: true, - center: [-10, 8, -2] as Vector3Tuple, - radius: 135, - height: 28, - thickness: 3, - segments: 48, -}; diff --git a/src/data/world/waterConfig.ts b/src/data/world/waterConfig.ts index 5f94972..3ebf86c 100644 --- a/src/data/world/waterConfig.ts +++ b/src/data/world/waterConfig.ts @@ -35,8 +35,8 @@ export const WATER_SHADER_CONFIG = { export const WATER_STREAMING_CONFIG = { enabled: true, loadDistance: 40, - unloadDistance: 35, - updateInterval: 250, + unloadDistance: 48, + updateInterval: 350, }; export const WATER_SURFACES: WaterSurfaceConfig[] = [ diff --git a/src/data/world/worldBoundsConfig.ts b/src/data/world/worldBoundsConfig.ts new file mode 100644 index 0000000..ac97661 --- /dev/null +++ b/src/data/world/worldBoundsConfig.ts @@ -0,0 +1,13 @@ +import { TERRAIN_COLORS } from "@/data/world/terrainConfig"; +import type { Vector3Tuple } from "@/types/three/three"; + +export const WORLD_BOUNDS_CONFIG = { + enabled: true, + center: [0, 0, 0] as Vector3Tuple, + planeColor: TERRAIN_COLORS.grass1.hex, + planeY: -0.04, + planeCollisionThickness: 1, + size: [270, 260] as const, + wallHeight: 28, + wallThickness: 4, +}; diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index d62872e..6935dd5 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -26,6 +26,7 @@ import { MapInstancingSystem } from "@/world/map-instancing/MapInstancingSystem" import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig"; import { VegetationSystem } from "@/world/vegetation/VegetationSystem"; import { WaterSystem } from "@/world/water/WaterSystem"; +import { WorldPlane } from "@/world/WorldPlane"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; import { logger } from "@/utils/core/Logger"; import { loadMapSceneData } from "@/utils/map/loadMapSceneData"; @@ -258,6 +259,7 @@ export function GameMap({ ))} + {isMapModelVisible("terrain", { groups, models }) ? ( diff --git a/src/world/GameMapCollision.tsx b/src/world/GameMapCollision.tsx index 2d5e498..7ce3d21 100644 --- a/src/world/GameMapCollision.tsx +++ b/src/world/GameMapCollision.tsx @@ -11,7 +11,7 @@ import * as THREE from "three"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useOctreeGraphNode } from "@/hooks/three/useOctreeGraphNode"; -import { TerrainBoundaryCollision } from "@/world/collision/TerrainBoundaryCollision"; +import { WorldBoundsCollision } from "@/world/collision/WorldBoundsCollision"; import type { MapNode } from "@/types/editor/editor"; import type { OctreeReadyHandler } from "@/types/three/three"; import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading"; @@ -174,7 +174,7 @@ export function GameMapCollision({ return ( - {mapReady ? : null} + {mapReady ? : null} {mapReady ? collisionNodes.map((mapNode, index) => ( + + + + ); +} diff --git a/src/world/collision/TerrainBoundaryCollision.tsx b/src/world/collision/TerrainBoundaryCollision.tsx deleted file mode 100644 index 8ac0e50..0000000 --- a/src/world/collision/TerrainBoundaryCollision.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { TERRAIN_BOUNDARY_CONFIG } from "@/data/world/terrainBoundaryConfig"; - -function createBoundarySegments(): React.JSX.Element[] { - const segments: React.JSX.Element[] = []; - const { - center, - height, - radius, - segments: segmentCount, - thickness, - } = TERRAIN_BOUNDARY_CONFIG; - const arcLength = (Math.PI * 2 * radius) / segmentCount; - - for (let index = 0; index < segmentCount; index++) { - const angle = (index / segmentCount) * Math.PI * 2; - const x = center[0] + Math.cos(angle) * radius; - const z = center[2] + Math.sin(angle) * radius; - - segments.push( - - - - , - ); - } - - return segments; -} - -export function TerrainBoundaryCollision(): React.JSX.Element | null { - if (!TERRAIN_BOUNDARY_CONFIG.enabled) { - return null; - } - - return <>{createBoundarySegments()}; -} diff --git a/src/world/collision/WorldBoundsCollision.tsx b/src/world/collision/WorldBoundsCollision.tsx new file mode 100644 index 0000000..d6b2ce2 --- /dev/null +++ b/src/world/collision/WorldBoundsCollision.tsx @@ -0,0 +1,11 @@ +import { WorldPlaneCollision } from "@/world/collision/WorldPlaneCollision"; +import { WorldWallsCollision } from "@/world/collision/WorldWallsCollision"; + +export function WorldBoundsCollision(): React.JSX.Element { + return ( + + + + + ); +} diff --git a/src/world/collision/WorldPlaneCollision.tsx b/src/world/collision/WorldPlaneCollision.tsx new file mode 100644 index 0000000..0f3ab4d --- /dev/null +++ b/src/world/collision/WorldPlaneCollision.tsx @@ -0,0 +1,20 @@ +import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig"; + +export function WorldPlaneCollision(): React.JSX.Element | null { + if (!WORLD_BOUNDS_CONFIG.enabled) { + return null; + } + + const { center, planeCollisionThickness, planeY, size } = WORLD_BOUNDS_CONFIG; + const [width, depth] = size; + + return ( + + + + + ); +} diff --git a/src/world/collision/WorldWallsCollision.tsx b/src/world/collision/WorldWallsCollision.tsx new file mode 100644 index 0000000..e062f1d --- /dev/null +++ b/src/world/collision/WorldWallsCollision.tsx @@ -0,0 +1,38 @@ +import { WORLD_BOUNDS_CONFIG } from "@/data/world/worldBoundsConfig"; + +export function WorldWallsCollision(): React.JSX.Element | null { + if (!WORLD_BOUNDS_CONFIG.enabled) { + return null; + } + + const { center, size, wallHeight, wallThickness } = WORLD_BOUNDS_CONFIG; + const [width, depth] = size; + const wallY = center[1] + wallHeight / 2; + const halfWidth = width / 2; + const halfDepth = depth / 2; + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/world/player/PlayerController.tsx b/src/world/player/PlayerController.tsx index a8f48f9..a799236 100644 --- a/src/world/player/PlayerController.tsx +++ b/src/world/player/PlayerController.tsx @@ -17,6 +17,8 @@ import { PLAYER_AIR_CONTROL_FACTOR, PLAYER_CAPSULE_RADIUS, PLAYER_EYE_HEIGHT, + PLAYER_FALL_RESPAWN_DELAY, + PLAYER_FALL_RESPAWN_Y, PLAYER_GRAVITY, PLAYER_JUMP_SPEED, PLAYER_MAX_DELTA, @@ -57,6 +59,22 @@ const _up = new THREE.Vector3(0, 1, 0); const _translateVec = new THREE.Vector3(); const _collisionCorrection = new THREE.Vector3(); +function resetPlayerCapsule( + capsule: Capsule, + spawnPosition: Vector3Tuple, + camera: THREE.Camera, + velocity: THREE.Vector3, +): void { + capsule.start.set( + spawnPosition[0], + spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, + spawnPosition[2], + ); + capsule.end.set(...spawnPosition); + velocity.set(0, 0, 0); + camera.position.copy(capsule.end); +} + function createSpawnCapsule(spawnPosition: Vector3Tuple): Capsule { return new Capsule( new THREE.Vector3( @@ -104,6 +122,7 @@ export function PlayerController({ const movementLockedRef = useRef(movementLocked); const keys = useRef({ ...DEFAULT_KEYS }); const velocity = useRef(new THREE.Vector3()); + const fallDuration = useRef(0); const onFloor = useRef(false); const wantsJump = useRef(false); const initializedRef = useRef(false); @@ -112,16 +131,15 @@ export function PlayerController({ const capsule = useRef(createSpawnCapsule(spawnPosition)); useLayoutEffect(() => { - capsule.current.start.set( - spawnPosition[0], - spawnPosition[1] - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS, - spawnPosition[2], + resetPlayerCapsule( + capsule.current, + spawnPosition, + camera, + velocity.current, ); - capsule.current.end.set(...spawnPosition); - velocity.current.set(0, 0, 0); + fallDuration.current = 0; onFloor.current = false; wantsJump.current = false; - camera.position.copy(capsule.current.end); initializedRef.current = true; }, [camera, spawnPosition]); @@ -211,6 +229,27 @@ export function PlayerController({ useFrame((_, delta) => { if (!initializedRef.current) return; + const dt = Math.min(delta, PLAYER_MAX_DELTA); + + if (capsule.current.end.y < PLAYER_FALL_RESPAWN_Y) { + fallDuration.current += dt; + + if (fallDuration.current >= PLAYER_FALL_RESPAWN_DELAY) { + resetPlayerCapsule( + capsule.current, + spawnPosition, + camera, + velocity.current, + ); + fallDuration.current = 0; + onFloor.current = false; + wantsJump.current = false; + return; + } + } else { + fallDuration.current = 0; + } + if (isPlayerInputLocked() || !canMove) { keys.current = { ...DEFAULT_KEYS }; velocity.current.set(0, 0, 0); @@ -218,8 +257,6 @@ export function PlayerController({ return; } - const dt = Math.min(delta, PLAYER_MAX_DELTA); - camera.getWorldDirection(_forward); _forward.setY(0); if (_forward.lengthSq() > 0) {