const PLAYER_BULLET_SPEED = 700; // pixels/sec const ALIEN_BULLET_SPEED = 350; // pixels/sec const MISSILE_SPEED = 180; // pixels/sec — slow enough for the player to outrun const MISSILE_TURN_RATE = 120; // degrees/sec max steering const BULLET_LIFETIME = 1800; // ms const MISSILE_LIFETIME = 5000; // ms export default class Bullet { constructor(scene, x, y, angle, owner, group, ownerEntity = null) { this.scene = scene; this.owner = owner; // 'player', 'alien', or 'missile' this.ownerEntity = ownerEntity; this.alive = true; this.isHoming = (owner === 'missile'); const textureKey = owner === 'player' ? 'bullet' : owner === 'missile' ? 'homing_missile' : 'alien_bullet'; this.sprite = scene.physics.add.sprite(x, y, textureKey); group.add(this.sprite); this.sprite.gameEntity = this; this.sprite.body.setAllowGravity(false); const speed = owner === 'player' ? PLAYER_BULLET_SPEED : owner === 'missile' ? MISSILE_SPEED : ALIEN_BULLET_SPEED; const rad = Phaser.Math.DegToRad(angle); this.sprite.setVelocity(Math.cos(rad) * speed, Math.sin(rad) * speed); // Track current heading for homing missiles; rotate sprite to face direction of travel this.currentAngleDeg = angle; if (this.isHoming) { this.sprite.setAngle(angle); } this.createdAt = scene.time.now; this.lifetime = this.isHoming ? MISSILE_LIFETIME : BULLET_LIFETIME; } update(time) { if (!this.alive) return false; // Expire by lifetime if (time - this.createdAt > this.lifetime) { this.destroy(); return false; } // Expire if far off-screen const { x, y } = this.sprite; if (x < -80 || x > 1680 || y < -80 || y > 980) { this.destroy(); return false; } // Homing steering — updates velocity and rotation every frame if (this.isHoming) { this._steerTowardPlayer(); } return true; } _steerTowardPlayer() { const player = this.scene.player; if (!player || !player.alive || !player.sprite.active) return; // Cap dt to avoid huge first-frame jumps const dt = Math.min(this.scene.game.loop.delta / 1000, 0.1); const targetAngle = Phaser.Math.RadToDeg( Math.atan2( player.sprite.y - this.sprite.y, player.sprite.x - this.sprite.x ) ); // Rotate toward target at limited turn rate const maxTurn = MISSILE_TURN_RATE * dt; const diff = Phaser.Math.Angle.ShortestBetween(this.currentAngleDeg, targetAngle); this.currentAngleDeg += Phaser.Math.Clamp(diff, -maxTurn, maxTurn); // Apply updated velocity and rotate sprite to face direction of travel const rad = Phaser.Math.DegToRad(this.currentAngleDeg); this.sprite.setVelocity(Math.cos(rad) * MISSILE_SPEED, Math.sin(rad) * MISSILE_SPEED); this.sprite.setAngle(this.currentAngleDeg); } destroy() { if (!this.alive) return; this.alive = false; if (this.sprite && this.sprite.active) { this.sprite.destroy(); } } }