216 lines
6.5 KiB
JavaScript
216 lines
6.5 KiB
JavaScript
// 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();
|
|
}
|
|
}
|
|
}
|