Merge branch 'develop' into feat/e-bike

This commit is contained in:
math-pixel
2026-06-02 19:23:01 +02:00
209 changed files with 809 additions and 642 deletions
+21 -5
View File
@@ -31,11 +31,20 @@ import { CharacterSystem } from "@/world/characters/CharacterSystem";
import { Player } from "@/world/player/Player";
import { TestMap } from "@/world/debug/TestMap";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import type { HandTrackingGloveHandedness } from "@/hooks/handTracking/useHandTrackingGloveStatus";
import type { HandTrackingHand } from "@/types/handTracking/handTracking";
interface WorldProps {
onLoadingStateChange?: SceneLoadingChangeHandler | undefined;
}
function hasTrackedHand(
hands: HandTrackingHand[],
handedness: HandTrackingGloveHandedness,
): boolean {
return hands.some((hand) => hand.handedness.toLowerCase() === handedness);
}
export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
useEnvironmentDebug();
useMapPerformanceDebug();
@@ -49,7 +58,7 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
(state) => state.showPlayerModel,
);
const showDebugOctree = useDebugVisualsStore((state) => state.showOctree);
const { status, usageStatus } = useHandTrackingSnapshot();
const { hands, status, usageStatus } = useHandTrackingSnapshot();
const {
octree,
gameplayReady,
@@ -63,8 +72,11 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
? PLAYER_SPAWN_POSITION_GAME
: PLAYER_SPAWN_POSITION_PHYSICS;
const showHandTrackingGloves =
sceneMode === "physics" ||
(status !== "idle" && usageStatus !== "inactive");
status === "connected" && usageStatus !== "inactive" && hands.length > 0;
const showLeftHandTrackingGlove =
showHandTrackingGloves && hasTrackedHand(hands, "left");
const showRightHandTrackingGlove =
showHandTrackingGloves && hasTrackedHand(hands, "right");
const spawnPlayer =
cameraMode !== "debug" &&
(sceneMode === "game" ? gameplayReady : octree !== null);
@@ -82,8 +94,12 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
) : null}
{showHandTrackingGloves ? (
<Suspense fallback={null}>
<HandTrackingGlove handedness="left" />
<HandTrackingGlove handedness="right" />
{showLeftHandTrackingGlove ? (
<HandTrackingGlove handedness="left" />
) : null}
{showRightHandTrackingGlove ? (
<HandTrackingGlove handedness="right" />
) : null}
</Suspense>
) : null}
{cameraMode === "debug" ? <DebugCameraControls /> : null}
+10 -15
View File
@@ -17,6 +17,8 @@ import {
TEST_SCENE_GRABBABLE_METALNESS,
TEST_SCENE_GRABBABLE_POSITION,
TEST_SCENE_GRABBABLE_ROUGHNESS,
TEST_SCENE_GPS_PREVIEW_POSITION,
TEST_SCENE_GPS_PREVIEW_ROTATION,
GAME_REPAIR_ZONES,
TEST_SCENE_REPAIR_ZONE_MARKER_RADIUS,
TEST_SCENE_REPAIR_ZONE_MARKER_TUBE_RADIUS,
@@ -110,24 +112,17 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
try {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length > 0) {
console.log(
`[TestMap] ${parsed.length} waypoints chargés depuis localStorage.`,
);
// Schedule state update to avoid synchronous setState in effect
queueMicrotask(() => {
if (!cancelled) setWaypoints(parsed);
});
return;
}
} catch (e) {
console.error("Failed to parse local storage waypoints", e);
} catch {
// Ignore parse errors — fall through to fetch fallback
}
}
// 2. Try public/roadNetwork.json
console.log(
"[TestMap] Tentative de chargement depuis /roadNetwork.json...",
);
fetch("/roadNetwork.json")
.then((res) => {
if (res.ok) return res.json();
@@ -136,14 +131,11 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
.then((data) => {
if (cancelled) return;
if (Array.isArray(data)) {
console.log(
`[TestMap] ${data.length} waypoints chargés depuis /roadNetwork.json.`,
);
setWaypoints(data);
}
})
.catch((err) => {
console.log("[TestMap] Aucun point d'A* trouvé par défaut.", err);
.catch(() => {
// No A* waypoints available — silent fallback
});
return () => {
@@ -253,7 +245,10 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
</Physics>
{/* Dynamic Futuristic 3D GPS Dashboard Preview */}
<group position={[0, 2.8, -4.8]} rotation={[0, 0, 0]}>
<group
position={TEST_SCENE_GPS_PREVIEW_POSITION}
rotation={TEST_SCENE_GPS_PREVIEW_ROTATION}
>
{/* Futuristic glowing screen frame (commented out to show true 3D transparency!) */}
{/*
<mesh>
@@ -149,6 +149,7 @@ export function MapInstancingSystem({
const streamingEnabled =
streaming &&
CHUNK_CONFIG.enabled &&
graphicsPresetConfig.chunkStreamingEnabled &&
sceneMode === "game" &&
cameraMode === "player";
+13 -2
View File
@@ -1,18 +1,29 @@
import { useEffect } from "react";
import { useThree } from "@react-three/fiber";
import { PointerLockControls } from "@react-three/drei";
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import { setGlobalCamera } from "@/world/GameCinematics";
export function PlayerCamera(): React.JSX.Element {
const camera = useThree((state) => state.camera);
const isSettingsMenuOpen = useSettingsStore(
(state) => state.isSettingsMenuOpen,
);
useEffect(() => {
setGlobalCamera(camera);
return () => {
setGlobalCamera(null);
document.exitPointerLock();
if (document.pointerLockElement) {
document.exitPointerLock();
}
};
}, [camera]);
return <PointerLockControls />;
return (
<PointerLockControls
enabled={!isSettingsMenuOpen}
selector="#game-canvas"
/>
);
}
+16 -12
View File
@@ -34,6 +34,7 @@ import {
EBIKE_CAMERA_TRANSFORM,
EBIKE_DECELERATION_DURATION_MS,
} from "@/data/ebike/ebikeConfig";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
/** Global window properties used for ebike communication */
interface EbikeGlobalState {
@@ -152,6 +153,7 @@ export function PlayerController({
spawnPosition,
}: PlayerControllerProps): null {
const camera = useThree((state) => state.camera);
const sceneMode = useSceneMode();
const movementLocked = useRepairMovementLocked();
const terrainHeight = useTerrainHeightSampler();
const movementLockedRef = useRef(movementLocked);
@@ -483,19 +485,21 @@ export function PlayerController({
}
}
const groundHeight = terrainHeight.getHeight(
capsule.current.end.x,
capsule.current.end.z,
);
if (groundHeight !== null && velocity.current.y <= 0) {
const groundOffset = getCapsuleFootY(capsule.current) - groundHeight;
if (sceneMode === "game") {
const groundHeight = terrainHeight.getHeight(
capsule.current.end.x,
capsule.current.end.z,
);
if (groundHeight !== null && velocity.current.y <= 0) {
const groundOffset = getCapsuleFootY(capsule.current) - groundHeight;
if (groundOffset <= PLAYER_GROUND_SNAP_DISTANCE) {
capsule.current.translate(
_collisionCorrection.set(0, -groundOffset, 0),
);
velocity.current.y = 0;
onFloor.current = true;
if (groundOffset <= PLAYER_GROUND_SNAP_DISTANCE) {
capsule.current.translate(
_collisionCorrection.set(0, -groundOffset, 0),
);
velocity.current.y = 0;
onFloor.current = true;
}
}
}
+112 -15
View File
@@ -1,8 +1,24 @@
import { Suspense, useMemo } from "react";
import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import {
getMapLodModelPath,
getMapLodScaleMultiplier,
selectMapModelPathByDistance,
} from "@/data/world/mapLodConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useGraphicsPresetConfig } from "@/hooks/world/useGraphicsSettings";
import {
useGraphicsPreset,
useGraphicsPresetConfig,
} from "@/hooks/world/useGraphicsSettings";
import { useVisibleWorldChunks } from "@/hooks/world/useVisibleWorldChunks";
import {
isMapModelVisible,
@@ -18,6 +34,7 @@ import {
} from "@/data/world/vegetationConfig";
import { isInsideLaFabrikFootprint } from "@/data/world/laFabrikConfig";
import { createWorldInstanceChunks } from "@/utils/world/chunkInstances";
import type { GraphicsPreset } from "@/data/world/graphicsConfig";
interface VegetationSystemProps {
onlyMapName?: string | null;
@@ -70,12 +87,78 @@ function removeLaFabrikVegetation(
});
}
function areChunkModelPathsEqual(
a: ReadonlyMap<string, string>,
b: ReadonlyMap<string, string>,
): boolean {
return (
a.size === b.size && [...a].every(([key, value]) => b.get(key) === value)
);
}
function useVegetationChunkModelPaths(
chunks: readonly VegetationChunk[],
preset: GraphicsPreset,
): ReadonlyMap<string, string> {
const camera = useThree((state) => state.camera);
const lastUpdateRef = useRef(-CHUNK_CONFIG.updateInterval);
const modelPathsRef = useRef<Map<string, string>>(new Map());
const [modelPaths, setModelPaths] = useState<ReadonlyMap<string, string>>(
() => new Map(),
);
const updateModelPaths = useCallback(() => {
const cameraX = camera.position.x;
const cameraZ = camera.position.z;
const next = new Map<string, string>();
for (const chunk of chunks) {
let nearestDistance = Number.POSITIVE_INFINITY;
for (const instance of chunk.instances) {
const distance = Math.hypot(
instance.position[0] - cameraX,
instance.position[2] - cameraZ,
);
if (distance < nearestDistance) nearestDistance = distance;
}
const modelPath = selectMapModelPathByDistance({
distance: nearestDistance,
modelName: VEGETATION_TYPES[chunk.type].mapName,
modelPath: chunk.modelPath,
preset,
});
next.set(chunk.key, modelPath);
}
if (areChunkModelPathsEqual(next, modelPathsRef.current)) return;
modelPathsRef.current = next;
setModelPaths(next);
}, [camera, chunks, preset]);
useEffect(() => {
updateModelPaths();
}, [updateModelPaths]);
useFrame(({ clock }) => {
const now = clock.elapsedTime * 1000;
if (now - lastUpdateRef.current < CHUNK_CONFIG.updateInterval) return;
lastUpdateRef.current = now;
updateModelPaths();
});
return modelPaths;
}
export function VegetationSystem({
onlyMapName = null,
streaming = true,
}: VegetationSystemProps): React.JSX.Element | null {
const cameraMode = useCameraMode();
const sceneMode = useSceneMode();
const graphicsPresetKey = useGraphicsPreset();
const graphicsPreset = useGraphicsPresetConfig();
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
@@ -83,6 +166,7 @@ export function VegetationSystem({
const streamingEnabled =
streaming &&
CHUNK_CONFIG.enabled &&
graphicsPreset.chunkStreamingEnabled &&
sceneMode === "game" &&
cameraMode === "player";
@@ -112,25 +196,38 @@ export function VegetationSystem({
unloadRadius: graphicsPreset.chunkUnloadRadius,
});
const chunkModelPaths = useVegetationChunkModelPaths(
visibleChunks,
graphicsPresetKey,
);
if (isLoading || !data) {
return null;
}
return (
<group name="vegetation-system">
{visibleChunks.map((chunk) => (
<Suspense key={chunk.key} fallback={null}>
<InstancedVegetation
modelPath={chunk.modelPath}
instances={chunk.instances}
scaleMultiplier={chunk.scaleMultiplier}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
windStrength={chunk.windStrength}
rotationOffset={chunk.rotationOffset}
/>
</Suspense>
))}
{visibleChunks.map((chunk) => {
const modelPath = chunkModelPaths.get(chunk.key) ?? chunk.modelPath;
const mapName = VEGETATION_TYPES[chunk.type].mapName;
const isLod = modelPath === getMapLodModelPath(mapName);
const scaleMultiplier =
chunk.scaleMultiplier *
(isLod ? getMapLodScaleMultiplier(mapName) : 1);
return (
<Suspense key={`${chunk.key}:${modelPath}`} fallback={null}>
<InstancedVegetation
modelPath={modelPath}
instances={chunk.instances}
scaleMultiplier={scaleMultiplier}
castShadow={chunk.castShadow}
receiveShadow={chunk.receiveShadow}
windStrength={chunk.windStrength}
rotationOffset={chunk.rotationOffset}
/>
</Suspense>
);
})}
</group>
);
}