update : put every constante in the data folder
This commit is contained in:
@@ -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<RapierRigidBody>(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 (
|
||||||
|
<RigidBody
|
||||||
|
ref={rbRef}
|
||||||
|
type="dynamic"
|
||||||
|
colliders={colliders}
|
||||||
|
position={position}
|
||||||
|
>
|
||||||
|
<InteractableObject
|
||||||
|
kind="grab"
|
||||||
|
label={label}
|
||||||
|
position={position}
|
||||||
|
bodyRef={rbRef}
|
||||||
|
onPress={() => {
|
||||||
|
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}
|
||||||
|
</InteractableObject>
|
||||||
|
</RigidBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { RigidBody } from "@react-three/rapier";
|
|
||||||
import type { RapierRigidBody } from "@react-three/rapier";
|
import type { RapierRigidBody } from "@react-three/rapier";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { RefObject } from "react";
|
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 { Debug } from "@/utils/debug/Debug";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
import {
|
import {
|
||||||
@@ -17,9 +21,7 @@ interface InteractableObjectProps {
|
|||||||
kind: InteractableKind;
|
kind: InteractableKind;
|
||||||
label: string;
|
label: string;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
rigidBodyType?: "dynamic" | "fixed";
|
bodyRef?: RefObject<RapierRigidBody | null>;
|
||||||
colliders?: "cuboid" | "ball" | "hull";
|
|
||||||
rbRef?: RefObject<RapierRigidBody | null>;
|
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onRelease?: () => void;
|
onRelease?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -34,16 +36,12 @@ export function InteractableObject({
|
|||||||
kind,
|
kind,
|
||||||
label,
|
label,
|
||||||
position,
|
position,
|
||||||
rigidBodyType = "dynamic",
|
bodyRef,
|
||||||
colliders = "cuboid",
|
|
||||||
rbRef,
|
|
||||||
onPress,
|
onPress,
|
||||||
onRelease = () => {},
|
onRelease = () => {},
|
||||||
children,
|
children,
|
||||||
}: InteractableObjectProps): React.JSX.Element {
|
}: InteractableObjectProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const internalRef = useRef<RapierRigidBody>(null);
|
|
||||||
const bodyRef = rbRef ?? internalRef;
|
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const debugSphereRef = useRef<THREE.Mesh>(null);
|
const debugSphereRef = useRef<THREE.Mesh>(null);
|
||||||
|
|
||||||
@@ -67,7 +65,6 @@ export function InteractableObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
const body = bodyRef.current;
|
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
const manager = InteractionManager.getInstance();
|
const manager = InteractionManager.getInstance();
|
||||||
@@ -77,8 +74,8 @@ export function InteractableObject({
|
|||||||
debug.active && debug.getShowInteractionSpheres();
|
debug.active && debug.getShowInteractionSpheres();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body) {
|
if (bodyRef?.current) {
|
||||||
const t = body.translation();
|
const t = bodyRef.current.translation();
|
||||||
_objectPos.set(t.x, t.y, t.z);
|
_objectPos.set(t.x, t.y, t.z);
|
||||||
} else {
|
} else {
|
||||||
_objectPos.set(...position);
|
_objectPos.set(...position);
|
||||||
@@ -99,7 +96,6 @@ export function InteractableObject({
|
|||||||
_raycaster.far = INTERACTION_RADIUS;
|
_raycaster.far = INTERACTION_RADIUS;
|
||||||
|
|
||||||
const hits = group ? _raycaster.intersectObject(group, true) : [];
|
const hits = group ? _raycaster.intersectObject(group, true) : [];
|
||||||
|
|
||||||
const validHit = hits.find((h) => h.object !== debugSphereRef.current);
|
const validHit = hits.find((h) => h.object !== debugSphereRef.current);
|
||||||
|
|
||||||
if (validHit) {
|
if (validHit) {
|
||||||
@@ -110,24 +106,23 @@ export function InteractableObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RigidBody
|
|
||||||
ref={bodyRef}
|
|
||||||
type={rigidBodyType}
|
|
||||||
colliders={colliders}
|
|
||||||
position={position}
|
|
||||||
>
|
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
{children}
|
{children}
|
||||||
<mesh ref={debugSphereRef} visible={false}>
|
<mesh ref={debugSphereRef} visible={false}>
|
||||||
<sphereGeometry args={[INTERACTION_RADIUS, 16, 16]} />
|
<sphereGeometry
|
||||||
|
args={[
|
||||||
|
INTERACTION_RADIUS,
|
||||||
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
|
INTERACTION_DEBUG_SPHERE_SEGMENTS,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
color="#facc15"
|
color={INTERACTION_DEBUG_SPHERE_COLOR}
|
||||||
wireframe
|
wireframe
|
||||||
transparent
|
transparent
|
||||||
opacity={0.25}
|
opacity={INTERACTION_DEBUG_SPHERE_OPACITY}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
</RigidBody>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <primitive object={scene.clone()} position={position} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SpawnedModel[]>([]);
|
||||||
|
const positionRef = useRef(position);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RigidBody type="fixed" colliders={colliders} position={position}>
|
||||||
|
<InteractableObject
|
||||||
|
kind="trigger"
|
||||||
|
label={label}
|
||||||
|
position={position}
|
||||||
|
onPress={() => {
|
||||||
|
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}
|
||||||
|
</InteractableObject>
|
||||||
|
</RigidBody>
|
||||||
|
|
||||||
|
{spawnModel &&
|
||||||
|
spawned.map((s) => (
|
||||||
|
<SpawnedModelInstance
|
||||||
|
key={s.id}
|
||||||
|
path={spawnModel}
|
||||||
|
position={s.position}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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];
|
||||||
@@ -1,3 +1,25 @@
|
|||||||
export function Environment(): React.JSX.Element {
|
import * as THREE from "three";
|
||||||
return <color attach="background" args={["#0b1018"]} />;
|
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 <primitive attach="background" object={texture} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment(): React.JSX.Element {
|
||||||
|
const sceneMode = useSceneMode();
|
||||||
|
|
||||||
|
if (sceneMode === "physics") {
|
||||||
|
return (
|
||||||
|
<color attach="background" args={[PHYSICS_SCENE_BACKGROUND_COLOR]} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SkyBox />;
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-14
@@ -1,6 +1,26 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import type { AmbientLight, DirectionalLight } from "three";
|
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";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
|
||||||
type LightingState = {
|
type LightingState = {
|
||||||
@@ -11,24 +31,40 @@ type LightingState = {
|
|||||||
sunZ: number;
|
sunZ: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIGHTING_STATE: LightingState = {
|
const LIGHTING_STATE: LightingState = { ...LIGHTING_DEFAULTS };
|
||||||
ambientIntensity: 1.8,
|
|
||||||
sunIntensity: 2.8,
|
|
||||||
sunX: 60,
|
|
||||||
sunY: 80,
|
|
||||||
sunZ: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Lighting(): React.JSX.Element {
|
export function Lighting(): React.JSX.Element {
|
||||||
const ambient = useRef<AmbientLight>(null);
|
const ambient = useRef<AmbientLight>(null);
|
||||||
const sun = useRef<DirectionalLight>(null);
|
const sun = useRef<DirectionalLight>(null);
|
||||||
|
|
||||||
useDebugFolder("Lighting", (folder) => {
|
useDebugFolder("Lighting", (folder) => {
|
||||||
folder.add(LIGHTING_STATE, "ambientIntensity", 0, 5, 0.1).name("Ambient");
|
folder
|
||||||
folder.add(LIGHTING_STATE, "sunIntensity", 0, 8, 0.1).name("Sun Intensity");
|
.add(
|
||||||
folder.add(LIGHTING_STATE, "sunX", -100, 100, 1).name("Sun X");
|
LIGHTING_STATE,
|
||||||
folder.add(LIGHTING_STATE, "sunY", 0, 150, 1).name("Sun Y");
|
"ambientIntensity",
|
||||||
folder.add(LIGHTING_STATE, "sunZ", -100, 100, 1).name("Sun Z");
|
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(() => {
|
useFrame(() => {
|
||||||
@@ -51,7 +87,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
<ambientLight
|
<ambientLight
|
||||||
ref={ambient}
|
ref={ambient}
|
||||||
intensity={LIGHTING_STATE.ambientIntensity}
|
intensity={LIGHTING_STATE.ambientIntensity}
|
||||||
color="#dbeafe"
|
color={AMBIENT_LIGHT_COLOR}
|
||||||
/>
|
/>
|
||||||
<directionalLight
|
<directionalLight
|
||||||
ref={sun}
|
ref={sun}
|
||||||
@@ -61,7 +97,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
LIGHTING_STATE.sunZ,
|
LIGHTING_STATE.sunZ,
|
||||||
]}
|
]}
|
||||||
intensity={LIGHTING_STATE.sunIntensity}
|
intensity={LIGHTING_STATE.sunIntensity}
|
||||||
color="#fff7ed"
|
color={SUN_LIGHT_COLOR}
|
||||||
castShadow
|
castShadow
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@ import { useThree } from "@react-three/fiber";
|
|||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Octree } from "three/addons/math/Octree.js";
|
import { Octree } from "three/addons/math/Octree.js";
|
||||||
|
import { MAP_DEBUG_BOX_HELPER_COLOR } from "@/data/debugConfig";
|
||||||
import { Debug } from "@/utils/debug/Debug";
|
import { Debug } from "@/utils/debug/Debug";
|
||||||
|
|
||||||
const MAP_PATH = "/models/map/model.gltf";
|
const MAP_PATH = "/models/map/model.gltf";
|
||||||
@@ -38,7 +39,7 @@ export function Map({ onOctreeReady }: MapProps): React.JSX.Element {
|
|||||||
|
|
||||||
groupRef.current.traverse((child) => {
|
groupRef.current.traverse((child) => {
|
||||||
if (!(child instanceof THREE.Mesh)) return;
|
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);
|
scene.add(helper);
|
||||||
helpers.push(helper);
|
helpers.push(helper);
|
||||||
});
|
});
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,9 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import type { Octree } from "three/addons/math/Octree.js";
|
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 { useCameraMode } from "@/hooks/debug/useCameraMode";
|
||||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||||
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
|
import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls";
|
||||||
@@ -32,7 +36,9 @@ export function World(): React.JSX.Element {
|
|||||||
{cameraMode !== "debug" ? (
|
{cameraMode !== "debug" ? (
|
||||||
<PlayerComponent
|
<PlayerComponent
|
||||||
octree={octree}
|
octree={octree}
|
||||||
spawnY={sceneMode === "game" ? 100 : 3}
|
spawnY={
|
||||||
|
sceneMode === "game" ? PLAYER_SPAWN_Y_GAME : PLAYER_SPAWN_Y_PHYSICS
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,8 +2,25 @@ import { useEffect, useRef } from "react";
|
|||||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Octree } from "three/addons/math/Octree.js";
|
import { Octree } from "three/addons/math/Octree.js";
|
||||||
import { GrabCube } from "@/world/objects/GrabCube";
|
import { GrabbableObject } from "@/components/3d/GrabbableObject";
|
||||||
import { TriggerSphere } from "@/world/objects/TriggerSphere";
|
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 {
|
interface TestSceneProps {
|
||||||
onOctreeReady: (octree: Octree) => void;
|
onOctreeReady: (octree: Octree) => void;
|
||||||
@@ -28,21 +45,54 @@ export function TestScene({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Invisible floor mesh for Octree player collision */}
|
|
||||||
<group ref={floorRef}>
|
<group ref={floorRef}>
|
||||||
<mesh visible={false} position={[0, -0.5, 0]}>
|
<mesh visible={false} position={TEST_SCENE_FLOOR_POSITION}>
|
||||||
<boxGeometry args={[200, 1, 200]} />
|
<boxGeometry args={TEST_SCENE_FLOOR_SIZE} />
|
||||||
<meshBasicMaterial />
|
<meshBasicMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* Rapier physics for interactable objects */}
|
|
||||||
<Physics>
|
<Physics>
|
||||||
<RigidBody type="fixed">
|
<RigidBody type="fixed">
|
||||||
<CuboidCollider args={[100, 0.5, 100]} position={[0, -0.5, 0]} />
|
<CuboidCollider
|
||||||
|
args={TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS}
|
||||||
|
position={TEST_SCENE_FLOOR_POSITION}
|
||||||
|
/>
|
||||||
</RigidBody>
|
</RigidBody>
|
||||||
<GrabCube />
|
|
||||||
<TriggerSphere />
|
<GrabbableObject
|
||||||
|
position={TEST_SCENE_GRABBABLE_POSITION}
|
||||||
|
colliders="cuboid"
|
||||||
|
>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
|
<boxGeometry args={TEST_SCENE_GRABBABLE_BOX_SIZE} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={TEST_SCENE_GRABBABLE_COLOR}
|
||||||
|
roughness={TEST_SCENE_GRABBABLE_ROUGHNESS}
|
||||||
|
metalness={TEST_SCENE_GRABBABLE_METALNESS}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</GrabbableObject>
|
||||||
|
|
||||||
|
<TriggerObject
|
||||||
|
position={TEST_SCENE_TRIGGER_POSITION}
|
||||||
|
soundPath={TEST_SCENE_TRIGGER_SOUND_PATH}
|
||||||
|
>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
|
<sphereGeometry
|
||||||
|
args={[
|
||||||
|
TEST_SCENE_TRIGGER_RADIUS,
|
||||||
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
|
TEST_SCENE_TRIGGER_SEGMENTS,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={TEST_SCENE_TRIGGER_COLOR}
|
||||||
|
roughness={TEST_SCENE_TRIGGER_ROUGHNESS}
|
||||||
|
metalness={TEST_SCENE_TRIGGER_METALNESS}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
</TriggerObject>
|
||||||
</Physics>
|
</Physics>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<RapierRigidBody>(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 (
|
|
||||||
<InteractableObject
|
|
||||||
kind="grab"
|
|
||||||
label="Prendre"
|
|
||||||
position={SPAWN_POSITION}
|
|
||||||
rigidBodyType="dynamic"
|
|
||||||
colliders="cuboid"
|
|
||||||
rbRef={rbRef}
|
|
||||||
onPress={() => {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<mesh castShadow receiveShadow>
|
|
||||||
<boxGeometry args={[CUBE_SIZE, CUBE_SIZE, CUBE_SIZE]} />
|
|
||||||
<meshStandardMaterial color="#e07b39" roughness={0.6} metalness={0.1} />
|
|
||||||
</mesh>
|
|
||||||
</InteractableObject>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<InteractableObject
|
|
||||||
kind="trigger"
|
|
||||||
label="Interagir"
|
|
||||||
position={SPAWN_POSITION}
|
|
||||||
rigidBodyType="fixed"
|
|
||||||
colliders="ball"
|
|
||||||
onPress={() => {
|
|
||||||
AudioManager.getInstance().playSound(soundPath);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<mesh castShadow receiveShadow>
|
|
||||||
<sphereGeometry args={[SPHERE_RADIUS, 32, 32]} />
|
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.5} />
|
|
||||||
</mesh>
|
|
||||||
</InteractableObject>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { PointerLockControls } from "@react-three/drei";
|
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 {
|
export function PlayerCamera(): React.JSX.Element {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import type { Octree } from "three/addons/math/Octree.js";
|
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 { PlayerCamera } from "@/world/player/PlayerCamera";
|
||||||
import { PlayerController } from "@/world/player/PlayerController";
|
import { PlayerController } from "@/world/player/PlayerController";
|
||||||
|
|
||||||
@@ -11,12 +16,12 @@ interface PlayerComponentProps {
|
|||||||
|
|
||||||
export function PlayerComponent({
|
export function PlayerComponent({
|
||||||
octree = null,
|
octree = null,
|
||||||
spawnY = 100,
|
spawnY = PLAYER_SPAWN_Y_DEFAULT,
|
||||||
}: PlayerComponentProps): React.JSX.Element {
|
}: PlayerComponentProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
camera.position.set(0, spawnY, 0);
|
camera.position.set(PLAYER_SPAWN_X, spawnY, PLAYER_SPAWN_Z);
|
||||||
}, [camera, spawnY]);
|
}, [camera, spawnY]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,16 +3,29 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { Capsule } from "three/addons/math/Capsule.js";
|
import { Capsule } from "three/addons/math/Capsule.js";
|
||||||
import type { Octree } from "three/addons/math/Octree.js";
|
import type { Octree } from "three/addons/math/Octree.js";
|
||||||
import { InteractionManager } from "@/stateManager/InteractionManager";
|
|
||||||
import {
|
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,
|
PLAYER_CAPSULE_RADIUS,
|
||||||
} from "@/world/player/PlayerCamera";
|
PLAYER_EYE_HEIGHT,
|
||||||
|
PLAYER_GRAVITY,
|
||||||
const WALK_SPEED = 11;
|
PLAYER_JUMP_SPEED,
|
||||||
const AIR_CONTROL = 0.35;
|
PLAYER_MAX_DELTA,
|
||||||
const JUMP_SPEED = 9;
|
PLAYER_SPAWN_X,
|
||||||
const GRAVITY = 30;
|
PLAYER_SPAWN_Z,
|
||||||
|
PLAYER_WALK_SPEED,
|
||||||
|
PLAYER_XZ_DAMPING_FACTOR,
|
||||||
|
} from "@/data/playerConfig";
|
||||||
|
import { InteractionManager } from "@/stateManager/InteractionManager";
|
||||||
|
|
||||||
type Keys = {
|
type Keys = {
|
||||||
forward: boolean;
|
forward: boolean;
|
||||||
@@ -60,11 +73,11 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const spawnY = camera.position.y;
|
const spawnY = camera.position.y;
|
||||||
capsule.current.start.set(
|
capsule.current.start.set(
|
||||||
0,
|
PLAYER_SPAWN_X,
|
||||||
spawnY - PLAYER_EYE_HEIGHT + PLAYER_CAPSULE_RADIUS,
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -73,22 +86,22 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
switch (event.key.toLowerCase()) {
|
||||||
case "z":
|
case MOVE_FORWARD_KEY:
|
||||||
keys.current.forward = true;
|
keys.current.forward = true;
|
||||||
break;
|
break;
|
||||||
case "s":
|
case MOVE_BACKWARD_KEY:
|
||||||
keys.current.backward = true;
|
keys.current.backward = true;
|
||||||
break;
|
break;
|
||||||
case "q":
|
case MOVE_LEFT_KEY:
|
||||||
keys.current.left = true;
|
keys.current.left = true;
|
||||||
break;
|
break;
|
||||||
case "d":
|
case MOVE_RIGHT_KEY:
|
||||||
keys.current.right = true;
|
keys.current.right = true;
|
||||||
break;
|
break;
|
||||||
case " ":
|
case JUMP_KEY:
|
||||||
wantsJump.current = true;
|
wantsJump.current = true;
|
||||||
break;
|
break;
|
||||||
case "e":
|
case INTERACT_KEY:
|
||||||
if (interaction.getState().focused?.kind === "trigger") {
|
if (interaction.getState().focused?.kind === "trigger") {
|
||||||
interaction.pressInteract();
|
interaction.pressInteract();
|
||||||
}
|
}
|
||||||
@@ -101,19 +114,19 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
|
|
||||||
const handleKeyUp = (event: KeyboardEvent): void => {
|
const handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
switch (event.key.toLowerCase()) {
|
switch (event.key.toLowerCase()) {
|
||||||
case "z":
|
case MOVE_FORWARD_KEY:
|
||||||
keys.current.forward = false;
|
keys.current.forward = false;
|
||||||
break;
|
break;
|
||||||
case "s":
|
case MOVE_BACKWARD_KEY:
|
||||||
keys.current.backward = false;
|
keys.current.backward = false;
|
||||||
break;
|
break;
|
||||||
case "q":
|
case MOVE_LEFT_KEY:
|
||||||
keys.current.left = false;
|
keys.current.left = false;
|
||||||
break;
|
break;
|
||||||
case "d":
|
case MOVE_RIGHT_KEY:
|
||||||
keys.current.right = false;
|
keys.current.right = false;
|
||||||
break;
|
break;
|
||||||
case "e":
|
case INTERACT_KEY:
|
||||||
if (interaction.getState().focused?.kind === "trigger") {
|
if (interaction.getState().focused?.kind === "trigger") {
|
||||||
interaction.releaseInteract();
|
interaction.releaseInteract();
|
||||||
}
|
}
|
||||||
@@ -125,14 +138,14 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (event: MouseEvent): void => {
|
const handleMouseDown = (event: MouseEvent): void => {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
if (interaction.getState().focused?.kind === "grab") {
|
if (interaction.getState().focused?.kind === "grab") {
|
||||||
interaction.pressInteract();
|
interaction.pressInteract();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = (event: MouseEvent): void => {
|
const handleMouseUp = (event: MouseEvent): void => {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
|
||||||
if (interaction.getState().holding) {
|
if (interaction.getState().holding) {
|
||||||
interaction.releaseInteract();
|
interaction.releaseInteract();
|
||||||
}
|
}
|
||||||
@@ -153,8 +166,7 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
// Clamp delta so physics don't explode on tab focus regain
|
const dt = Math.min(delta, PLAYER_MAX_DELTA);
|
||||||
const dt = Math.min(delta, 0.05);
|
|
||||||
|
|
||||||
// Compute wish direction from camera yaw (XZ only)
|
// Compute wish direction from camera yaw (XZ only)
|
||||||
camera.getWorldDirection(_forward);
|
camera.getWorldDirection(_forward);
|
||||||
@@ -172,12 +184,15 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
|
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
|
||||||
|
|
||||||
// Accelerate horizontally
|
// Accelerate horizontally
|
||||||
const accel = onFloor.current ? WALK_SPEED : WALK_SPEED * AIR_CONTROL;
|
const accel = onFloor.current
|
||||||
velocity.current.x += _wishDir.x * accel * dt * 9;
|
? PLAYER_WALK_SPEED
|
||||||
velocity.current.z += _wishDir.z * accel * dt * 9;
|
: 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(-PLAYER_XZ_DAMPING_FACTOR * dt);
|
||||||
const damping = Math.exp(-8 * dt);
|
|
||||||
velocity.current.x *= damping;
|
velocity.current.x *= damping;
|
||||||
velocity.current.z *= damping;
|
velocity.current.z *= damping;
|
||||||
|
|
||||||
@@ -185,11 +200,11 @@ export function PlayerController({ octree }: PlayerControllerProps): null {
|
|||||||
if (onFloor.current) {
|
if (onFloor.current) {
|
||||||
velocity.current.y = Math.max(0, velocity.current.y);
|
velocity.current.y = Math.max(0, velocity.current.y);
|
||||||
if (wantsJump.current) {
|
if (wantsJump.current) {
|
||||||
velocity.current.y = JUMP_SPEED;
|
velocity.current.y = PLAYER_JUMP_SPEED;
|
||||||
onFloor.current = false;
|
onFloor.current = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
velocity.current.y -= GRAVITY * dt;
|
velocity.current.y -= PLAYER_GRAVITY * dt;
|
||||||
}
|
}
|
||||||
wantsJump.current = false;
|
wantsJump.current = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user