// Speed and fire-rate constants per alien type const SPEED = { basic: 130, spread: 155, missile: 110 }; const FIRE_RATE = { basic: 2500, spread: 3200, missile: 4000 }; // Explosion ring/flash color per type const EXPLODE_COLOR = { basic: 0x00ffff, spread: 0xff4400, missile: 0xcc00ff }; export default class AlienShip { /** * @param {Phaser.Scene} scene * @param {number} x * @param {number} y * @param {Phaser.Physics.Arcade.Group} group * @param {'basic'|'spread'|'missile'} type */ constructor(scene, x, y, group, type = 'basic') { this.scene = scene; this.alive = true; this.type = type; // Select texture by type const texKey = type === 'spread' ? 'alien_spread' : type === 'missile' ? 'alien_missile' : 'alien'; this.sprite = scene.physics.add.sprite(x, y, texKey); group.add(this.sprite); this.sprite.gameEntity = this; this.sprite.body.setAllowGravity(false); // Circular hitbox — missile alien is slightly larger if (type === 'missile') { this.sprite.body.setCircle(22, 18, 8); } else if (type === 'spread') { this.sprite.body.setCircle(20, 16, 7); } else { this.sprite.body.setCircle(20, 12, 4); } this.lastFired = 0; } update(time, player) { if (!this.alive) return; if (!player || !player.alive || !player.sprite.active) return; // Move toward player const dx = player.sprite.x - this.sprite.x; const dy = player.sprite.y - this.sprite.y; const dist = Math.sqrt(dx * dx + dy * dy); const speed = SPEED[this.type]; if (dist > 5) { this.sprite.setVelocity( (dx / dist) * speed, (dy / dist) * speed ); } // Fire periodically if (time - this.lastFired > FIRE_RATE[this.type]) { this.lastFired = time; const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx)); if (this.type === 'spread') { this._fireSpread(angle); } else if (this.type === 'missile') { this.scene.spawnHomingMissile(this.sprite.x, this.sprite.y, this); } else { this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this); } } // Wrap around screen this.scene.physics.world.wrap(this.sprite, 40); } _fireSpread(centerAngle) { // 5 bullets fanning out ±40° around the target direction [-40, -20, 0, 20, 40].forEach(offset => { this.scene.spawnAlienBullet( this.sprite.x, this.sprite.y, centerAngle + offset, this ); }); } warpOut() { if (!this.alive) return; this.alive = false; this.sprite.body.setVelocity(0, 0); const scene = this.scene; const x = this.sprite.x; const y = this.sprite.y; // Spin and shrink the sprite into the singularity scene.tweens.add({ targets: this.sprite, scaleX: 0, scaleY: 0, angle: this.sprite.angle + 720, duration: 500, ease: 'Cubic.In', onComplete: () => { if (this.sprite && this.sprite.active) this.sprite.destroy(); } }); // Black-hole ring effect const gfx = scene.add.graphics(); const counter = { t: 0 }; scene.tweens.add({ targets: counter, t: 1, duration: 650, onUpdate: () => { const t = counter.t; gfx.clear(); const r = t < 0.3 ? (t / 0.3) * 38 : ((1 - t) / 0.7) * 38; if (r < 1) return; const a = Math.max(0, 1 - t * 0.7); gfx.fillStyle(0x000000, a); gfx.fillCircle(x, y, r * 0.55); gfx.lineStyle(3, 0x00ffff, a); gfx.strokeCircle(x, y, r); gfx.lineStyle(1, 0xffffff, a * 0.6); gfx.strokeCircle(x, y, r * 0.55); const off = t * Math.PI * 6; for (let i = 0; i < 6; i++) { const ang = (i / 6) * Math.PI * 2 + off; gfx.lineStyle(1, 0x88ffff, a * 0.7); gfx.beginPath(); gfx.moveTo(x + Math.cos(ang) * r * 0.55, y + Math.sin(ang) * r * 0.55); gfx.lineTo(x + Math.cos(ang) * r, y + Math.sin(ang) * r); gfx.strokePath(); } }, onComplete: () => gfx.destroy() }); } explode() { if (!this.alive) return; this.alive = false; this.sprite.body.setVelocity(0, 0); const scene = this.scene; const x = this.sprite.x; const y = this.sprite.y; const color = EXPLODE_COLOR[this.type]; // Brief flash before implosion this.sprite.setTint(color); scene.time.delayedCall(60, () => { if (this.sprite && this.sprite.active) this.sprite.clearTint(); }); // Phase 1: sprite collapses and spins scene.tweens.add({ targets: this.sprite, scaleX: 0, scaleY: 0, angle: this.sprite.angle + 360, duration: 400, ease: 'Cubic.In', onComplete: () => { if (this.sprite && this.sprite.active) this.sprite.destroy(); } }); // Phase 2: expanding shockwave ring scene.time.delayedCall(200, () => { const gfx = scene.add.graphics(); gfx.setDepth(5); const counter = { t: 0 }; scene.tweens.add({ targets: counter, t: 1, duration: 500, onUpdate: () => { const t = counter.t; gfx.clear(); const radius = t * 120; if (radius < 1) return; gfx.lineStyle(3, color, Math.max(0, 1 - t)); gfx.strokeCircle(x, y, radius); }, onComplete: () => gfx.destroy() }); }); } destroy() { if (!this.alive) return; this.alive = false; if (this.sprite && this.sprite.active) { this.sprite.destroy(); } } }