2026-03-21 11:16:39 +00:00
|
|
|
|
import Phaser from 'phaser'
|
|
|
|
|
|
import { TILE_SIZE } from '../config'
|
|
|
|
|
|
|
2026-03-21 11:34:04 +00:00
|
|
|
|
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
|
|
|
|
|
|
const SNAPSHOT_EVERY = 2000 // ms between periodic log snapshots
|
2026-03-21 11:16:39 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* First test scene: observes pure Phaser default zoom behavior.
|
|
|
|
|
|
* No custom scroll compensation — cam.setZoom() only, zoom anchors to camera center.
|
2026-03-21 11:19:54 +00:00
|
|
|
|
* Logs zoom events and periodic snapshots to /api/log (written to game-test.log).
|
2026-03-21 11:16:39 +00:00
|
|
|
|
*
|
|
|
|
|
|
* Controls: Scroll wheel to zoom, WASD / Arrow keys to pan.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class ZoomTestScene extends Phaser.Scene {
|
|
|
|
|
|
private logText!: Phaser.GameObjects.Text
|
2026-03-21 11:34:04 +00:00
|
|
|
|
private hudCamera!: Phaser.Cameras.Scene2D.Camera
|
|
|
|
|
|
private worldObjects: Phaser.GameObjects.GameObject[] = []
|
|
|
|
|
|
private hudObjects: Phaser.GameObjects.GameObject[] = []
|
2026-03-21 11:16:39 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-21 11:19:54 +00:00
|
|
|
|
private snapshotTimer = 0
|
2026-03-21 11:16:39 +00:00
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
super({ key: 'ZoomTest' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create(): void {
|
2026-03-21 11:19:54 +00:00
|
|
|
|
// Clear log file at scene start
|
|
|
|
|
|
fetch('/api/log', { method: 'DELETE' })
|
|
|
|
|
|
this.writeLog('scene_start', { tileSize: TILE_SIZE, gridTiles: GRID_TILES })
|
|
|
|
|
|
|
2026-03-21 11:16:39 +00:00
|
|
|
|
this.drawGrid()
|
|
|
|
|
|
this.setupCamera()
|
|
|
|
|
|
this.setupInput()
|
2026-03-21 11:34:04 +00:00
|
|
|
|
this.createHUD()
|
|
|
|
|
|
this.setupCameras()
|
2026-03-21 11:16:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Draws the static world grid into world space.
|
2026-03-21 11:34:04 +00:00
|
|
|
|
* All objects are registered in worldObjects for HUD-camera exclusion.
|
2026-03-21 11:16:39 +00:00
|
|
|
|
*/
|
|
|
|
|
|
private drawGrid(): void {
|
|
|
|
|
|
const worldPx = GRID_TILES * TILE_SIZE
|
|
|
|
|
|
const g = this.add.graphics()
|
2026-03-21 11:34:04 +00:00
|
|
|
|
this.worldObjects.push(g)
|
2026-03-21 11:16:39 +00:00
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-21 11:34:04 +00:00
|
|
|
|
const label = this.add.text(
|
2026-03-21 11:16:39 +00:00
|
|
|
|
tx * TILE_SIZE + 4,
|
|
|
|
|
|
ty * TILE_SIZE + 4,
|
|
|
|
|
|
`${tx},${ty}`,
|
|
|
|
|
|
{ fontSize: '9px', color: '#ffff88', fontFamily: 'monospace' }
|
|
|
|
|
|
).setDepth(1)
|
2026-03-21 11:34:04 +00:00
|
|
|
|
this.worldObjects.push(label)
|
2026-03-21 11:16:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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.
|
2026-03-21 11:19:54 +00:00
|
|
|
|
* Each zoom event is logged immediately with before/after state.
|
2026-03-21 11:16:39 +00:00
|
|
|
|
*/
|
|
|
|
|
|
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', (
|
2026-03-21 11:19:54 +00:00
|
|
|
|
ptr: Phaser.Input.Pointer,
|
2026-03-21 11:16:39 +00:00
|
|
|
|
_objs: unknown,
|
|
|
|
|
|
_dx: number,
|
|
|
|
|
|
dy: number
|
|
|
|
|
|
) => {
|
2026-03-21 11:34:04 +00:00
|
|
|
|
const zoomBefore = cam.zoom
|
2026-03-21 11:19:54 +00:00
|
|
|
|
const scrollXBefore = cam.scrollX
|
|
|
|
|
|
const scrollYBefore = cam.scrollY
|
|
|
|
|
|
|
2026-03-21 11:16:39 +00:00
|
|
|
|
const newZoom = Phaser.Math.Clamp(cam.zoom - Math.sign(dy) * ZOOM_STEP, MIN_ZOOM, MAX_ZOOM)
|
|
|
|
|
|
cam.setZoom(newZoom)
|
2026-03-21 11:19:54 +00:00
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.writeLog('zoom', {
|
2026-03-21 11:34:04 +00:00
|
|
|
|
direction: dy > 0 ? 'out' : 'in',
|
|
|
|
|
|
zoomBefore: +zoomBefore.toFixed(4),
|
|
|
|
|
|
zoomAfter: +cam.zoom.toFixed(4),
|
2026-03-21 11:19:54 +00:00
|
|
|
|
scrollX_before: +scrollXBefore.toFixed(2),
|
|
|
|
|
|
scrollY_before: +scrollYBefore.toFixed(2),
|
|
|
|
|
|
scrollX_after: +cam.scrollX.toFixed(2),
|
|
|
|
|
|
scrollY_after: +cam.scrollY.toFixed(2),
|
|
|
|
|
|
scrollX_delta: +(cam.scrollX - scrollXBefore).toFixed(2),
|
|
|
|
|
|
scrollY_delta: +(cam.scrollY - scrollYBefore).toFixed(2),
|
2026-03-21 11:34:04 +00:00
|
|
|
|
mouseScreen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) },
|
|
|
|
|
|
mouseWorld: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) },
|
2026-03-21 11:19:54 +00:00
|
|
|
|
centerWorld_after: {
|
|
|
|
|
|
x: +(cam.scrollX + (cam.width / cam.zoom) / 2).toFixed(2),
|
|
|
|
|
|
y: +(cam.scrollY + (cam.height / cam.zoom) / 2).toFixed(2),
|
|
|
|
|
|
},
|
|
|
|
|
|
vpTiles_after: {
|
|
|
|
|
|
w: +((cam.width / cam.zoom) / TILE_SIZE).toFixed(3),
|
|
|
|
|
|
h: +((cam.height / cam.zoom) / TILE_SIZE).toFixed(3),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}, 0)
|
2026-03-21 11:16:39 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-21 11:34:04 +00:00
|
|
|
|
* Creates all HUD elements: log overlay and screen-center crosshair.
|
|
|
|
|
|
* All objects are registered in hudObjects for main-camera exclusion.
|
|
|
|
|
|
* Uses a dedicated HUD camera (zoom=1, fixed) so elements are never scaled.
|
2026-03-21 11:16:39 +00:00
|
|
|
|
*/
|
2026-03-21 11:34:04 +00:00
|
|
|
|
private createHUD(): void {
|
|
|
|
|
|
const w = this.scale.width
|
|
|
|
|
|
const h = this.scale.height
|
|
|
|
|
|
|
|
|
|
|
|
// Screen-center crosshair (red)
|
|
|
|
|
|
const cross = this.add.graphics()
|
|
|
|
|
|
const arm = 16
|
|
|
|
|
|
cross.lineStyle(1, 0xff2222, 0.9)
|
|
|
|
|
|
cross.lineBetween(w / 2 - arm, h / 2, w / 2 + arm, h / 2)
|
|
|
|
|
|
cross.lineBetween(w / 2, h / 2 - arm, w / 2, h / 2 + arm)
|
|
|
|
|
|
cross.fillStyle(0xff2222, 1.0)
|
|
|
|
|
|
cross.fillCircle(w / 2, h / 2, 2)
|
|
|
|
|
|
this.hudObjects.push(cross)
|
|
|
|
|
|
|
|
|
|
|
|
// Log text overlay
|
2026-03-21 11:16:39 +00:00
|
|
|
|
this.logText = this.add.text(10, 10, '', {
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
color: '#e8e8e8',
|
|
|
|
|
|
backgroundColor: '#000000bb',
|
|
|
|
|
|
padding: { x: 10, y: 8 },
|
|
|
|
|
|
lineSpacing: 3,
|
|
|
|
|
|
fontFamily: 'monospace',
|
2026-03-21 11:34:04 +00:00
|
|
|
|
}).setDepth(100)
|
|
|
|
|
|
this.hudObjects.push(this.logText)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Adds a dedicated HUD camera (zoom=1, no scroll) and separates
|
|
|
|
|
|
* world objects from HUD objects so neither camera renders both layers.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private setupCameras(): void {
|
|
|
|
|
|
this.hudCamera = this.cameras.add(0, 0, this.scale.width, this.scale.height)
|
|
|
|
|
|
this.hudCamera.setScroll(0, 0)
|
|
|
|
|
|
this.hudCamera.setZoom(1)
|
|
|
|
|
|
|
|
|
|
|
|
this.cameras.main.ignore(this.hudObjects)
|
|
|
|
|
|
this.hudCamera.ignore(this.worldObjects)
|
2026-03-21 11:16:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
update(_time: number, delta: number): void {
|
|
|
|
|
|
this.handleKeyboard(delta)
|
|
|
|
|
|
this.updateOverlay()
|
2026-03-21 11:19:54 +00:00
|
|
|
|
|
|
|
|
|
|
this.snapshotTimer += delta
|
|
|
|
|
|
if (this.snapshotTimer >= SNAPSHOT_EVERY) {
|
|
|
|
|
|
this.snapshotTimer = 0
|
|
|
|
|
|
this.writeSnapshot()
|
|
|
|
|
|
}
|
2026-03-21 11:16:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Moves camera with WASD / arrow keys at CAMERA_SPEED px/s (world space).
|
|
|
|
|
|
* @param delta - Frame delta in milliseconds
|
|
|
|
|
|
*/
|
|
|
|
|
|
private handleKeyboard(delta: number): void {
|
2026-03-21 11:34:04 +00:00
|
|
|
|
const cam = this.cameras.main
|
|
|
|
|
|
const speed = CAMERA_SPEED * (delta / 1000) / cam.zoom
|
2026-03-21 11:16:39 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-03-21 11:34:04 +00:00
|
|
|
|
const vpWidthPx = cam.width / cam.zoom
|
|
|
|
|
|
const vpHeightPx = cam.height / cam.zoom
|
2026-03-21 11:16:39 +00:00
|
|
|
|
const vpWidthTiles = vpWidthPx / TILE_SIZE
|
|
|
|
|
|
const vpHeightTiles = vpHeightPx / TILE_SIZE
|
2026-03-21 11:34:04 +00:00
|
|
|
|
const centerWorldX = cam.scrollX + vpWidthPx / 2
|
|
|
|
|
|
const centerWorldY = cam.scrollY + vpHeightPx / 2
|
|
|
|
|
|
const mouseTileX = Math.floor(ptr.worldX / TILE_SIZE)
|
|
|
|
|
|
const mouseTileY = Math.floor(ptr.worldY / TILE_SIZE)
|
|
|
|
|
|
const centerTileX = Math.floor(centerWorldX / TILE_SIZE)
|
|
|
|
|
|
const centerTileY = Math.floor(centerWorldY / TILE_SIZE)
|
|
|
|
|
|
const renderer = this.game.renderer.type === Phaser.WEBGL ? 'WebGL' : 'Canvas'
|
2026-03-21 11:16:39 +00:00
|
|
|
|
|
|
|
|
|
|
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<string, unknown>)['roundPixels']}`,
|
|
|
|
|
|
`Renderer: ${renderer}`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'[Scroll] Zoom [WASD / ↑↓←→] Pan',
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
this.logText.setText(lines)
|
|
|
|
|
|
}
|
2026-03-21 11:19:54 +00:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Writes a periodic full-state snapshot to the log.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private writeSnapshot(): void {
|
|
|
|
|
|
const cam = this.cameras.main
|
|
|
|
|
|
const ptr = this.input.activePointer
|
|
|
|
|
|
const vpW = cam.width / cam.zoom
|
|
|
|
|
|
const vpH = cam.height / cam.zoom
|
|
|
|
|
|
|
|
|
|
|
|
this.writeLog('snapshot', {
|
|
|
|
|
|
zoom: +cam.zoom.toFixed(4),
|
|
|
|
|
|
scrollX: +cam.scrollX.toFixed(2),
|
|
|
|
|
|
scrollY: +cam.scrollY.toFixed(2),
|
|
|
|
|
|
vpScreen: { w: cam.width, h: cam.height },
|
|
|
|
|
|
vpWorld: { w: +vpW.toFixed(2), h: +vpH.toFixed(2) },
|
|
|
|
|
|
vpTiles: { w: +((vpW / TILE_SIZE).toFixed(3)), h: +((vpH / TILE_SIZE).toFixed(3)) },
|
|
|
|
|
|
centerWorld: {
|
|
|
|
|
|
x: +(cam.scrollX + vpW / 2).toFixed(2),
|
|
|
|
|
|
y: +(cam.scrollY + vpH / 2).toFixed(2),
|
|
|
|
|
|
},
|
|
|
|
|
|
mouse: {
|
|
|
|
|
|
screen: { x: +ptr.x.toFixed(1), y: +ptr.y.toFixed(1) },
|
|
|
|
|
|
world: { x: +ptr.worldX.toFixed(2), y: +ptr.worldY.toFixed(2) },
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* POSTs a structured log entry to the Vite dev server middleware.
|
|
|
|
|
|
* Written to game-test.log in the project root.
|
|
|
|
|
|
* @param event - Event type label
|
|
|
|
|
|
* @param data - Payload object to serialize as JSON
|
|
|
|
|
|
*/
|
|
|
|
|
|
private writeLog(event: string, data: Record<string, unknown>): void {
|
|
|
|
|
|
const entry = JSON.stringify({ t: Date.now(), event, ...data })
|
|
|
|
|
|
fetch('/api/log', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
|
|
|
|
body: entry,
|
|
|
|
|
|
}).catch(() => { /* swallow if dev server not running */ })
|
|
|
|
|
|
}
|
2026-03-21 11:16:39 +00:00
|
|
|
|
}
|