Add combat diagnostics and testable hit reducer

This commit is contained in:
Admin 2026-09-18 00:33:55 +00:00
parent f03ff1e0cd
commit 8941fe662c

View File

@ -2,6 +2,8 @@ import Phaser from 'phaser';
import './style.css'; import './style.css';
import airshipSheetUrl from './assets/airships.svg?url'; import airshipSheetUrl from './assets/airships.svg?url';
import explosionSheetUrl from './assets/explosions.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 WORLD_SIZE = 2400;
const PLAYER_MAX_SPEED = 180; const PLAYER_MAX_SPEED = 180;
@ -46,6 +48,11 @@ class GameScene extends Phaser.Scene {
this.pendingPlayerHits = []; this.pendingPlayerHits = [];
this.wave = 1; this.wave = 1;
this.defeated = 0; this.defeated = 0;
airshipLog('scene:init', {
hull: this.playerHull,
wave: this.wave,
});
} }
preload() { 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.playerShots, this.pirates, this.hitPirate, null, this);
this.physics.add.overlap(this.pirateShots, this.player, this.hitPlayer, 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.cursors = this.input.keyboard.createCursorKeys();
this.keys = this.input.keyboard.addKeys({ this.keys = this.input.keyboard.addKeys({
firePort: Phaser.Input.Keyboard.KeyCodes.Q, firePort: Phaser.Input.Keyboard.KeyCodes.Q,
@ -96,6 +130,13 @@ class GameScene extends Phaser.Scene {
this.scale.on('resize', this.layoutHud, this); this.scale.on('resize', this.layoutHud, this);
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => { this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
this.scale.off('resize', this.layoutHud, this); 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.layoutHud({ width: this.scale.width, height: this.scale.height });
this.spawnWave(); this.spawnWave();
@ -216,6 +257,7 @@ class GameScene extends Phaser.Scene {
spawnWave() { spawnWave() {
const count = Math.min(this.wave, MAX_PIRATES); 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'}`); this.status.setText(`WAVE ${this.wave}\n${count} PIRATE${count === 1 ? '' : 'S'}`);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@ -387,7 +429,17 @@ class GameScene extends Phaser.Scene {
fireBroadside(ship, side, group, texture, damage, velocityScale) { fireBroadside(ship, side, group, texture, damage, velocityScale) {
if (!ship?.body) return; if (!ship?.body) return;
const actor = group === this.pirateShots ? 'pirate' : 'player';
const base = headingRad(ship) + side * Math.PI / 2; 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]; const offsets = [-7, 0, 7];
for (const offset of offsets) { for (const offset of offsets) {
@ -413,9 +465,16 @@ class GameScene extends Phaser.Scene {
shot.destroy(); shot.destroy();
this.spawnExplosion(x, y, 0.32); 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); pirate.setData('hull', hull);
airshipLog('combat:pirate-hit', {
damage,
hullBefore: previousHull,
hullAfter: hull,
});
if (hull <= 0) { if (hull <= 0) {
this.spawnExplosion(pirate.x, pirate.y, 1.25); this.spawnExplosion(pirate.x, pirate.y, 1.25);
pirate.destroy(); pirate.destroy();
@ -439,13 +498,19 @@ class GameScene extends Phaser.Scene {
if (!shot?.active || this.gameState !== 'playing') return; if (!shot?.active || this.gameState !== 'playing') return;
if (shot.getData('playerHitQueued')) return; if (shot.getData('playerHitQueued')) return;
const rawDamage = Number(shot.getData('damage')); const damage = normalizePlayerDamage(shot.getData('damage'), PIRATE_SHOT_DAMAGE);
const damage = clamp(Number.isFinite(rawDamage) ? rawDamage : PIRATE_SHOT_DAMAGE, 1, 25);
// The Arcade overlap callback only records the collision. Object removal // The Arcade overlap callback only records the collision. Object removal
// and hull mutation happen from the scene update after the physics step. // and hull mutation happen from the scene update after the physics step.
shot.setData('playerHitQueued', true); shot.setData('playerHitQueued', true);
this.pendingPlayerHits.push({ shot, damage }); 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() { processPlayerHits() {
@ -454,20 +519,35 @@ class GameScene extends Phaser.Scene {
const hits = this.pendingPlayerHits; const hits = this.pendingPlayerHits;
this.pendingPlayerHits = []; this.pendingPlayerHits = [];
for (const { shot, damage } of hits) { for (const { shot } of hits) {
if (shot?.active) shot.destroy(); 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) { spawnExplosion(x, y, scale = 1) {
@ -575,6 +655,14 @@ class GameScene extends Phaser.Scene {
finishGame() { finishGame() {
if (this.gameState !== 'playing') return; 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.gameState = 'lost';
this.player.setVelocity(0, 0); this.player.setVelocity(0, 0);
this.playerShots.clear(true, true); this.playerShots.clear(true, true);