import Phaser from 'phaser' import { WORLD_TILES } from '../config' import { stateManager } from '../StateManager' import type { LocalAdapter } from '../NetworkAdapter' const CAMERA_SPEED = 400 // px/s const MIN_ZOOM = 0.25 const MAX_ZOOM = 2.0 const ZOOM_STEP = 0.1 export class CameraSystem { private scene: Phaser.Scene private adapter: LocalAdapter private keys!: { up: Phaser.Input.Keyboard.Key down: Phaser.Input.Keyboard.Key left: Phaser.Input.Keyboard.Key right: Phaser.Input.Keyboard.Key w: Phaser.Input.Keyboard.Key s: Phaser.Input.Keyboard.Key a: Phaser.Input.Keyboard.Key d: Phaser.Input.Keyboard.Key } private saveTimer = 0 private readonly SAVE_TICK = 2000 private middlePanActive = false 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 // Start at saved player position (reused as camera anchor) cam.scrollX = state.player.x - cam.width / 2 cam.scrollY = state.player.y - cam.height / 2 const kb = this.scene.input.keyboard! this.keys = { up: kb.addKey(Phaser.Input.Keyboard.KeyCodes.UP), down: kb.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN), left: kb.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT), right: kb.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT), w: kb.addKey(Phaser.Input.Keyboard.KeyCodes.W), s: kb.addKey(Phaser.Input.Keyboard.KeyCodes.S), a: kb.addKey(Phaser.Input.Keyboard.KeyCodes.A), d: kb.addKey(Phaser.Input.Keyboard.KeyCodes.D), } // 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 this.scene.input.on('pointerdown', (ptr: Phaser.Input.Pointer) => { if (ptr.middleButtonDown()) { this.middlePanActive = true this.lastPanX = ptr.x this.lastPanY = ptr.y } }) // Middle-click pan: move camera while held this.scene.input.on('pointermove', (ptr: Phaser.Input.Pointer) => { if (!this.middlePanActive) return const dx = (ptr.x - this.lastPanX) / cam.zoom const dy = (ptr.y - this.lastPanY) / cam.zoom cam.scrollX -= dx cam.scrollY -= dy this.lastPanX = ptr.x this.lastPanY = ptr.y }) // Middle-click pan: stop on button release this.scene.input.on('pointerup', (ptr: Phaser.Input.Pointer) => { if (this.middlePanActive && !ptr.middleButtonDown()) { this.middlePanActive = false } }) } /** * 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 const up = this.keys.up.isDown || this.keys.w.isDown const down = this.keys.down.isDown || this.keys.s.isDown const left = this.keys.left.isDown || this.keys.a.isDown const right = this.keys.right.isDown || this.keys.d.isDown let dx = 0, dy = 0 if (left) dx -= speed if (right) dx += speed if (up) dy -= speed if (down) dy += speed if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707 } 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) // Periodically save camera center as "player position" this.saveTimer += delta if (this.saveTimer >= this.SAVE_TICK) { this.saveTimer = 0 this.adapter.send({ type: 'PLAYER_MOVE', x: cam.scrollX + cam.width / 2, y: cam.scrollY + cam.height / 2, }) } } /** * 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, 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) } } }