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