Add combat diagnostics and testable hit reducer
This commit is contained in:
parent
f03ff1e0cd
commit
8941fe662c
118
src/game-v4.js
118
src/game-v4.js
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user