export class InputHandler { constructor(app) { this.app = app; this.canvas = null; this.isRightMouseDown = false; this.lastMouseX = 0; this.lastMouseY = 0; this.currentTileX = null; this.currentTileY = null; this.cursorUpdateThrottle = 100; // ms this.lastCursorUpdate = 0; this.keyPanSpeed = 3; // Controls camera movement speed with arrow keys } init() { this.canvas = document.getElementById('gameCanvas'); // Mouse events this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e)); this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e)); this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e)); this.canvas.addEventListener('wheel', (e) => this.onWheel(e)); // Prevent context menu on canvas and document this.canvas.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); return false; }); // Also prevent on document level for better coverage document.addEventListener('contextmenu', (e) => { if (e.target === this.canvas || this.canvas.contains(e.target)) { e.preventDefault(); e.stopPropagation(); return false; } }); // Keyboard events document.addEventListener('keydown', (e) => this.onKeyDown(e)); } onMouseDown(event) { if (event.button === 2) { // Right mouse button this.isRightMouseDown = true; this.lastMouseX = event.clientX; this.lastMouseY = event.clientY; this.canvas.style.cursor = 'grabbing'; } else if (event.button === 0) { // Left mouse button const tile = this.app.renderer.screenToWorld(event.clientX, event.clientY); if (this.app.isPlacingBuilding && this.app.selectedBuildingType) { // Place building this.app.placeBuilding(tile.x, tile.y); } } } onMouseUp(event) { if (event.button === 2) { // Right mouse button this.isRightMouseDown = false; // A right-click should cancel building placement mode this.app.isPlacingBuilding = false; this.app.selectedBuildingType = null; this.canvas.style.cursor = 'default'; // Check if click (not drag) const dragThreshold = 5; const dx = Math.abs(event.clientX - this.lastMouseX); const dy = Math.abs(event.clientY - this.lastMouseY); if (dx < dragThreshold && dy < dragThreshold) { // Right click on tile - show context menu with improved hit detection const tile = this.app.renderer.screenToWorld(event.clientX, event.clientY); const buildingInfo = this.findBuildingNearTile(tile.x, tile.y); if (buildingInfo && buildingInfo.building.owner_id === this.app.player.player_id) { this.app.uiManager.showContextMenu( event.clientX, event.clientY, buildingInfo.x, buildingInfo.y ); } } } } onMouseMove(event) { // Update tile position const tile = this.app.renderer.screenToWorld(event.clientX, event.clientY); if (tile.x !== this.currentTileX || tile.y !== this.currentTileY) { this.currentTileX = tile.x; this.currentTileY = tile.y; // Highlight tile with building detection feedback const buildingInfo = this.findBuildingNearTile(tile.x, tile.y); if (buildingInfo && buildingInfo.building.owner_id === this.app.player?.player_id) { // Highlight the actual building tile if we found one nearby this.app.renderer.highlightTile(buildingInfo.x, buildingInfo.y); this.canvas.style.cursor = 'pointer'; // Show pointer cursor over owned buildings } else { // Normal tile highlight this.app.renderer.highlightTile(tile.x, tile.y); this.canvas.style.cursor = this.isRightMouseDown ? 'grabbing' : 'default'; } // Send cursor position to server (throttled) const now = Date.now(); if (now - this.lastCursorUpdate > this.cursorUpdateThrottle) { this.app.sendCursorPosition(tile.x, tile.y); this.lastCursorUpdate = now; } } // Handle camera panning if (this.isRightMouseDown) { const dx = (event.clientX - this.lastMouseX) * 0.1; const dy = (event.clientY - this.lastMouseY) * 0.1; this.app.renderer.moveCamera(-dx, dy); this.lastMouseX = event.clientX; this.lastMouseY = event.clientY; } } onWheel(event) { event.preventDefault(); const delta = event.deltaY > 0 ? -0.1 : 0.1; this.app.renderer.zoomCamera(delta); } onKeyDown(event) { if (event.key === 'Escape') { this.app.isPlacingBuilding = false; this.app.selectedBuildingType = null; this.app.uiManager.hideContextMenu(); } let moved = false; const s = this.keyPanSpeed; // shorthand for speed switch (event.key) { case 'ArrowUp': this.app.renderer.moveCamera(0, -s); // Move straight up in world coordinates moved = true; break; case 'ArrowDown': this.app.renderer.moveCamera(0, s); // Move straight down in world coordinates moved = true; break; case 'ArrowLeft': this.app.renderer.moveCamera(-s, 0); // Move straight left in world coordinates moved = true; break; case 'ArrowRight': this.app.renderer.moveCamera(s, 0); // Move straight right in world coordinates moved = true; break; } if (moved) { event.preventDefault(); // Prevents the browser from scrolling } } findBuildingNearTile(centerX, centerY) { /** * Improved building hit detection with expanded click area. * * Problem: Previously, users had to click exactly on the building's ground tile * which was frustrating and imprecise, especially for 3D buildings. * * Solution: This function checks a 3x3 grid around the clicked point, * making buildings much easier to target by expanding the effective click area. * * Search order: exact tile first, then adjacent tiles, then diagonal tiles * to ensure the closest building is selected when multiple are nearby. */ // First try exact tile (for precision when needed) let building = this.app.gameState.buildings[`${centerX},${centerY}`]; if (building) { return { building, x: centerX, y: centerY }; } // Then check a 3x3 area around the clicked tile for easier targeting // Check closer tiles first for better UX const searchOrder = [ [0, -1], [0, 1], [-1, 0], [1, 0], // Adjacent tiles (distance 1) [-1, -1], [-1, 1], [1, -1], [1, 1] // Diagonal tiles (distance 1.41) ]; for (const [dx, dy] of searchOrder) { const x = centerX + dx; const y = centerY + dy; building = this.app.gameState.buildings[`${x},${y}`]; if (building) { return { building, x, y }; } } return null; // No building found in area } }