// movementManager.js import * as THREE from 'three'; export class MovementManager { constructor(vrm, animationMgr, scene) { this.vrm = vrm; this.animationMgr = animationMgr; this.scene = scene; // Movement state this.isMoving = false; this.targetPosition = null; this.moveSpeed = 1.5; // units per second this.rotationSpeed = 5; // radians per second this.stopDistance = 0.1; // how close to get before stopping // Animation states this.idleAction = null; this.walkAction = null; this.runAction = null; this.currentAction = null; // Movement mixer (separate from VRMA mixer) this.movementMixer = new THREE.AnimationMixer(vrm.scene); // Debug helper (optional) this.targetMarker = null; this.showDebugMarker = false; } // Load locomotion animations (idle, walk, run) async loadLocomotionAnimations(idleUrl, walkUrl, runUrl = null) { const loader = new THREE.GLTFLoader(); try { // Load idle animation if (idleUrl) { const idleGltf = await loader.loadAsync(idleUrl); const idleClip = idleGltf.animations[0]; this.idleAction = this.movementMixer.clipAction(idleClip); this.idleAction.setLoop(THREE.LoopRepeat); } // Load walk animation if (walkUrl) { const walkGltf = await loader.loadAsync(walkUrl); const walkClip = walkGltf.animations[0]; this.walkAction = this.movementMixer.clipAction(walkClip); this.walkAction.setLoop(THREE.LoopRepeat); } // Load run animation (optional) if (runUrl) { const runGltf = await loader.loadAsync(runUrl); const runClip = runGltf.animations[0]; this.runAction = this.movementMixer.clipAction(runClip); this.runAction.setLoop(THREE.LoopRepeat); } // Start with idle if (this.idleAction) { this.currentAction = this.idleAction; this.idleAction.play(); } console.log("✅ Locomotion animations loaded"); } catch (error) { console.error("Failed to load locomotion animations:", error); } } // Transition between animations with crossfade crossFadeTo(targetAction, duration = 0.3) { if (!targetAction || targetAction === this.currentAction) return; targetAction.reset(); targetAction.setEffectiveTimeScale(1); targetAction.setEffectiveWeight(1); targetAction.play(); if (this.currentAction) { this.currentAction.crossFadeTo(targetAction, duration, false); } this.currentAction = targetAction; } // Move to a specific position moveTo(x, y, z, speed = null) { this.targetPosition = new THREE.Vector3(x, y, z); this.isMoving = true; if (speed) { this.moveSpeed = speed; } // Switch to walk animation if (this.walkAction) { this.crossFadeTo(this.walkAction, 0.2); } // Show debug marker if (this.showDebugMarker) { this.createTargetMarker(this.targetPosition); } console.log(`🚶 Moving to position: (${x.toFixed(2)}, ${y.toFixed(2)}, ${z.toFixed(2)})`); } // Stop movement stop() { this.isMoving = false; this.targetPosition = null; // Switch back to idle if (this.idleAction) { this.crossFadeTo(this.idleAction, 0.3); } // Remove debug marker if (this.targetMarker) { this.scene.remove(this.targetMarker); this.targetMarker = null; } } // Update loop (call this in your main animation loop) update(deltaTime) { // Update movement mixer if (this.movementMixer) { this.movementMixer.update(deltaTime); } // Handle movement if (this.isMoving && this.targetPosition) { const currentPos = this.vrm.scene.position; const direction = new THREE.Vector3() .subVectors(this.targetPosition, currentPos); const distance = direction.length(); // Check if we've arrived if (distance < this.stopDistance) { this.stop(); console.log("✅ Arrived at destination"); return; } // Normalize direction direction.normalize(); // Move towards target const moveDistance = this.moveSpeed * deltaTime; const actualMove = Math.min(moveDistance, distance); currentPos.x += direction.x * actualMove; currentPos.z += direction.z * actualMove; // Keep Y position (don't move vertically unless specified) if (this.targetPosition.y !== currentPos.y) { currentPos.y = this.targetPosition.y; } // Rotate to face direction of movement const targetRotation = Math.atan2(direction.x, direction.z); const currentRotation = this.vrm.scene.rotation.y; // Smooth rotation interpolation let rotationDiff = targetRotation - currentRotation; // Normalize rotation difference to [-PI, PI] while (rotationDiff > Math.PI) rotationDiff -= Math.PI * 2; while (rotationDiff < -Math.PI) rotationDiff += Math.PI * 2; const maxRotation = this.rotationSpeed * deltaTime; const actualRotation = Math.max(-maxRotation, Math.min(maxRotation, rotationDiff)); this.vrm.scene.rotation.y += actualRotation; } } // Create a visual marker at target position (for debugging) createTargetMarker(position) { // Remove old marker if (this.targetMarker) { this.scene.remove(this.targetMarker); } // Create new marker const geometry = new THREE.SphereGeometry(0.1, 16, 16); const material = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.7 }); this.targetMarker = new THREE.Mesh(geometry, material); this.targetMarker.position.copy(position); this.scene.add(this.targetMarker); } // Enable/disable debug marker setDebugMarker(enabled) { this.showDebugMarker = enabled; if (!enabled && this.targetMarker) { this.scene.remove(this.targetMarker); this.targetMarker = null; } } // Set movement speed setSpeed(speed) { this.moveSpeed = speed; // Optionally switch to run animation for higher speeds if (this.runAction && speed > 2.5 && this.isMoving) { this.crossFadeTo(this.runAction, 0.2); } else if (this.walkAction && speed <= 2.5 && this.isMoving) { this.crossFadeTo(this.walkAction, 0.2); } } // Get current position getPosition() { return this.vrm.scene.position.clone(); } // Check if currently moving isCurrentlyMoving() { return this.isMoving; } }