Add pirate classes separation and control tuning
This commit is contained in:
parent
f85dfe4798
commit
d5cd19cdb1
@ -4,19 +4,22 @@ import airshipSheetUrl from './assets/airships.svg?url';
|
||||
import explosionSheetUrl from './assets/explosions.svg?url';
|
||||
import { airshipLog, registerDebugSnapshot } from './diagnostics.js';
|
||||
import { normalizePlayerDamage, reducePlayerProjectileHits } from './game-logic.js';
|
||||
import { pirateClassForWave, separationVector } from './pirate-ai.js';
|
||||
|
||||
const WORLD_SIZE = 2400;
|
||||
const PLAYER_MAX_SPEED = 180;
|
||||
const PLAYER_REVERSE_SPEED = -55;
|
||||
const PLAYER_ACCELERATION = 85;
|
||||
const PLAYER_BRAKE = 110;
|
||||
const PLAYER_TURN_RATE = 72;
|
||||
const PIRATE_SPEED = 118;
|
||||
const PIRATE_TURN_RATE = 52;
|
||||
const PLAYER_ACCELERATION = 92;
|
||||
const PLAYER_BRAKE = 118;
|
||||
const PLAYER_TURN_RATE = 76;
|
||||
const PLAYER_COAST_DRAG = 0.992;
|
||||
const HELM_DEAD_ZONE = 0.1;
|
||||
const HELM_STEER_CURVE = 1.18;
|
||||
const PLAYER_BROADSIDE_RELOAD_MS = 850;
|
||||
const SHOT_SPEED = 520;
|
||||
const SHOT_LIFETIME = 900;
|
||||
const PLAYER_MAX_HULL = 100;
|
||||
const PIRATE_MAX_HULL = 120;
|
||||
const PIRATE_FALLBACK_MAX_HULL = 120;
|
||||
const PIRATE_SHOT_DAMAGE = 6;
|
||||
const MAX_PIRATES = 6;
|
||||
const AUTO_RESET_MS = 2200;
|
||||
@ -42,7 +45,8 @@ class GameScene extends Phaser.Scene {
|
||||
this.playerSpeed = 0;
|
||||
this.playerHull = PLAYER_MAX_HULL;
|
||||
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.pendingPlayerHits = [];
|
||||
this.lastDiagnosticsAt = -Infinity;
|
||||
@ -271,7 +275,7 @@ class GameScene extends Phaser.Scene {
|
||||
const radius = Phaser.Math.Between(500, 760);
|
||||
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);
|
||||
this.spawnPirate(x, y, angle);
|
||||
this.spawnPirate(x, y, angle, pirateClassForWave(this.wave, i, count));
|
||||
}
|
||||
|
||||
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);
|
||||
pirate.setDepth(6).setCollideWorldBounds(true).setDisplaySize(78, 78);
|
||||
pirate.setDepth(6).setCollideWorldBounds(true).setDisplaySize(pirateClass.displaySize, pirateClass.displaySize);
|
||||
pirate.body.setAllowGravity(false);
|
||||
pirate.body.setSize(46, 58, true);
|
||||
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.play('pirate-idle');
|
||||
return pirate;
|
||||
@ -399,8 +414,9 @@ class GameScene extends Phaser.Scene {
|
||||
updatePlayer(dt, deltaMs) {
|
||||
if (!this.player?.body) return;
|
||||
|
||||
const helmX = Math.abs(this.helmVector.x) > 0.08 ? this.helmVector.x : 0;
|
||||
const helmY = Math.abs(this.helmVector.y) > 0.08 ? this.helmVector.y : 0;
|
||||
const rawHelmX = Math.abs(this.helmVector.x) > HELM_DEAD_ZONE ? this.helmVector.x : 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 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 (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);
|
||||
@ -428,34 +444,57 @@ class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const dx = this.player.x - pirate.x;
|
||||
const dy = this.player.y - pirate.y;
|
||||
const distance = Math.hypot(dx, dy);
|
||||
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;
|
||||
if (distance < 360) {
|
||||
if (distance < preferredRange + 40) {
|
||||
const optionA = normalizeDegrees(bearing + 90);
|
||||
const optionB = normalizeDegrees(bearing - 90);
|
||||
desiredHeading = Math.abs(signedAngleDelta(optionA, pirate.angle)) < Math.abs(signedAngleDelta(optionB, pirate.angle)) ? optionA : optionB;
|
||||
}
|
||||
|
||||
const delta = signedAngleDelta(desiredHeading, pirate.angle);
|
||||
pirate.angle += clamp(delta, -PIRATE_TURN_RATE * dt, PIRATE_TURN_RATE * dt);
|
||||
const separation = separationVector(pirate, pirates);
|
||||
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);
|
||||
pirate.setVelocity(Math.cos(h) * speed, Math.sin(h) * speed);
|
||||
|
||||
const relative = signedAngleDelta(bearing, pirate.angle);
|
||||
const broadsideError = Math.abs(Math.abs(relative) - 90);
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -463,7 +502,7 @@ class GameScene extends Phaser.Scene {
|
||||
|
||||
playerBroadside(side) {
|
||||
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.lastPlayerShot[broadside] = this.time.now;
|
||||
}
|
||||
@ -506,7 +545,7 @@ class GameScene extends Phaser.Scene {
|
||||
shot.destroy();
|
||||
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);
|
||||
pirate.setData('hull', hull);
|
||||
|
||||
@ -685,6 +724,8 @@ class GameScene extends Phaser.Scene {
|
||||
const activePirates = this.pirates?.countActive(true) || 0;
|
||||
const { pirate: nearest, range } = this.nearestPirate();
|
||||
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` : '--';
|
||||
|
||||
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.enemyHullLabel.setText(nearest
|
||||
? `PIRATE ${nearestHull}/${PIRATE_MAX_HULL}`
|
||||
? `${nearestClass} ${nearestHull}/${nearestMaxHull}`
|
||||
: `WAVE ${this.wave} CLEAR`);
|
||||
|
||||
const side = 14;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user