Add pirate classes separation and control tuning

This commit is contained in:
Admin 2026-09-18 05:45:32 +00:00
parent f85dfe4798
commit d5cd19cdb1

View File

@ -4,19 +4,22 @@ 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 { airshipLog, registerDebugSnapshot } from './diagnostics.js';
import { normalizePlayerDamage, reducePlayerProjectileHits } from './game-logic.js'; import { normalizePlayerDamage, reducePlayerProjectileHits } from './game-logic.js';
import { pirateClassForWave, separationVector } from './pirate-ai.js';
const WORLD_SIZE = 2400; const WORLD_SIZE = 2400;
const PLAYER_MAX_SPEED = 180; const PLAYER_MAX_SPEED = 180;
const PLAYER_REVERSE_SPEED = -55; const PLAYER_REVERSE_SPEED = -55;
const PLAYER_ACCELERATION = 85; const PLAYER_ACCELERATION = 92;
const PLAYER_BRAKE = 110; const PLAYER_BRAKE = 118;
const PLAYER_TURN_RATE = 72; const PLAYER_TURN_RATE = 76;
const PIRATE_SPEED = 118; const PLAYER_COAST_DRAG = 0.992;
const PIRATE_TURN_RATE = 52; const HELM_DEAD_ZONE = 0.1;
const HELM_STEER_CURVE = 1.18;
const PLAYER_BROADSIDE_RELOAD_MS = 850;
const SHOT_SPEED = 520; const SHOT_SPEED = 520;
const SHOT_LIFETIME = 900; const SHOT_LIFETIME = 900;
const PLAYER_MAX_HULL = 100; const PLAYER_MAX_HULL = 100;
const PIRATE_MAX_HULL = 120; const PIRATE_FALLBACK_MAX_HULL = 120;
const PIRATE_SHOT_DAMAGE = 6; const PIRATE_SHOT_DAMAGE = 6;
const MAX_PIRATES = 6; const MAX_PIRATES = 6;
const AUTO_RESET_MS = 2200; const AUTO_RESET_MS = 2200;
@ -42,7 +45,8 @@ class GameScene extends Phaser.Scene {
this.playerSpeed = 0; this.playerSpeed = 0;
this.playerHull = PLAYER_MAX_HULL; this.playerHull = PLAYER_MAX_HULL;
this.gameState = 'playing'; this.gameState = 'playing';
this.lastPlayerShot = { port: 0, starboard: 0 }; this.lastPlayerShot = { port: -Infinity, starboard: -Infinity };
this.playerBroadsideReloadMs = PLAYER_BROADSIDE_RELOAD_MS;
this.helmVector = { x: 0, y: 0 }; this.helmVector = { x: 0, y: 0 };
this.pendingPlayerHits = []; this.pendingPlayerHits = [];
this.lastDiagnosticsAt = -Infinity; this.lastDiagnosticsAt = -Infinity;
@ -271,7 +275,7 @@ class GameScene extends Phaser.Scene {
const radius = Phaser.Math.Between(500, 760); const radius = Phaser.Math.Between(500, 760);
const x = clamp(this.player.x + Math.cos(angle) * radius, 120, WORLD_SIZE - 120); const x = clamp(this.player.x + Math.cos(angle) * radius, 120, WORLD_SIZE - 120);
const y = clamp(this.player.y + Math.sin(angle) * radius, 120, WORLD_SIZE - 120); const y = clamp(this.player.y + Math.sin(angle) * radius, 120, WORLD_SIZE - 120);
this.spawnPirate(x, y, angle); this.spawnPirate(x, y, angle, pirateClassForWave(this.wave, i, count));
} }
this.time.delayedCall(900, () => { this.time.delayedCall(900, () => {
@ -279,13 +283,24 @@ class GameScene extends Phaser.Scene {
}); });
} }
spawnPirate(x, y, angle) { spawnPirate(x, y, angle, pirateClass) {
const pirate = this.pirates.create(x, y, 'airships', 4); const pirate = this.pirates.create(x, y, 'airships', 4);
pirate.setDepth(6).setCollideWorldBounds(true).setDisplaySize(78, 78); pirate.setDepth(6).setCollideWorldBounds(true).setDisplaySize(pirateClass.displaySize, pirateClass.displaySize);
pirate.body.setAllowGravity(false); pirate.body.setAllowGravity(false);
pirate.body.setSize(46, 58, true); pirate.body.setSize(46, 58, true);
pirate.angle = normalizeDegrees(Phaser.Math.RadToDeg(angle) + 90); pirate.angle = normalizeDegrees(Phaser.Math.RadToDeg(angle) + 90);
pirate.setData('hull', PIRATE_MAX_HULL); pirate.setData('class', pirateClass.key);
pirate.setData('classLabel', pirateClass.label);
pirate.setData('hull', pirateClass.hull);
pirate.setData('maxHull', pirateClass.hull);
pirate.setData('speed', pirateClass.speed);
pirate.setData('turnRate', pirateClass.turnRate);
pirate.setData('reloadMs', pirateClass.reloadMs);
pirate.setData('damage', pirateClass.damage);
pirate.setData('preferredRange', pirateClass.preferredRange);
pirate.setData('retreatRange', pirateClass.retreatRange);
pirate.setData('broadsideRange', pirateClass.broadsideRange);
pirate.setData('broadsideError', pirateClass.broadsideError);
pirate.setData('lastShot', this.time.now + Phaser.Math.Between(-700, 300)); pirate.setData('lastShot', this.time.now + Phaser.Math.Between(-700, 300));
pirate.play('pirate-idle'); pirate.play('pirate-idle');
return pirate; return pirate;
@ -399,8 +414,9 @@ class GameScene extends Phaser.Scene {
updatePlayer(dt, deltaMs) { updatePlayer(dt, deltaMs) {
if (!this.player?.body) return; if (!this.player?.body) return;
const helmX = Math.abs(this.helmVector.x) > 0.08 ? this.helmVector.x : 0; const rawHelmX = Math.abs(this.helmVector.x) > HELM_DEAD_ZONE ? this.helmVector.x : 0;
const helmY = Math.abs(this.helmVector.y) > 0.08 ? this.helmVector.y : 0; const helmX = Math.sign(rawHelmX) * Math.pow(Math.abs(rawHelmX), HELM_STEER_CURVE);
const helmY = Math.abs(this.helmVector.y) > HELM_DEAD_ZONE ? this.helmVector.y : 0;
const throttleForward = this.cursors.up.isDown ? 1 : Math.max(0, -helmY); const throttleForward = this.cursors.up.isDown ? 1 : Math.max(0, -helmY);
const throttleReverse = this.cursors.down.isDown ? 1 : Math.max(0, helmY); const throttleReverse = this.cursors.down.isDown ? 1 : Math.max(0, helmY);
@ -409,7 +425,7 @@ class GameScene extends Phaser.Scene {
if (this.keys.stop.isDown) this.playerSpeed *= Math.pow(0.025, dt); if (this.keys.stop.isDown) this.playerSpeed *= Math.pow(0.025, dt);
if (throttleForward === 0 && throttleReverse === 0 && !this.keys.stop.isDown) { if (throttleForward === 0 && throttleReverse === 0 && !this.keys.stop.isDown) {
this.playerSpeed *= Math.pow(0.985, deltaMs / 16.667); this.playerSpeed *= Math.pow(PLAYER_COAST_DRAG, deltaMs / 16.667);
} }
this.playerSpeed = clamp(this.playerSpeed, PLAYER_REVERSE_SPEED, PLAYER_MAX_SPEED); this.playerSpeed = clamp(this.playerSpeed, PLAYER_REVERSE_SPEED, PLAYER_MAX_SPEED);
@ -428,34 +444,57 @@ class GameScene extends Phaser.Scene {
} }
updatePirates(dt) { updatePirates(dt) {
for (const pirate of this.pirates.getChildren()) { const pirates = this.pirates.getChildren();
for (const pirate of pirates) {
if (!pirate.active || !pirate.body) continue; if (!pirate.active || !pirate.body) continue;
const dx = this.player.x - pirate.x; const dx = this.player.x - pirate.x;
const dy = this.player.y - pirate.y; const dy = this.player.y - pirate.y;
const distance = Math.hypot(dx, dy); const distance = Math.hypot(dx, dy);
const bearing = bearingTo(pirate, this.player); const bearing = bearingTo(pirate, this.player);
const preferredRange = pirate.getData('preferredRange') || 335;
const retreatRange = pirate.getData('retreatRange') || 210;
const turnRate = pirate.getData('turnRate') || 52;
const baseSpeed = pirate.getData('speed') || 118;
let desiredHeading = bearing; let desiredHeading = bearing;
if (distance < 360) { if (distance < preferredRange + 40) {
const optionA = normalizeDegrees(bearing + 90); const optionA = normalizeDegrees(bearing + 90);
const optionB = normalizeDegrees(bearing - 90); const optionB = normalizeDegrees(bearing - 90);
desiredHeading = Math.abs(signedAngleDelta(optionA, pirate.angle)) < Math.abs(signedAngleDelta(optionB, pirate.angle)) ? optionA : optionB; desiredHeading = Math.abs(signedAngleDelta(optionA, pirate.angle)) < Math.abs(signedAngleDelta(optionB, pirate.angle)) ? optionA : optionB;
} }
const delta = signedAngleDelta(desiredHeading, pirate.angle); const separation = separationVector(pirate, pirates);
pirate.angle += clamp(delta, -PIRATE_TURN_RATE * dt, PIRATE_TURN_RATE * dt); if (separation.x || separation.y) {
const desiredRad = degToRad(desiredHeading - 90);
const steerX = Math.cos(desiredRad) + separation.x * 1.15;
const steerY = Math.sin(desiredRad) + separation.y * 1.15;
desiredHeading = normalizeDegrees(Math.atan2(steerY, steerX) * 180 / Math.PI + 90);
}
const speed = distance > 430 ? PIRATE_SPEED : distance < 210 ? 72 : 96; const delta = signedAngleDelta(desiredHeading, pirate.angle);
pirate.angle += clamp(delta, -turnRate * dt, turnRate * dt);
const speed = distance > preferredRange + 100
? baseSpeed
: distance < retreatRange
? baseSpeed * 0.58
: baseSpeed * 0.82;
const h = headingRad(pirate); const h = headingRad(pirate);
pirate.setVelocity(Math.cos(h) * speed, Math.sin(h) * speed); pirate.setVelocity(Math.cos(h) * speed, Math.sin(h) * speed);
const relative = signedAngleDelta(bearing, pirate.angle); const relative = signedAngleDelta(bearing, pirate.angle);
const broadsideError = Math.abs(Math.abs(relative) - 90); const broadsideError = Math.abs(Math.abs(relative) - 90);
const lastShot = pirate.getData('lastShot') || 0; const lastShot = pirate.getData('lastShot') || 0;
if (distance < 420 && broadsideError < 38 && this.time.now - lastShot > 1650) { const broadsideRange = pirate.getData('broadsideRange') || 420;
const broadsideTolerance = pirate.getData('broadsideError') || 38;
const reloadMs = pirate.getData('reloadMs') || 1650;
const damage = pirate.getData('damage') || PIRATE_SHOT_DAMAGE;
if (distance < broadsideRange && broadsideError < broadsideTolerance && this.time.now - lastShot > reloadMs) {
const side = relative >= 0 ? 1 : -1; const side = relative >= 0 ? 1 : -1;
this.fireBroadside(pirate, side, this.pirateShots, 'pirate-shot', PIRATE_SHOT_DAMAGE, 0.9); this.fireBroadside(pirate, side, this.pirateShots, 'pirate-shot', damage, 0.9);
pirate.setData('lastShot', this.time.now); pirate.setData('lastShot', this.time.now);
} }
} }
@ -463,7 +502,7 @@ class GameScene extends Phaser.Scene {
playerBroadside(side) { playerBroadside(side) {
const broadside = side < 0 ? 'port' : 'starboard'; const broadside = side < 0 ? 'port' : 'starboard';
if (this.time.now - this.lastPlayerShot[broadside] < 850) return; if (this.time.now - this.lastPlayerShot[broadside] < PLAYER_BROADSIDE_RELOAD_MS) return;
this.fireBroadside(this.player, side, this.playerShots, 'player-shot', 16, 1.0); this.fireBroadside(this.player, side, this.playerShots, 'player-shot', 16, 1.0);
this.lastPlayerShot[broadside] = this.time.now; this.lastPlayerShot[broadside] = this.time.now;
} }
@ -506,7 +545,7 @@ class GameScene extends Phaser.Scene {
shot.destroy(); shot.destroy();
this.spawnExplosion(x, y, 0.32); this.spawnExplosion(x, y, 0.32);
const previousHull = pirate.getData('hull') || PIRATE_MAX_HULL; const previousHull = pirate.getData('hull') || pirate.getData('maxHull') || PIRATE_FALLBACK_MAX_HULL;
const hull = Math.max(0, previousHull - damage); const hull = Math.max(0, previousHull - damage);
pirate.setData('hull', hull); pirate.setData('hull', hull);
@ -685,6 +724,8 @@ class GameScene extends Phaser.Scene {
const activePirates = this.pirates?.countActive(true) || 0; const activePirates = this.pirates?.countActive(true) || 0;
const { pirate: nearest, range } = this.nearestPirate(); const { pirate: nearest, range } = this.nearestPirate();
const nearestHull = nearest ? (nearest.getData('hull') || 0) : 0; const nearestHull = nearest ? (nearest.getData('hull') || 0) : 0;
const nearestMaxHull = nearest ? (nearest.getData('maxHull') || PIRATE_FALLBACK_MAX_HULL) : PIRATE_FALLBACK_MAX_HULL;
const nearestClass = nearest ? (nearest.getData('classLabel') || 'PIRATE') : 'PIRATE';
const rangeText = Number.isFinite(range) ? `${range.toFixed(0)}m` : '--'; const rangeText = Number.isFinite(range) ? `${range.toFixed(0)}m` : '--';
this.speedText.setText(`${Math.abs(this.playerSpeed).toFixed(0)} ${this.playerSpeed < 0 ? 'REV' : 'FWD'}`); this.speedText.setText(`${Math.abs(this.playerSpeed).toFixed(0)} ${this.playerSpeed < 0 ? 'REV' : 'FWD'}`);
@ -696,7 +737,7 @@ class GameScene extends Phaser.Scene {
this.playerHullLabel.setText(`SPARROW ${this.playerHull}/${PLAYER_MAX_HULL}`); this.playerHullLabel.setText(`SPARROW ${this.playerHull}/${PLAYER_MAX_HULL}`);
this.enemyHullLabel.setText(nearest this.enemyHullLabel.setText(nearest
? `PIRATE ${nearestHull}/${PIRATE_MAX_HULL}` ? `${nearestClass} ${nearestHull}/${nearestMaxHull}`
: `WAVE ${this.wave} CLEAR`); : `WAVE ${this.wave} CLEAR`);
const side = 14; const side = 14;