211 lines
7.4 KiB
JavaScript
211 lines
7.4 KiB
JavaScript
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;
|
|
}
|
|
}
|