import * as THREE from 'three'; import { DIRECTIONS } from '../engine/dungeon/Constants.js'; export class CameraManager { constructor(renderer) { this.renderer = renderer; // Reference to GameRenderer to access scenes/resize if needed // Configuration // Configuration this.zoomLevel = 6.0; // Started further back as requested this.aspect = window.innerWidth / window.innerHeight; this.onZoomChange = null; // Isometric Setup: Orthographic Camera this.camera = new THREE.OrthographicCamera( -this.zoomLevel * this.aspect, this.zoomLevel * this.aspect, this.zoomLevel, -this.zoomLevel, 1, 1000 ); // Initial Position: Isometric View this.target = new THREE.Vector3(0, 0, 0); // Focus point this.isoOffset = new THREE.Vector3(20, 20, 20); // Relative offset this.camera.position.copy(this.target).add(this.isoOffset); this.camera.lookAt(this.target); // --- Controls State --- this.isDragging = false; this.lastMouseX = 0; this.lastMouseY = 0; this.panSpeed = 0.5; // Current Snap View (North, East, South, West) this.currentViewAngle = 0; // Animation state for smooth transitions this.isAnimating = false; this.animationStartPos = new THREE.Vector3(); this.animationTargetPos = new THREE.Vector3(); this.animationProgress = 0; this.animationDuration = 0.5; // seconds this.animationStartTime = 0; this.setupInputListeners(); } getCamera() { return this.camera; } centerOn(x, y) { // Calculate current offset relative to OLD target const currentOffset = this.camera.position.clone().sub(this.target); // Update target: Grid (x, y) -> World (x, 0, -y) this.target.set(x, 0, -y); // Restore position with new target + same relative offset this.camera.position.copy(this.target).add(currentOffset); this.camera.lookAt(this.target); } setupInputListeners() { // Zoom (Mouse Wheel) window.addEventListener('wheel', (e) => { e.preventDefault(); // Adjust Zoom Level property if (e.deltaY < 0) this.zoomLevel = Math.max(3, this.zoomLevel - 1); else this.zoomLevel = Math.min(15, this.zoomLevel + 1); this.updateProjection(); if (this.onZoomChange) this.onZoomChange(this.zoomLevel); }, { passive: false }); // Pan Listeners (Middle Click) window.addEventListener('mousedown', (e) => { if (e.button === 1) { // Middle Mouse this.isDragging = true; this.lastMouseX = e.clientX; this.lastMouseY = e.clientY; } }); window.addEventListener('mouseup', () => { this.isDragging = false; }); window.addEventListener('mousemove', (e) => { if (this.isDragging) { const dx = e.clientX - this.lastMouseX; const dy = e.clientY - this.lastMouseY; this.lastMouseX = e.clientX; this.lastMouseY = e.clientY; this.pan(-dx, dy); } }); // Resize Listener linkage window.addEventListener('resize', () => { this.aspect = window.innerWidth / window.innerHeight; this.updateProjection(); }); } updateProjection() { this.camera.left = -this.zoomLevel * this.aspect; this.camera.right = this.zoomLevel * this.aspect; this.camera.top = this.zoomLevel; this.camera.bottom = -this.zoomLevel; this.camera.updateProjectionMatrix(); } pan(dx, dy) { // Move Speed Factor const moveSpeed = this.panSpeed * 0.05 * (this.zoomLevel / 10); // Direction: Dragging the "World" // Mouse Left (dx < 0) -> Camera moves Right (+X) // Mouse Up (dy < 0) -> Camera moves Down (-Y) const moveX = dx * moveSpeed; const moveY = dy * moveSpeed; // Apply to Camera (Local Space) this.camera.translateX(moveX); this.camera.translateY(moveY); // Calculate World Movement to update Target const vRight = new THREE.Vector3(1, 0, 0).applyQuaternion(this.camera.quaternion); const vUp = new THREE.Vector3(0, 1, 0).applyQuaternion(this.camera.quaternion); const worldTranslation = new THREE.Vector3() .addScaledVector(vRight, moveX) .addScaledVector(vUp, moveY); // Apply same movement to Target so relative offset is preserved // This ensures lookAt() doesn't pivot the camera around the old center this.target.add(worldTranslation); } update(deltaTime) { // Update camera animation if active if (this.isAnimating) { const elapsed = (performance.now() - this.animationStartTime) / 1000; this.animationProgress = Math.min(elapsed / this.animationDuration, 1); // Easing function (ease-in-out) const t = this.animationProgress; const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // Interpolate position this.camera.position.lerpVectors(this.animationStartPos, this.animationTargetPos, eased); this.camera.lookAt(this.target); // End animation if (this.animationProgress >= 1) { this.isAnimating = false; this.camera.position.copy(this.animationTargetPos); } } } // --- Fixed Orbit Logic --- setIsoView(direction) { // Rotate camera around target while maintaining isometric angle // Isometric view: 45 degree angle from horizontal const distance = 28; // Distance from target const isoAngle = Math.PI / 4; // 45 degrees for isometric view // Horizontal rotation angle based on direction let horizontalAngle = 0; switch (direction) { case DIRECTIONS.NORTH: // 'N' horizontalAngle = Math.PI / 4; // 45 degrees (NE in isometric) break; case DIRECTIONS.EAST: // 'E' horizontalAngle = -Math.PI / 4; // -45 degrees (SE in isometric) break; case DIRECTIONS.SOUTH: // 'S' horizontalAngle = -3 * Math.PI / 4; // -135 degrees (SW in isometric) break; case DIRECTIONS.WEST: // 'W' horizontalAngle = 3 * Math.PI / 4; // 135 degrees (NW in isometric) break; } // Calculate camera position maintaining isometric angle // x and z form a circle on the horizontal plane // y is elevated to maintain the isometric angle const horizontalDistance = distance * Math.cos(isoAngle); const height = distance * Math.sin(isoAngle); const x = this.target.x + horizontalDistance * Math.cos(horizontalAngle); const z = this.target.z + horizontalDistance * Math.sin(horizontalAngle); // Start animation instead of instant change this.animationStartPos.copy(this.camera.position); this.animationTargetPos.set(x, height, z); this.animationProgress = 0; this.animationStartTime = performance.now(); this.isAnimating = true; this.currentViewAngle = horizontalAngle; } }