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) {