diff --git a/src/test/ZoomTestScene.ts b/src/test/ZoomTestScene.ts new file mode 100644 index 0000000..4468841 --- /dev/null +++ b/src/test/ZoomTestScene.ts @@ -0,0 +1,239 @@ +import Phaser from 'phaser' +import { TILE_SIZE } from '../config' + +const GRID_TILES = 50 // world size in tiles +const MIN_ZOOM = 0.25 +const MAX_ZOOM = 4.0 +const ZOOM_STEP = 0.1 +const MARKER_EVERY = 5 // small crosshair every N tiles +const LABEL_EVERY = 10 // coordinate label every N tiles +const CAMERA_SPEED = 400 // px/s + +/** + * First test scene: observes pure Phaser default zoom behavior. + * No custom scroll compensation — cam.setZoom() only, zoom anchors to camera center. + * + * Controls: Scroll wheel to zoom, WASD / Arrow keys to pan. + */ +export class ZoomTestScene extends Phaser.Scene { + private logText!: Phaser.GameObjects.Text + 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 + } + + constructor() { + super({ key: 'ZoomTest' }) + } + + create(): void { + this.drawGrid() + this.setupCamera() + this.setupInput() + this.createOverlay() + } + + /** + * Draws the static world grid into world space. + * - Faint tile lines on every tile boundary + * - Small green crosshairs every MARKER_EVERY tiles + * - Yellow labeled crosshairs every LABEL_EVERY tiles + * - Red world border + */ + private drawGrid(): void { + const worldPx = GRID_TILES * TILE_SIZE + const g = this.add.graphics() + + // Background fill + g.fillStyle(0x111811) + g.fillRect(0, 0, worldPx, worldPx) + + // Tile grid lines + g.lineStyle(1, 0x223322, 0.5) + for (let i = 0; i <= GRID_TILES; i++) { + const p = i * TILE_SIZE + g.lineBetween(p, 0, p, worldPx) + g.lineBetween(0, p, worldPx, p) + } + + // Crosshair markers + for (let tx = 0; tx <= GRID_TILES; tx += MARKER_EVERY) { + for (let ty = 0; ty <= GRID_TILES; ty += MARKER_EVERY) { + const px = tx * TILE_SIZE + const py = ty * TILE_SIZE + const isLabel = tx % LABEL_EVERY === 0 && ty % LABEL_EVERY === 0 + const color = isLabel ? 0xffff00 : 0x00ff88 + const arm = isLabel ? 10 : 6 + + g.lineStyle(1, color, isLabel ? 1.0 : 0.7) + g.lineBetween(px - arm, py, px + arm, py) + g.lineBetween(px, py - arm, px, py + arm) + + g.fillStyle(color, 1.0) + g.fillCircle(px, py, isLabel ? 2.5 : 1.5) + } + } + + // Coordinate labels at LABEL_EVERY intersections + for (let tx = 0; tx <= GRID_TILES; tx += LABEL_EVERY) { + for (let ty = 0; ty <= GRID_TILES; ty += LABEL_EVERY) { + this.add.text( + tx * TILE_SIZE + 4, + ty * TILE_SIZE + 4, + `${tx},${ty}`, + { fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' } + ).setDepth(1) + } + } + + // World border + g.lineStyle(2, 0xff4444, 1.0) + g.strokeRect(0, 0, worldPx, worldPx) + } + + /** + * Sets camera bounds and centers the view on the world. + */ + private setupCamera(): void { + const cam = this.cameras.main + const worldPx = GRID_TILES * TILE_SIZE + cam.setBounds(0, 0, worldPx, worldPx) + cam.scrollX = worldPx / 2 - cam.width / 2 + cam.scrollY = worldPx / 2 - cam.height / 2 + } + + /** + * Registers scroll wheel zoom and stores keyboard key references. + * Zoom uses cam.setZoom() only — pure Phaser default, anchors to camera center. + */ + private setupInput(): void { + const cam = this.cameras.main + const kb = this.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), + } + + this.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) + cam.setZoom(newZoom) + }) + } + + /** + * Creates the fixed HUD text overlay (scroll factor 0 = screen space). + */ + private createOverlay(): void { + this.logText = this.add.text(10, 10, '', { + fontSize: '13px', + color: '#e8e8e8', + backgroundColor: '#000000bb', + padding: { x: 10, y: 8 }, + lineSpacing: 3, + fontFamily: 'monospace', + }) + .setScrollFactor(0) + .setDepth(100) + } + + update(_time: number, delta: number): void { + this.handleKeyboard(delta) + this.updateOverlay() + } + + /** + * Moves camera with WASD / arrow keys at CAMERA_SPEED px/s (world space). + * @param delta - Frame delta in milliseconds + */ + private handleKeyboard(delta: number): void { + const cam = this.cameras.main + const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom + const worldPx = GRID_TILES * TILE_SIZE + + let dx = 0, dy = 0 + if (this.keys.left.isDown || this.keys.a.isDown) dx -= speed + if (this.keys.right.isDown || this.keys.d.isDown) dx += speed + if (this.keys.up.isDown || this.keys.w.isDown) dy -= speed + if (this.keys.down.isDown || this.keys.s.isDown) dy += speed + + if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707 } + + cam.scrollX = Phaser.Math.Clamp(cam.scrollX + dx, 0, worldPx - cam.width / cam.zoom) + cam.scrollY = Phaser.Math.Clamp(cam.scrollY + dy, 0, worldPx - cam.height / cam.zoom) + } + + /** + * Recomputes and renders all diagnostic values to the HUD overlay each frame. + */ + private updateOverlay(): void { + const cam = this.cameras.main + const ptr = this.input.activePointer + + // Viewport size in world pixels (what is actually visible) + const vpWidthPx = cam.width / cam.zoom + const vpHeightPx = cam.height / cam.zoom + + // Viewport size in tiles + const vpWidthTiles = vpWidthPx / TILE_SIZE + const vpHeightTiles = vpHeightPx / TILE_SIZE + + // Camera center in world coords + const centerWorldX = cam.scrollX + vpWidthPx / 2 + const centerWorldY = cam.scrollY + vpHeightPx / 2 + + // Tile under mouse + const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE) + const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE) + + // Tile at camera center + const centerTileX = Math.floor(centerWorldX / TILE_SIZE) + const centerTileY = Math.floor(centerWorldY / TILE_SIZE) + + const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas' + + const lines = [ + '── ZOOM TEST [Phaser default] ──', + '', + `Zoom: ${cam.zoom.toFixed(4)}`, + `scrollX / scrollY: ${cam.scrollX.toFixed(2)} / ${cam.scrollY.toFixed(2)}`, + '', + `Viewport (screen): ${cam.width} × ${cam.height} px`, + `Viewport (world): ${vpWidthPx.toFixed(2)} × ${vpHeightPx.toFixed(2)} px`, + `Viewport (tiles): ${vpWidthTiles.toFixed(3)} × ${vpHeightTiles.toFixed(3)}`, + '', + `Center world: ${centerWorldX.toFixed(2)}, ${centerWorldY.toFixed(2)}`, + `Center tile: ${centerTileX}, ${centerTileY}`, + '', + `Mouse screen: ${ptr.x.toFixed(1)}, ${ptr.y.toFixed(1)}`, + `Mouse world: ${ptr.worldX.toFixed(2)}, ${ptr.worldY.toFixed(2)}`, + `Mouse tile: ${mouseTileX}, ${mouseTileY}`, + '', + `Canvas: ${this.scale.width} × ${this.scale.height} px`, + `TILE_SIZE: ${TILE_SIZE} px`, + `roundPixels: ${(this.game.renderer.config as Record)['roundPixels']}`, + `Renderer: ${renderer}`, + '', + '[Scroll] Zoom [WASD / ↑↓←→] Pan', + ] + + this.logText.setText(lines) + } +} diff --git a/src/test/main.ts b/src/test/main.ts new file mode 100644 index 0000000..2aa42ff --- /dev/null +++ b/src/test/main.ts @@ -0,0 +1,21 @@ +import Phaser from 'phaser' +import { ZoomTestScene } from './ZoomTestScene' + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + width: window.innerWidth, + height: window.innerHeight, + backgroundColor: '#0d1a0d', + scene: [ZoomTestScene], + scale: { + mode: Phaser.Scale.RESIZE, + autoCenter: Phaser.Scale.CENTER_BOTH, + }, + render: { + pixelArt: false, + antialias: true, + roundPixels: true, + }, +} + +new Phaser.Game(config) diff --git a/test.html b/test.html new file mode 100644 index 0000000..1eebe4a --- /dev/null +++ b/test.html @@ -0,0 +1,16 @@ + + + + + + Game — Test Scenes + + + + + + diff --git a/vite.config.ts b/vite.config.ts index b6ab531..6d5059d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite' +import { resolve } from 'path' export default defineConfig({ server: { @@ -7,6 +8,12 @@ export default defineConfig({ }, build: { outDir: 'dist', - assetsInlineLimit: 0 - } + assetsInlineLimit: 0, + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html'), + test: resolve(__dirname, 'test.html'), + }, + }, + }, })