From d5cd19cdb1c83aab39020e0bbde5d12c5ff4b6d3 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 18 Sep 2026 05:45:32 +0000 Subject: [PATCH] Add pirate classes separation and control tuning --- src/game-v4.js | 89 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/src/game-v4.js b/src/game-v4.js index f8e55f5..aa72123 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -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;