Files
nissefolk/src/systems/CameraSystem.ts

164 lines
5.8 KiB
TypeScript
Raw Normal View History

2026-03-20 08:11:31 +00:00
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
private mouseScreenX = 0
private mouseScreenY = 0
private mouseWorldX = 0
private mouseWorldY = 0
private debugCross?: Phaser.GameObjects.Graphics
2026-03-20 08:11:31 +00:00
constructor(scene: Phaser.Scene, adapter: LocalAdapter) {
this.scene = scene
this.adapter = adapter
}
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),
}
// Debug cross at viewport center (fixed to screen, not world)
this.debugCross = this.scene.add.graphics()
this.debugCross.setScrollFactor(0).setDepth(999)
this.debugCross.lineStyle(2, 0xff0000, 1)
const cx = cam.width / 2
const cy = cam.height / 2
this.debugCross.lineBetween(cx - 12, cy, cx + 12, cy)
this.debugCross.lineBetween(cx, cy - 12, cx, cy + 12)
// Track mouse world position on every move so we have a stable value for zoom
this.scene.input.on('pointermove', (ptr: Phaser.Input.Pointer) => {
this.mouseScreenX = ptr.x
this.mouseScreenY = ptr.y
this.mouseWorldX = cam.scrollX + ptr.x / cam.zoom
this.mouseWorldY = cam.scrollY + ptr.y / cam.zoom
})
// Scroll wheel zoom — zoom toward mouse pointer
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)
if (newZoom === cam.zoom) return
cam.setZoom(newZoom)
cam.scrollX = this.mouseWorldX - this.mouseScreenX / newZoom
cam.scrollY = this.mouseWorldY - this.mouseScreenY / newZoom
const centerX = cam.scrollX + cam.width / (2 * cam.zoom)
const centerY = cam.scrollY + cam.height / (2 * cam.zoom)
console.log(`[zoom] screen=(${this.mouseScreenX.toFixed(0)},${this.mouseScreenY.toFixed(0)}) world=(${this.mouseWorldX.toFixed(0)},${this.mouseWorldY.toFixed(0)}) center=(${centerX.toFixed(0)},${centerY.toFixed(0)}) zoom=${cam.zoom.toFixed(2)}`)
})
// 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
}
2026-03-20 08:11:31 +00:00
})
}
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 // TILE_SIZE hardcoded since WORLD_PX may not exist
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,
})
}
}
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),
}
}
getCenterTile(): { tileX: number; tileY: number } {
const { x, y } = this.getCenterWorld()
return { tileX: Math.floor(x / 32), tileY: Math.floor(y / 32) }
}
}