Files
WarhammerQuest/src/view/CameraManager.js

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;
}
}