From 8941fe662c0fcbd982210b3c8b28e06f12a143cf Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 18 Sep 2026 00:33:55 +0000 Subject: [PATCH] Add combat diagnostics and testable hit reducer --- src/game-v4.js | 118 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 15 deletions(-) diff --git a/src/game-v4.js b/src/game-v4.js index c235ac7..af50ac6 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -2,6 +2,8 @@ import Phaser from 'phaser'; import './style.css'; import airshipSheetUrl from './assets/airships.svg?url'; import explosionSheetUrl from './assets/explosions.svg?url'; +import { airshipLog, registerDebugSnapshot } from './diagnostics.js'; +import { normalizePlayerDamage, reducePlayerHitQueue } from './game-logic.js'; const WORLD_SIZE = 2400; const PLAYER_MAX_SPEED = 180; @@ -46,6 +48,11 @@ class GameScene extends Phaser.Scene { this.pendingPlayerHits = []; this.wave = 1; this.defeated = 0; + + airshipLog('scene:init', { + hull: this.playerHull, + wave: this.wave, + }); } preload() { @@ -79,6 +86,33 @@ class GameScene extends Phaser.Scene { this.physics.add.overlap(this.playerShots, this.pirates, this.hitPirate, null, this); this.physics.add.overlap(this.pirateShots, this.player, this.hitPlayer, null, this); + registerDebugSnapshot(() => ({ + gameState: this.gameState, + playerHull: this.playerHull, + playerSpeed: this.playerSpeed, + pendingPlayerHits: this.pendingPlayerHits?.length || 0, + activePirates: this.pirates?.countActive?.(true) || 0, + playerShots: this.playerShots?.countActive?.(true) || 0, + pirateShots: this.pirateShots?.countActive?.(true) || 0, + wave: this.wave, + defeated: this.defeated, + now: this.time?.now ?? null, + player: this.player ? { + x: Math.round(this.player.x), + y: Math.round(this.player.y), + angle: Math.round(this.player.angle), + active: this.player.active, + visible: this.player.visible, + hasBody: Boolean(this.player.body), + } : null, + })); + + airshipLog('scene:create', { + hull: this.playerHull, + wave: this.wave, + worldSize: WORLD_SIZE, + }); + this.cursors = this.input.keyboard.createCursorKeys(); this.keys = this.input.keyboard.addKeys({ firePort: Phaser.Input.Keyboard.KeyCodes.Q, @@ -96,6 +130,13 @@ class GameScene extends Phaser.Scene { this.scale.on('resize', this.layoutHud, this); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { this.scale.off('resize', this.layoutHud, this); + registerDebugSnapshot(null); + airshipLog('scene:shutdown', { + gameState: this.gameState, + hull: this.playerHull, + wave: this.wave, + defeated: this.defeated, + }); }); this.layoutHud({ width: this.scale.width, height: this.scale.height }); this.spawnWave(); @@ -216,6 +257,7 @@ class GameScene extends Phaser.Scene { spawnWave() { const count = Math.min(this.wave, MAX_PIRATES); + airshipLog('wave:spawn', { wave: this.wave, count }); this.status.setText(`WAVE ${this.wave}\n${count} PIRATE${count === 1 ? '' : 'S'}`); for (let i = 0; i < count; i++) { @@ -387,7 +429,17 @@ class GameScene extends Phaser.Scene { fireBroadside(ship, side, group, texture, damage, velocityScale) { if (!ship?.body) return; + const actor = group === this.pirateShots ? 'pirate' : 'player'; const base = headingRad(ship) + side * Math.PI / 2; + + airshipLog('combat:broadside-fired', { + actor, + side: side < 0 ? 'port' : 'starboard', + damage, + x: Math.round(ship.x), + y: Math.round(ship.y), + angle: Math.round(ship.angle), + }, 'debug'); const offsets = [-7, 0, 7]; for (const offset of offsets) { @@ -413,9 +465,16 @@ class GameScene extends Phaser.Scene { shot.destroy(); this.spawnExplosion(x, y, 0.32); - const hull = Math.max(0, (pirate.getData('hull') || PIRATE_MAX_HULL) - damage); + const previousHull = pirate.getData('hull') || PIRATE_MAX_HULL; + const hull = Math.max(0, previousHull - damage); pirate.setData('hull', hull); + airshipLog('combat:pirate-hit', { + damage, + hullBefore: previousHull, + hullAfter: hull, + }); + if (hull <= 0) { this.spawnExplosion(pirate.x, pirate.y, 1.25); pirate.destroy(); @@ -439,13 +498,19 @@ class GameScene extends Phaser.Scene { if (!shot?.active || this.gameState !== 'playing') return; if (shot.getData('playerHitQueued')) return; - const rawDamage = Number(shot.getData('damage')); - const damage = clamp(Number.isFinite(rawDamage) ? rawDamage : PIRATE_SHOT_DAMAGE, 1, 25); + const damage = normalizePlayerDamage(shot.getData('damage'), PIRATE_SHOT_DAMAGE); // The Arcade overlap callback only records the collision. Object removal // and hull mutation happen from the scene update after the physics step. shot.setData('playerHitQueued', true); this.pendingPlayerHits.push({ shot, damage }); + + airshipLog('combat:player-hit-queued', { + damage, + pendingHits: this.pendingPlayerHits.length, + shotAgeMs: this.time.now - (shot.getData('bornAt') || this.time.now), + hull: this.playerHull, + }); } processPlayerHits() { @@ -454,20 +519,35 @@ class GameScene extends Phaser.Scene { const hits = this.pendingPlayerHits; this.pendingPlayerHits = []; - for (const { shot, damage } of hits) { + for (const { shot } of hits) { if (shot?.active) shot.destroy(); - - if (this.time.now - this.lastPlayerHitAt < PLAYER_HIT_COOLDOWN_MS) continue; - this.lastPlayerHitAt = this.time.now; - - const currentHull = Number.isFinite(this.playerHull) ? this.playerHull : PLAYER_MAX_HULL; - this.playerHull = Math.max(0, currentHull - damage); - - if (this.playerHull <= 0) { - this.finishGame(); - break; - } } + + const beforeHull = this.playerHull; + const result = reducePlayerHitQueue({ + hull: this.playerHull, + lastHitAt: this.lastPlayerHitAt, + now: this.time.now, + damages: hits.map(({ damage }) => damage), + cooldownMs: PLAYER_HIT_COOLDOWN_MS, + maxHull: PLAYER_MAX_HULL, + }); + + this.playerHull = result.hull; + this.lastPlayerHitAt = result.lastHitAt; + + airshipLog('combat:player-hit-processed', { + queuedHits: hits.length, + appliedHits: result.appliedHits, + ignoredHits: result.ignoredHits, + damage: result.totalDamage, + hullBefore: beforeHull, + hullAfter: this.playerHull, + gameOver: result.gameOver, + now: this.time.now, + }, result.gameOver ? 'warn' : 'info'); + + if (result.gameOver) this.finishGame(); } spawnExplosion(x, y, scale = 1) { @@ -575,6 +655,14 @@ class GameScene extends Phaser.Scene { finishGame() { if (this.gameState !== 'playing') return; + + airshipLog('game:finish', { + hull: this.playerHull, + wave: this.wave, + defeated: this.defeated, + pendingHits: this.pendingPlayerHits?.length || 0, + }, 'warn'); + this.gameState = 'lost'; this.player.setVelocity(0, 0); this.playerShots.clear(true, true);