10 Commits

Author SHA1 Message Date
math-pixel 4faa226326 working move kikle 2026-05-19 17:10:34 +02:00
math-pixel dd66966507 working move kikle 2026-05-19 17:04:01 +02:00
math-pixel 5893afe42a working move kikle 2026-05-19 16:34:48 +02:00
math-pixel 1ead7ab3a7 working move kikle 2026-05-19 16:17:02 +02:00
math-pixel 047c58678b working move kikle 2026-05-19 16:12:58 +02:00
math-pixel ed9051b0dc working move kikle 2026-05-19 16:10:57 +02:00
math-pixel 08be6bee48 add good inclinason cam 2026-05-19 15:54:40 +02:00
math-pixel ce0eb90321 inhance move 2026-05-19 15:50:11 +02:00
math-pixel 96d7ec7fc0 move forward cam 2026-05-19 15:36:50 +02:00
math-pixel 9ab4b4a002 first move with bike 2026-05-19 15:32:59 +02:00
3 changed files with 346 additions and 58 deletions
+148 -55
View File
@@ -1,27 +1,29 @@
import { useRef } from "react";
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
import { useClonedObject } from "@/hooks/three/useClonedObject";
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
import { animateCameraTransition } from "@/world/GameCinematics";
import { animateCameraTransformTransition } from "@/world/GameCinematics";
import { useGameStore } from "@/managers/stores/useGameStore";
import { PLAYER_EYE_HEIGHT } from "@/data/player/playerConfig";
import type { Vector3Tuple } from "@/types/three/three";
const EBIKE_MODEL_PATH = "/models/ebike/model.gltf";
interface CameraTransform {
export interface CameraTransform {
position: Vector3Tuple;
rotation: Vector3Tuple;
}
const EBIKE_CAMERA_TRANSFORM: CameraTransform = {
position: [-3, 8, 0],
rotation: [90, 90, 90],
export const EBIKE_CAMERA_TRANSFORM: CameraTransform = {
position: [-3.5, 6, 0],
rotation: [-10, -90, 0],
};
const EBIKE_DROP_PLAYER_TRANSFORM: CameraTransform = {
position: [3, 1.5, 0],
position: [0, 1.5, -3],
rotation: [0, 0, 0],
};
@@ -37,8 +39,140 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
});
const model = useClonedObject(scene);
const movementMode = useGameStore((state) => state.player.movementMode);
const camera = useThree((state) => state.camera);
const restingPosition = useRef<Vector3Tuple>([
position[0],
position[1] - PLAYER_EYE_HEIGHT,
position[2],
]);
const restingRotation = useRef<number>(0);
const forkRef = useRef<THREE.Object3D | null>(null);
useEffect(() => {
if (model) {
const fork = model.getObjectByName("fourche");
if (fork) {
forkRef.current = fork;
}
}
}, [model]);
useEffect(() => {
(window as any).ebikeVisualGroup = groupRef;
(window as any).ebikeParkedPosition = restingPosition.current;
(window as any).ebikeParkedRotation = restingRotation.current;
return () => {
(window as any).ebikeVisualGroup = null;
(window as any).ebikeParkedPosition = null;
(window as any).ebikeParkedRotation = null;
};
}, []);
useFrame((_, delta) => {
if (groupRef.current) {
if (movementMode === "ebike") {
restingPosition.current = [
groupRef.current.position.x,
groupRef.current.position.y,
groupRef.current.position.z,
];
restingRotation.current = groupRef.current.rotation.y;
// Smoothly rotate the front fork ("fourche") up to 15 degrees in its own Z axis
const steerFactor = (window as any).ebikeSteerFactor || 0;
if (forkRef.current) {
// 15 degrees is 0.26 radians
const targetForkRotation = steerFactor * 0.26;
forkRef.current.rotation.z = THREE.MathUtils.lerp(
forkRef.current.rotation.z,
targetForkRotation,
12 * delta
);
}
} else {
groupRef.current.position.set(...restingPosition.current);
groupRef.current.rotation.set(0, restingRotation.current, 0);
// Reset fork rotation when parked
if (forkRef.current) {
forkRef.current.rotation.z = 0;
}
}
(window as any).ebikeParkedPosition = restingPosition.current;
(window as any).ebikeParkedRotation = restingRotation.current;
}
});
const camPointPos: Vector3Tuple = [
restingPosition.current[0] + EBIKE_CAMERA_TRANSFORM.position[0],
restingPosition.current[1] + EBIKE_CAMERA_TRANSFORM.position[1],
restingPosition.current[2] + EBIKE_CAMERA_TRANSFORM.position[2],
];
const dropPointPos: Vector3Tuple = [
restingPosition.current[0] + EBIKE_DROP_PLAYER_TRANSFORM.position[0],
restingPosition.current[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
restingPosition.current[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
];
const handleInteract = (): void => {
if (movementMode === "walk") {
const cameraOffset = new THREE.Vector3(...EBIKE_CAMERA_TRANSFORM.position);
cameraOffset.applyAxisAngle(new THREE.Vector3(0, 1, 0), restingRotation.current);
const targetCamPos: Vector3Tuple = [
restingPosition.current[0] + cameraOffset.x,
restingPosition.current[1] + cameraOffset.y,
restingPosition.current[2] + cameraOffset.z,
];
const targetRotation: Vector3Tuple = [
EBIKE_CAMERA_TRANSFORM.rotation[0],
EBIKE_CAMERA_TRANSFORM.rotation[1] + THREE.MathUtils.radToDeg(restingRotation.current),
EBIKE_CAMERA_TRANSFORM.rotation[2],
];
animateCameraTransformTransition(targetCamPos, targetRotation, 1, () => {
useGameStore.getState().setPlayerMovementMode("ebike");
});
} else {
const currentPos = new THREE.Vector3();
if (groupRef.current) {
groupRef.current.getWorldPosition(currentPos);
} else {
currentPos.set(...position);
}
const targetCamPos: Vector3Tuple = [
currentPos.x + EBIKE_DROP_PLAYER_TRANSFORM.position[0],
currentPos.y + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
currentPos.z + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
];
// Get camera's current rotation in degrees so we keep the exact orientation during dismount
const currentEuler = new THREE.Euler().setFromQuaternion(camera.quaternion, "YXZ");
const targetRotation: Vector3Tuple = [
THREE.MathUtils.radToDeg(currentEuler.x),
THREE.MathUtils.radToDeg(currentEuler.y),
THREE.MathUtils.radToDeg(currentEuler.z),
];
animateCameraTransformTransition(targetCamPos, targetRotation, 1, () => {
useGameStore.getState().setPlayerMovementMode("walk");
});
}
};
const handleInteractRef = useRef(handleInteract);
handleInteractRef.current = handleInteract;
const debugRef = useRef({ showCameraPoints: true });
const debugActions = useRef({
toggleRide: () => {
handleInteractRef.current();
}
});
useDebugFolder("Ebike", (folder) => {
folder
.add(debugRef.current, "showCameraPoints")
@@ -46,53 +180,12 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
.onChange((value: boolean) => {
debugRef.current.showCameraPoints = value;
});
folder
.add(debugActions.current, "toggleRide")
.name("Monter / Descendre");
});
const camPointPos: Vector3Tuple = [
position[0] + EBIKE_CAMERA_TRANSFORM.position[0],
position[1] + EBIKE_CAMERA_TRANSFORM.position[1],
position[2] + EBIKE_CAMERA_TRANSFORM.position[2],
];
const dropPointPos: Vector3Tuple = [
position[0] + EBIKE_DROP_PLAYER_TRANSFORM.position[0],
position[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
position[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
];
const handleInteract = (): void => {
if (movementMode === "walk") {
const targetCamPos: Vector3Tuple = [
position[0] + EBIKE_CAMERA_TRANSFORM.position[0],
position[1] + EBIKE_CAMERA_TRANSFORM.position[1],
position[2] + EBIKE_CAMERA_TRANSFORM.position[2],
];
const targetLookAt: Vector3Tuple = [
position[0],
position[1] + 1,
position[2],
];
animateCameraTransition(targetCamPos, targetLookAt, 1, () => {
useGameStore.getState().setPlayerMovementMode("ebike");
});
} else {
const targetCamPos: Vector3Tuple = [
position[0] + EBIKE_DROP_PLAYER_TRANSFORM.position[0],
position[1] + EBIKE_DROP_PLAYER_TRANSFORM.position[1],
position[2] + EBIKE_DROP_PLAYER_TRANSFORM.position[2],
];
const targetLookAt: Vector3Tuple = [
position[0],
position[1] + 1,
position[2],
];
animateCameraTransition(targetCamPos, targetLookAt, 1, () => {
useGameStore.getState().setPlayerMovementMode("walk");
});
}
};
return (
<>
<group ref={groupRef} position={position}>
@@ -103,12 +196,12 @@ export function Ebike({ position }: EbikeProps): React.JSX.Element {
movementMode === "walk" ? "Monter sur le bike" : "Descendre du bike"
}
position={position}
radius={10}
radius={15}
onPress={handleInteract}
>
<mesh>
<boxGeometry args={[1.5, 1.5, 1.5]} />
<meshStandardMaterial color="red" opacity={0.5} transparent />
<boxGeometry args={[10, 13, 2]} />
<meshBasicMaterial colorWrite={false} depthWrite={false} />
</mesh>
</InteractableObject>
</group>
+59
View File
@@ -233,3 +233,62 @@ export function animateCameraTransition(
0,
);
}
export function animateCameraTransformTransition(
targetPosition: Vector3Tuple,
targetRotation: Vector3Tuple,
duration: number = 1,
onComplete?: () => void,
): void {
if (!globalCamera) {
logger.warn("GameCinematics", "Camera not found for transition");
onComplete?.();
return;
}
const camera = globalCamera;
cameraTransitionTimeline?.kill();
useGameStore.getState().setCinematicPlaying(true);
// Convert target rotation in degrees to quaternion
const targetEuler = new THREE.Euler(
THREE.MathUtils.degToRad(targetRotation[0]),
THREE.MathUtils.degToRad(targetRotation[1]),
THREE.MathUtils.degToRad(targetRotation[2]),
"YXZ"
);
const startQuaternion = camera.quaternion.clone();
const endQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const transitionObj = { progress: 0 };
cameraTransitionTimeline = gsap.timeline({
onUpdate: () => {
camera.quaternion.copy(startQuaternion).slerp(endQuaternion, transitionObj.progress);
},
onComplete: () => {
cameraTransitionTimeline = null;
useGameStore.getState().setCinematicPlaying(false);
onComplete?.();
},
});
cameraTransitionTimeline.to(camera.position, {
x: targetPosition[0],
y: targetPosition[1],
z: targetPosition[2],
duration,
ease: "power2.inOut",
});
cameraTransitionTimeline.to(
transitionObj,
{
progress: 1,
duration,
ease: "power2.inOut",
},
0,
);
}
+137 -1
View File
@@ -27,6 +27,7 @@ import { InteractionManager } from "@/managers/InteractionManager";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import type { Vector3Tuple } from "@/types/three/three";
import { EBIKE_CAMERA_TRANSFORM } from "@/components/ebike/Ebike";
type Keys = {
forward: boolean;
@@ -110,10 +111,70 @@ export function PlayerController({
const currentSpeed = useGameStore((state) => state.player.currentSpeed);
const movementMode = useGameStore((state) => state.player.movementMode);
const movementModeRef = useRef(movementMode);
const prevMovementModeRef = useRef(movementMode);
const ebikeAngle = useRef(0);
useEffect(() => {
movementModeRef.current = movementMode;
}, [movementMode]);
useEffect(() => {
if (movementMode === "ebike") {
// Teleport player capsule to the bike's current parked position
const targetPos: Vector3Tuple = (window as any).ebikeParkedPosition || [0, 8.2, 0];
const targetRot: number = (window as any).ebikeParkedRotation || 0;
const headY = targetPos[1] + PLAYER_EYE_HEIGHT;
const bottomY = targetPos[1] + PLAYER_CAPSULE_RADIUS;
capsule.current.start.set(
targetPos[0],
bottomY,
targetPos[2],
);
capsule.current.end.set(
targetPos[0],
headY,
targetPos[2],
);
velocity.current.set(0, 0, 0);
onFloor.current = false;
wantsJump.current = false;
// Initialize ebikeAngle to the bike's actual parked orientation!
ebikeAngle.current = targetRot;
// Position the camera exactly at the EBIKE_CAMERA_TRANSFORM offset rotated by targetRot
const cameraOffset = new THREE.Vector3(...EBIKE_CAMERA_TRANSFORM.position);
cameraOffset.applyAxisAngle(_up, targetRot);
const camPos = new THREE.Vector3()
.copy(capsule.current.end)
.add(cameraOffset);
camera.position.copy(camPos);
// Set the camera's exact rotation according to EBIKE_CAMERA_TRANSFORM.rotation + targetRot
const pitchRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[0]);
const yawRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[1]) + targetRot;
const rollRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[2]);
camera.rotation.set(pitchRad, yawRad, rollRad, "YXZ");
} else if (movementMode === "walk" && prevMovementModeRef.current === "ebike") {
// Restore default walk FOV
const perspectiveCam = camera as THREE.PerspectiveCamera;
perspectiveCam.fov = 60;
perspectiveCam.updateProjectionMatrix();
// Dismount! Teleport player capsule 3 units to the right
const rightDir = new THREE.Vector3();
camera.getWorldDirection(_forward);
_forward.setY(0).normalize();
rightDir.crossVectors(_forward, _up).normalize();
const shift = rightDir.multiplyScalar(3);
capsule.current.translate(shift);
camera.position.copy(capsule.current.end);
}
prevMovementModeRef.current = movementMode;
}, [movementMode, camera]);
const capsule = useRef(createSpawnCapsule(spawnPosition));
@@ -226,6 +287,17 @@ export function PlayerController({
const dt = Math.min(delta, PLAYER_MAX_DELTA);
// Rotate camera on Y-axis for ebike steering
if (movementModeRef.current === "ebike") {
const turnSpeed = 1.8; // radians per second
if (keys.current.left) {
ebikeAngle.current += turnSpeed * dt;
}
if (keys.current.right) {
ebikeAngle.current -= turnSpeed * dt;
}
}
camera.getWorldDirection(_forward);
_forward.setY(0);
if (_forward.lengthSq() > 0) {
@@ -237,9 +309,11 @@ export function PlayerController({
if (!movementLocked) {
if (keys.current.forward) _wishDir.add(_forward);
if (keys.current.backward) _wishDir.sub(_forward);
if (movementModeRef.current !== "ebike") {
if (keys.current.left) _wishDir.sub(_right);
if (keys.current.right) _wishDir.add(_right);
}
}
if (_wishDir.lengthSq() > 0) _wishDir.normalize();
const accel = onFloor.current
@@ -288,9 +362,71 @@ export function PlayerController({
}
}
if (movementModeRef.current !== "ebike") {
if (movementModeRef.current === "ebike") {
// Calculate dynamic steering factor
let targetSteer = 0;
if (keys.current.left) targetSteer = 1;
else if (keys.current.right) targetSteer = -1;
const currentSteer = (window as any).ebikeSteerFactor || 0;
const steerFactor = THREE.MathUtils.lerp(currentSteer, targetSteer, 8 * dt);
(window as any).ebikeSteerFactor = steerFactor;
// 1. Dynamic FOV stretch based on speed!
const speed = velocity.current.length();
const targetFov = 60 + Math.min(speed * 0.35, 9); // stretch FOV up to 9 degrees at high speed (halved by two)!
const perspectiveCam = camera as THREE.PerspectiveCamera;
perspectiveCam.fov = THREE.MathUtils.lerp(perspectiveCam.fov, targetFov, 6 * dt);
perspectiveCam.updateProjectionMatrix();
// 2. Camera lag & dynamic swing trailing
const cameraOffset = new THREE.Vector3(...EBIKE_CAMERA_TRANSFORM.position);
cameraOffset.applyAxisAngle(_up, ebikeAngle.current);
// Swing camera to optimize the view for both left and right turns:
// Since the camera is on the left (X = -3.5), it naturally trails beautifully in right turns,
// but cuts forward in left turns. We compensate by pushing the camera backward (+Z) during left turns!
const swingX = -Math.abs(steerFactor) * 1.5;
const swingZ = steerFactor > 0 ? steerFactor * 2.5 : steerFactor * 1.0;
const cameraSwing = new THREE.Vector3(swingX, 0, swingZ);
cameraSwing.applyAxisAngle(_up, ebikeAngle.current);
cameraOffset.add(cameraSwing);
const targetCamPos = new THREE.Vector3()
.copy(capsule.current.end)
.add(cameraOffset);
// Smoothly lerp camera position to eliminate rigidity
camera.position.lerp(targetCamPos, 12 * dt);
// 3. Dynamic camera roll based on steering!
const pitchRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[0]);
const yawRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[1]) + ebikeAngle.current;
// COMMENTED OUT: Camera roll/tilt during turns (keeping it flat)
// const rollRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[2]) - steerFactor * 0.08;
const rollRad = THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[2]);
camera.rotation.set(pitchRad, yawRad, rollRad, "YXZ");
// 4. Synchronize visual e-bike position and apply leaning!
const ebikeVisual = (window as any).ebikeVisualGroup?.current;
if (ebikeVisual) {
ebikeVisual.position.set(
capsule.current.end.x,
capsule.current.end.y - PLAYER_EYE_HEIGHT,
capsule.current.end.z
);
// Lean (roll) the bike sideways in turns (up to 15 degrees)
const leanAngle = steerFactor * 0.26; // rotate in direction of turn!
ebikeVisual.rotation.set(0, ebikeAngle.current, leanAngle, "YXZ");
}
} else {
camera.position.copy(capsule.current.end);
}
// Save player capsule end position and camera yaw globally so other components (like Ebike) can access it
(window as any).playerPos = [capsule.current.end.x, capsule.current.end.y, capsule.current.end.z];
(window as any).ebikeAngle = ebikeAngle.current;
});
return null;