merge develop into feat/map-environment
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
CinematicManifest,
|
||||
} from "@/types/cinematics/cinematics";
|
||||
import type { DialogueManifest } from "@/types/dialogues/dialogues";
|
||||
import type { Vector3Tuple } from "@/types/three/three";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
|
||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
@@ -16,6 +17,11 @@ import { queueDialogueById } from "@/utils/dialogues/playDialogue";
|
||||
|
||||
export function GameCinematics(): null {
|
||||
const camera = useThree((state) => state.camera);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalCamera(camera);
|
||||
}, [camera]);
|
||||
|
||||
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
|
||||
const [dialogueManifest, setDialogueManifest] =
|
||||
useState<DialogueManifest | null>(null);
|
||||
@@ -171,3 +177,120 @@ function playCinematic(
|
||||
|
||||
timelineRef.current = timeline;
|
||||
}
|
||||
|
||||
let cameraTransitionTimeline: gsap.core.Timeline | null = null;
|
||||
let globalCamera: THREE.Camera | null = null;
|
||||
|
||||
export function setGlobalCamera(camera: THREE.Camera | null): void {
|
||||
globalCamera = camera;
|
||||
}
|
||||
|
||||
export function animateCameraTransition(
|
||||
targetPosition: Vector3Tuple,
|
||||
targetLookAt: 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);
|
||||
|
||||
const target = new THREE.Vector3(...targetLookAt);
|
||||
|
||||
cameraTransitionTimeline = gsap.timeline({
|
||||
onUpdate: () => camera.lookAt(target),
|
||||
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(
|
||||
target,
|
||||
{
|
||||
x: targetLookAt[0],
|
||||
y: targetLookAt[1],
|
||||
z: targetLookAt[2],
|
||||
duration,
|
||||
ease: "power2.inOut",
|
||||
},
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Ebike } from "@/components/ebike/Ebike";
|
||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
||||
import {
|
||||
@@ -80,6 +81,7 @@ export function GameStageContent(): React.JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{mainState === "intro" ? <StageAnchor {...INTRO_STAGE_ANCHOR} /> : null}
|
||||
<Ebike position={[0, 10, 0]} />
|
||||
{REPAIR_MISSION_POSITION_ENTRIES.map(({ mission }) => {
|
||||
const position = getRepairMissionPosition(mission, anchors);
|
||||
if (!position) return null;
|
||||
|
||||
+130
-3
@@ -1,11 +1,13 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Component, useRef } from "react";
|
||||
import { Component, useRef, useState, useEffect } from "react";
|
||||
import * as THREE from "three";
|
||||
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
|
||||
import { Line } from "@react-three/drei";
|
||||
import { RepairGame } from "@/components/three/gameplay/RepairGame";
|
||||
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
||||
import { AnimatedModel } from "@/components/three/models/AnimatedModel";
|
||||
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||
import { EbikeGPSMap } from "@/components/ebike/EbikeGPSMap";
|
||||
import {
|
||||
TEST_SCENE_FLOOR_COLLIDER_HALF_EXTENTS,
|
||||
TEST_SCENE_FLOOR_POSITION,
|
||||
@@ -15,9 +17,9 @@ import {
|
||||
TEST_SCENE_GRABBABLE_METALNESS,
|
||||
TEST_SCENE_GRABBABLE_POSITION,
|
||||
TEST_SCENE_GRABBABLE_ROUGHNESS,
|
||||
GAME_REPAIR_ZONES,
|
||||
TEST_SCENE_REPAIR_ZONE_MARKER_RADIUS,
|
||||
TEST_SCENE_REPAIR_ZONE_MARKER_TUBE_RADIUS,
|
||||
TEST_SCENE_REPAIR_ZONES,
|
||||
TEST_SCENE_TRIGGER_COLOR,
|
||||
TEST_SCENE_TRIGGER_METALNESS,
|
||||
TEST_SCENE_TRIGGER_POSITION,
|
||||
@@ -84,11 +86,61 @@ class ModelPreviewErrorBoundary extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
interface Waypoint {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
connections: number[];
|
||||
}
|
||||
|
||||
export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
||||
const floorRef = useRef<THREE.Group>(null);
|
||||
const [waypoints, setWaypoints] = useState<Waypoint[]>([]);
|
||||
|
||||
useOctreeGraphNode(floorRef, onOctreeReady);
|
||||
|
||||
// Load waypoints with double-safe fallback
|
||||
useEffect(() => {
|
||||
// 1. Try localStorage
|
||||
const saved = localStorage.getItem("la-fabrik-waypoints");
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
console.log(
|
||||
`[TestMap] ${parsed.length} waypoints chargés depuis localStorage.`,
|
||||
);
|
||||
setWaypoints(parsed);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse local storage waypoints", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
throw new Error("Impossible de charger /roadNetwork.json");
|
||||
})
|
||||
.then((data) => {
|
||||
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);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<group ref={floorRef}>
|
||||
@@ -98,6 +150,45 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
{/* Render Pathfinder Maps Waypoints & Routes visually */}
|
||||
<group name="pathfinder-maps-visuals">
|
||||
{/* Render Connection Lines */}
|
||||
{waypoints.flatMap((wp) =>
|
||||
wp.connections.map((connId) => {
|
||||
const other = waypoints.find((w) => w.id === connId);
|
||||
// Draw each line only once by enforcing wp.id < other.id
|
||||
if (other && wp.id < other.id) {
|
||||
return (
|
||||
<Line
|
||||
key={`route-${wp.id}-${other.id}`}
|
||||
points={[
|
||||
[wp.x, wp.y + 0.3, wp.z],
|
||||
[other.x, other.y + 0.3, other.z],
|
||||
]}
|
||||
color="#10b981" // Beautiful emerald green
|
||||
lineWidth={2.5}
|
||||
transparent
|
||||
opacity={0.8}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
)}
|
||||
|
||||
{/* Render Waypoint Spheres */}
|
||||
{waypoints.map((wp) => (
|
||||
<mesh key={`wp-sphere-${wp.id}`} position={[wp.x, wp.y + 0.3, wp.z]}>
|
||||
<sphereGeometry args={[0.35, 16, 16]} />
|
||||
<meshBasicMaterial
|
||||
color="#059669" // Deep emerald green
|
||||
transparent
|
||||
opacity={0.8}
|
||||
/>
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
|
||||
<Physics>
|
||||
<RigidBody type="fixed">
|
||||
<CuboidCollider
|
||||
@@ -141,7 +232,7 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
||||
</mesh>
|
||||
</TriggerObject>
|
||||
|
||||
{TEST_SCENE_REPAIR_ZONES.map((zone) => (
|
||||
{GAME_REPAIR_ZONES.map((zone) => (
|
||||
<group key={zone.mission}>
|
||||
<group position={zone.position}>
|
||||
<RepairPlaygroundZoneMarker color={zone.color} />
|
||||
@@ -151,6 +242,42 @@ 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]}>
|
||||
{/* Futuristic glowing screen frame (commented out to show true 3D transparency!) */}
|
||||
{/*
|
||||
<mesh>
|
||||
<boxGeometry args={[4.2, 4.2, 0.1]} />
|
||||
<meshStandardMaterial color="#0f172a" roughness={0.2} metalness={0.8} transparent opacity={0.4} />
|
||||
</mesh>
|
||||
*/}
|
||||
{/* Glow accent border (commented out to remove any orange transparency tint!) */}
|
||||
{/*
|
||||
<mesh position={[0, 0, 0.01]}>
|
||||
<boxGeometry args={[4.05, 4.05, 0.02]} />
|
||||
<meshBasicMaterial color="#f97316" transparent opacity={0.1} />
|
||||
</mesh>
|
||||
*/}
|
||||
{/* GPS Map screen plane */}
|
||||
<group position={[0, 0, 0.06]}>
|
||||
<EbikeGPSMap
|
||||
width={4}
|
||||
height={4}
|
||||
startPos={{ x: 10, y: 0, z: -10 }}
|
||||
destPos={{ x: -40, y: 0, z: 30 }}
|
||||
mapImageUrl="/assets/gps/map_background.png"
|
||||
worldBounds={{
|
||||
minX: -166,
|
||||
maxX: 163,
|
||||
minZ: -142,
|
||||
maxZ: 138,
|
||||
}}
|
||||
zoom={1}
|
||||
canvasSize={900}
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<ModelPreviewErrorBoundary modelPath={ELECTRICIENNE_ANIMATED_MODEL_PATH}>
|
||||
<AnimatedModel
|
||||
modelPath={ELECTRICIENNE_ANIMATED_MODEL_PATH}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { PointerLockControls } from "@react-three/drei";
|
||||
import { setGlobalCamera } from "@/world/GameCinematics";
|
||||
|
||||
export function PlayerCamera(): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalCamera(camera);
|
||||
return () => {
|
||||
setGlobalCamera(null);
|
||||
document.exitPointerLock();
|
||||
};
|
||||
}, []);
|
||||
}, [camera]);
|
||||
|
||||
return <PointerLockControls />;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
PLAYER_GRAVITY,
|
||||
PLAYER_JUMP_SPEED,
|
||||
PLAYER_MAX_DELTA,
|
||||
PLAYER_WALK_SPEED,
|
||||
PLAYER_XZ_DAMPING_FACTOR,
|
||||
} from "@/data/player/playerConfig";
|
||||
import { useRepairMovementLocked } from "@/hooks/gameplay/useRepairMovementLocked";
|
||||
@@ -30,6 +29,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;
|
||||
@@ -136,9 +136,74 @@ export function PlayerController({
|
||||
const wantsJump = useRef(false);
|
||||
const initializedRef = useRef(false);
|
||||
const canMove = useGameStore((state) => state.missionFlow.canMove);
|
||||
|
||||
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);
|
||||
const capsule = useRef(createSpawnCapsule(spawnPosition));
|
||||
|
||||
useEffect(() => {
|
||||
movementModeRef.current = movementMode;
|
||||
}, [movementMode]);
|
||||
useEffect(() => {
|
||||
if (movementMode === "ebike") {
|
||||
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;
|
||||
|
||||
ebikeAngle.current = 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);
|
||||
|
||||
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"
|
||||
) {
|
||||
const perspectiveCam = camera as THREE.PerspectiveCamera;
|
||||
perspectiveCam.fov = 60;
|
||||
perspectiveCam.updateProjectionMatrix();
|
||||
|
||||
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]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
resetPlayerCapsule(
|
||||
capsule.current,
|
||||
@@ -266,6 +331,16 @@ export function PlayerController({
|
||||
return;
|
||||
}
|
||||
|
||||
if (movementModeRef.current === "ebike") {
|
||||
const turnSpeed = 1.8;
|
||||
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) {
|
||||
@@ -277,14 +352,16 @@ export function PlayerController({
|
||||
if (!movementLocked) {
|
||||
if (keys.current.forward) _wishDir.add(_forward);
|
||||
if (keys.current.backward) _wishDir.sub(_forward);
|
||||
if (keys.current.left) _wishDir.sub(_right);
|
||||
if (keys.current.right) _wishDir.add(_right);
|
||||
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
|
||||
? PLAYER_WALK_SPEED
|
||||
: PLAYER_WALK_SPEED * PLAYER_AIR_CONTROL_FACTOR;
|
||||
? currentSpeed
|
||||
: currentSpeed * PLAYER_AIR_CONTROL_FACTOR;
|
||||
velocity.current.x +=
|
||||
_wishDir.x * accel * dt * PLAYER_ACCELERATION_MULTIPLIER;
|
||||
velocity.current.z +=
|
||||
@@ -353,7 +430,78 @@ export function PlayerController({
|
||||
}
|
||||
}
|
||||
|
||||
camera.position.copy(capsule.current.end);
|
||||
if (movementModeRef.current === "ebike") {
|
||||
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;
|
||||
|
||||
const speed = velocity.current.length();
|
||||
const targetFov = 60 + Math.min(speed * 0.35, 9);
|
||||
const perspectiveCam = camera as THREE.PerspectiveCamera;
|
||||
perspectiveCam.fov = THREE.MathUtils.lerp(
|
||||
perspectiveCam.fov,
|
||||
targetFov,
|
||||
6 * dt,
|
||||
);
|
||||
perspectiveCam.updateProjectionMatrix();
|
||||
|
||||
const cameraOffset = new THREE.Vector3(
|
||||
...EBIKE_CAMERA_TRANSFORM.position,
|
||||
);
|
||||
cameraOffset.applyAxisAngle(_up, ebikeAngle.current);
|
||||
|
||||
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);
|
||||
|
||||
camera.position.lerp(targetCamPos, 12 * dt);
|
||||
|
||||
const pitchRad = THREE.MathUtils.degToRad(
|
||||
EBIKE_CAMERA_TRANSFORM.rotation[0],
|
||||
);
|
||||
const yawRad =
|
||||
THREE.MathUtils.degToRad(EBIKE_CAMERA_TRANSFORM.rotation[1]) +
|
||||
ebikeAngle.current;
|
||||
const rollRad = THREE.MathUtils.degToRad(
|
||||
EBIKE_CAMERA_TRANSFORM.rotation[2],
|
||||
);
|
||||
camera.rotation.set(pitchRad, yawRad, rollRad, "YXZ");
|
||||
|
||||
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,
|
||||
);
|
||||
const leanAngle = steerFactor * 0.26;
|
||||
ebikeVisual.rotation.set(0, ebikeAngle.current, leanAngle, "YXZ");
|
||||
}
|
||||
} else {
|
||||
camera.position.copy(capsule.current.end);
|
||||
}
|
||||
|
||||
(window as any).playerPos = [
|
||||
capsule.current.end.x,
|
||||
capsule.current.end.y,
|
||||
capsule.current.end.z,
|
||||
];
|
||||
(window as any).ebikeAngle = ebikeAngle.current;
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user