diff --git a/src/systems/CameraSystem.ts b/src/systems/CameraSystem.ts index a41b456..98c89dd 100644 --- a/src/systems/CameraSystem.ts +++ b/src/systems/CameraSystem.ts @@ -27,11 +27,19 @@ export class CameraSystem { private lastPanX = 0 private lastPanY = 0 + /** + * @param scene - The Phaser scene this system belongs to + * @param adapter - Network adapter used to persist camera position + */ constructor(scene: Phaser.Scene, adapter: LocalAdapter) { this.scene = scene this.adapter = adapter } + /** + * Initializes the camera: restores saved position, registers keyboard keys, + * sets up scroll-wheel zoom-to-mouse, and middle-click pan. + */ create(): void { const state = stateManager.getState() const cam = this.scene.cameras.main @@ -52,10 +60,22 @@ export class CameraSystem { d: kb.addKey(Phaser.Input.Keyboard.KeyCodes.D), } - // Scroll wheel zoom - this.scene.input.on('wheel', (_ptr: Phaser.Input.Pointer, _objs: unknown, _dx: number, dy: number) => { - const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM) + // Scroll wheel: zoom-to-mouse. + // Phaser zooms from the screen center, so the world point under the mouse + // is corrected by shifting scroll by the mouse offset from center. + this.scene.input.on('wheel', (ptr: Phaser.Input.Pointer, _objs: unknown, _dx: number, dy: number) => { + const zoomBefore = cam.zoom + const newZoom = Phaser.Math.Clamp(zoomBefore - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM) cam.setZoom(newZoom) + + const factor = 1 / zoomBefore - 1 / newZoom + cam.scrollX += (ptr.x - cam.width / 2) * factor + cam.scrollY += (ptr.y - cam.height / 2) * factor + + const worldW = WORLD_TILES * 32 + const worldH = WORLD_TILES * 32 + cam.scrollX = Phaser.Math.Clamp(cam.scrollX, 0, worldW - cam.width / newZoom) + cam.scrollY = Phaser.Math.Clamp(cam.scrollY, 0, worldH - cam.height / newZoom) }) // Middle-click pan: start on button down @@ -86,6 +106,10 @@ export class CameraSystem { }) } + /** + * Moves the camera via keyboard input and periodically saves the position. + * @param delta - Frame delta in milliseconds + */ update(delta: number): void { const cam = this.scene.cameras.main const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom @@ -103,7 +127,7 @@ export class CameraSystem { if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707 } - const worldW = WORLD_TILES * 32 // TILE_SIZE hardcoded since WORLD_PX may not exist + const worldW = WORLD_TILES * 32 const worldH = WORLD_TILES * 32 cam.scrollX = Phaser.Math.Clamp(cam.scrollX + dx, 0, worldW - cam.width / cam.zoom) cam.scrollY = Phaser.Math.Clamp(cam.scrollY + dy, 0, worldH - cam.height / cam.zoom) @@ -120,14 +144,24 @@ export class CameraSystem { } } + /** + * Returns the world coordinates of the visual camera center. + * Phaser zooms from the screen center, so the center world point + * is scrollX + screenWidth/2 (independent of zoom level). + * @returns World position of the screen center + */ getCenterWorld(): { x: number; y: number } { const cam = this.scene.cameras.main return { - x: cam.scrollX + cam.width / (2 * cam.zoom), - y: cam.scrollY + cam.height / (2 * cam.zoom), + x: cam.scrollX + cam.width / 2, + y: cam.scrollY + cam.height / 2, } } + /** + * Returns the tile coordinates of the visual camera center. + * @returns Tile position (integer) of the screen center + */ getCenterTile(): { tileX: number; tileY: number } { const { x, y } = this.getCenterWorld() return { tileX: Math.floor(x / 32), tileY: Math.floor(y / 32) }