import { colord } from "colord"; import { EventBus } from "../../core/EventBus"; import { Cell, Game, Player } from "../../core/game/Game"; import { calculateBoundingBox, calculateBoundingBoxCenter, } from "../../core/Util"; import { ZoomEvent, DragEvent, CenterCameraEvent } from "../InputHandler"; import { GoToPlayerEvent, GoToUnitEvent } from "./layers/Leaderboard"; import { placeName } from "./NameBoxCalculator"; import { GameView } from "../../core/game/GameView"; export class TransformHandler { public scale: number = 1.8; private offsetX: number = -350; private offsetY: number = -200; private target: Cell; private intervalID = null; private changed = false; constructor( private game: GameView, private eventBus: EventBus, private canvas: HTMLCanvasElement, ) { this.eventBus.on(ZoomEvent, (e) => this.onZoom(e)); this.eventBus.on(DragEvent, (e) => this.onMove(e)); this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e)); this.eventBus.on(GoToUnitEvent, (e) => this.onGoToUnit(e)); this.eventBus.on(CenterCameraEvent, () => this.centerCamera()); } boundingRect(): DOMRect { return this.canvas.getBoundingClientRect(); } width(): number { return this.boundingRect().width; } hasChanged(): boolean { return this.changed; } handleTransform(context: CanvasRenderingContext2D) { // Disable image smoothing for pixelated effect context.imageSmoothingEnabled = false; // Apply zoom and pan context.setTransform( this.scale, 0, 0, this.scale, this.game.width() / 2 - this.offsetX * this.scale, this.game.height() / 2 - this.offsetY * this.scale, ); this.changed = false; } worldToScreenCoordinates(cell: Cell): { x: number; y: number } { // Step 1: Convert from Cell coordinates to game coordinates // (reverse of Math.floor operation - we'll use the exact values) const gameX = cell.x; const gameY = cell.y; // Step 2: Reverse the game center offset calculation // Original: gameX = centerX + this.game.width() / 2 // Therefore: centerX = gameX - this.game.width() / 2 const centerX = gameX - this.game.width() / 2; const centerY = gameY - this.game.height() / 2; // Step 3: Reverse the world point calculation // Original: centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX // Therefore: canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2 const canvasX = (centerX - this.offsetX) * this.scale + this.game.width() / 2; const canvasY = (centerY - this.offsetY) * this.scale + this.game.height() / 2; // Step 4: Convert canvas coordinates back to screen coordinates const canvasRect = this.boundingRect(); const screenX = canvasX + canvasRect.left; const screenY = canvasY + canvasRect.top; return { x: screenX, y: screenY }; } screenToWorldCoordinates(screenX: number, screenY: number): Cell { const canvasRect = this.boundingRect(); const canvasX = screenX - canvasRect.left; const canvasY = screenY - canvasRect.top; // Calculate the world point we want to zoom towards const centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX; const centerY = (canvasY - this.game.height() / 2) / this.scale + this.offsetY; const gameX = centerX + this.game.width() / 2; const gameY = centerY + this.game.height() / 2; return new Cell(Math.floor(gameX), Math.floor(gameY)); } screenBoundingRect(): [Cell, Cell] { const LeftX = -this.game.width() / 2 / this.scale + this.offsetX; const TopY = -this.game.height() / 2 / this.scale + this.offsetY; const gameLeftX = LeftX + this.game.width() / 2; const gameTopY = TopY + this.game.height() / 2; const rightX = (screen.width - this.game.width() / 2) / this.scale + this.offsetX; const rightY = (screen.height - this.game.height() / 2) / this.scale + this.offsetY; const gameRightX = rightX + this.game.width() / 2; const gameBottomY = rightY + this.game.height() / 2; return [ new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY)), ]; } isOnScreen(cell: Cell): boolean { const [topLeft, bottomRight] = this.screenBoundingRect(); return ( cell.x > topLeft.x && cell.x < bottomRight.x && cell.y > topLeft.y && cell.y < bottomRight.y ); } screenCenter(): { screenX: number; screenY: number } { const [upperLeft, bottomRight] = this.screenBoundingRect(); return { screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2), screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2), }; } onGoToPlayer(event: GoToPlayerEvent) { this.clearTarget(); this.target = new Cell( event.player.nameLocation().x, event.player.nameLocation().y, ); this.intervalID = setInterval(() => this.goTo(), 1); } onGoToUnit(event: GoToUnitEvent) { this.clearTarget(); this.target = new Cell( this.game.x(event.unit.lastTile()), this.game.y(event.unit.lastTile()), ); this.intervalID = setInterval(() => this.goTo(), 1); } centerCamera() { this.clearTarget(); const player = this.game.myPlayer(); if (!player || !player.nameLocation()) return; this.target = new Cell(player.nameLocation().x, player.nameLocation().y); this.intervalID = setInterval(() => this.goTo(), 1); } private goTo() { const { screenX, screenY } = this.screenCenter(); const screenMapCenter = new Cell(screenX, screenY); if ( this.game.manhattanDist( this.game.ref(screenX, screenY), this.game.ref(this.target.x, this.target.y), ) < 2 ) { this.clearTarget(); return; } const dX = Math.abs(screenMapCenter.x - this.target.x); if (dX > 2) { const offsetDx = Math.max(1, Math.floor(dX / 25)); if (screenMapCenter.x > this.target.x) { this.offsetX -= offsetDx; } else { this.offsetX += offsetDx; } } const dY = Math.abs(screenMapCenter.y - this.target.y); if (dY > 2) { const offsetDy = Math.max(1, Math.floor(dY / 25)); if (screenMapCenter.y > this.target.y) { this.offsetY -= offsetDy; } else { this.offsetY += offsetDy; } } this.changed = true; } onZoom(event: ZoomEvent) { this.clearTarget(); const oldScale = this.scale; const zoomFactor = 1 + event.delta / 600; this.scale /= zoomFactor; // Clamp the scale to prevent extreme zooming this.scale = Math.max(0.2, Math.min(20, this.scale)); const canvasRect = this.boundingRect(); const canvasX = event.x - canvasRect.left; const canvasY = event.y - canvasRect.top; // Calculate the world point we want to zoom towards const zoomPointX = (canvasX - this.game.width() / 2) / oldScale + this.offsetX; const zoomPointY = (canvasY - this.game.height() / 2) / oldScale + this.offsetY; // Adjust the offset this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale; this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale; this.changed = true; } onMove(event: DragEvent) { this.clearTarget(); this.offsetX -= event.deltaX / this.scale; this.offsetY -= event.deltaY / this.scale; this.changed = true; } private clearTarget() { if (this.intervalID != null) { clearInterval(this.intervalID); this.intervalID = null; } this.target = null; } }