update: add a physic scenne

This commit is contained in:
Tom Boullay
2026-04-17 10:48:18 +02:00
parent 1d4f223c35
commit 5111f2e558
22 changed files with 2050 additions and 216 deletions
+133
View File
@@ -0,0 +1,133 @@
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 { Debug } from "@/utils/debug/Debug";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import {
InteractionManager,
type InteractableHandle,
type InteractableKind,
} from "@/stateManager/InteractionManager";
import { INTERACTION_RADIUS } from "@/data/interactionConfig";
interface InteractableObjectProps {
kind: InteractableKind;
label: string;
position: [number, number, number];
rigidBodyType?: "dynamic" | "fixed";
colliders?: "cuboid" | "ball" | "hull";
rbRef?: RefObject<RapierRigidBody | null>;
onPress: () => void;
onRelease?: () => void;
children: React.ReactNode;
}
const _cameraPos = new THREE.Vector3();
const _cameraDir = new THREE.Vector3();
const _objectPos = new THREE.Vector3();
const _raycaster = new THREE.Raycaster();
export function InteractableObject({
kind,
label,
position,
rigidBodyType = "dynamic",
colliders = "cuboid",
rbRef,
onPress,
onRelease = () => {},
children,
}: InteractableObjectProps): React.JSX.Element {
const camera = useThree((state) => state.camera);
const internalRef = useRef<RapierRigidBody>(null);
const bodyRef = rbRef ?? internalRef;
const groupRef = useRef<THREE.Group>(null);
const debugSphereRef = useRef<THREE.Mesh>(null);
const handle = useRef<InteractableHandle>({
kind,
label,
onPress,
onRelease,
});
useEffect(() => {
handle.current.onPress = onPress;
handle.current.onRelease = onRelease;
});
useDebugFolder("Interaction", (folder) => {
folder
.add({ radius: INTERACTION_RADIUS }, "radius")
.name("Interaction radius")
.disable();
});
useFrame(() => {
const body = bodyRef.current;
const group = groupRef.current;
const debug = Debug.getInstance();
const manager = InteractionManager.getInstance();
if (debugSphereRef.current) {
debugSphereRef.current.visible =
debug.active && debug.getShowInteractionSpheres();
}
if (body) {
const t = body.translation();
_objectPos.set(t.x, t.y, t.z);
} else {
_objectPos.set(...position);
}
camera.getWorldPosition(_cameraPos);
const dist = _cameraPos.distanceTo(_objectPos);
if (dist > INTERACTION_RADIUS) {
if (manager.getState().focused === handle.current) {
manager.setFocused(null);
}
return;
}
camera.getWorldDirection(_cameraDir);
_raycaster.set(_cameraPos, _cameraDir);
_raycaster.far = INTERACTION_RADIUS;
const hits = group ? _raycaster.intersectObject(group, true) : [];
const validHit = hits.find((h) => h.object !== debugSphereRef.current);
if (validHit) {
manager.setFocused(handle.current);
} else if (manager.getState().focused === handle.current) {
manager.setFocused(null);
}
});
return (
<RigidBody
ref={bodyRef}
type={rigidBodyType}
colliders={colliders}
position={position}
>
<group ref={groupRef}>
{children}
<mesh ref={debugSphereRef} visible={false}>
<sphereGeometry args={[INTERACTION_RADIUS, 16, 16]} />
<meshBasicMaterial
color="#facc15"
wireframe
transparent
opacity={0.25}
/>
</mesh>
</group>
</RigidBody>
);
}
+9 -4
View File
@@ -1,11 +1,16 @@
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useInteraction } from "@/hooks/useInteraction";
export function Crosshair(): React.JSX.Element | null {
const cameraMode = useCameraMode();
const { focused } = useInteraction();
if (cameraMode !== "player") {
return null;
}
if (cameraMode !== "player") return null;
return <div className="crosshair" aria-hidden="true" />;
return (
<div
className={focused ? "crosshair crosshair--interact" : "crosshair"}
aria-hidden="true"
/>
);
}
+17
View File
@@ -0,0 +1,17 @@
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useInteraction } from "@/hooks/useInteraction";
export function InteractPrompt(): React.JSX.Element | null {
const cameraMode = useCameraMode();
const { focused, holding } = useInteraction();
if (cameraMode !== "player") return null;
if (!focused || holding || focused.kind !== "trigger") return null;
return (
<div className="interact-prompt" aria-live="polite">
<kbd className="interact-prompt__key">E</kbd>
<span className="interact-prompt__label">{focused.label}</span>
</div>
);
}