158 lines
5.0 KiB
JavaScript
158 lines
5.0 KiB
JavaScript
import { BaseEnemy } from './BaseEnemy.js';
|
|
|
|
const EXPLODE_RADIUS = 180; // px — triggers proximity explosion
|
|
const PROXIMITY_SHARD_PX = 250; // travel distance for proximity explosion
|
|
const DEATH_SHARD_PX = 200; // travel distance for on-death explosion
|
|
const SHARD_COUNT = 8;
|
|
const SHARD_SPEED = 360; // px/s baseline (overridden per explosion type)
|
|
const SHARD_DAMAGE = 15;
|
|
const SHARD_DURATION = 700; // ms until shards disappear
|
|
const SHARD_HIT_RADIUS = 18; // px — how close a shard must be to damage player
|
|
const TICK_MS = 16;
|
|
|
|
export class BomberEnemy extends BaseEnemy {
|
|
constructor(scene, x, y, player) {
|
|
super(scene, x, y, player, {
|
|
frameOffset: 12,
|
|
radius: 10,
|
|
hp: 20,
|
|
speed: 130,
|
|
xp: 20,
|
|
contactDamage: 5, // low — explosion is the main attack
|
|
});
|
|
this._offsetAngle = Math.random() * Math.PI * 2;
|
|
this._wobble = 0.3 + Math.random() * 0.4;
|
|
this._exploded = false;
|
|
this._deathSound = 'sfx-death-bomber';
|
|
}
|
|
|
|
update(delta) {
|
|
super.update(delta);
|
|
if (this._dying) return;
|
|
|
|
// Swarm-like wobble movement
|
|
const t = this.scene.time.now * 0.001;
|
|
const baseAngle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
|
|
const angle = baseAngle + Math.sin(t * 4 + this._offsetAngle) * this._wobble;
|
|
this.sprite.body.setVelocity(Math.cos(angle) * this.speed, Math.sin(angle) * this.speed);
|
|
|
|
// Proximity check — explode when close enough
|
|
const dist = Phaser.Math.Distance.Between(this.x, this.y, this.player.x, this.player.y);
|
|
if (dist < EXPLODE_RADIUS) {
|
|
this._triggerExplosion();
|
|
}
|
|
}
|
|
|
|
_triggerExplosion() {
|
|
if (this._dying) return;
|
|
this._exploded = true;
|
|
this._spawnShards(PROXIMITY_SHARD_PX);
|
|
this._die();
|
|
}
|
|
|
|
// Override _die so a bullet-killed Bomber still explodes (smaller radius).
|
|
_die() {
|
|
if (!this._exploded) {
|
|
this._exploded = true;
|
|
this._spawnShards(DEATH_SHARD_PX);
|
|
}
|
|
super._die();
|
|
}
|
|
|
|
_spawnShards(travelPx) {
|
|
const shardSpeed = travelPx / (SHARD_DURATION / 1000);
|
|
const scene = this.scene;
|
|
const ox = this.x;
|
|
const oy = this.y;
|
|
const player = this.player;
|
|
const barriers = scene.barrierManager;
|
|
|
|
// Burst flash at explosion center
|
|
const flash = scene.add.circle(ox, oy, 36, 0xff6600, 0.9).setDepth(25);
|
|
scene.tweens.add({
|
|
targets: flash, alpha: 0, scaleX: 2.5, scaleY: 2.5,
|
|
duration: 320, onComplete: () => flash.destroy(),
|
|
});
|
|
const ring = scene.add.circle(ox, oy, 42, 0xffaa00, 0).setDepth(25)
|
|
.setStrokeStyle(3, 0xffaa00, 0.9);
|
|
scene.tweens.add({
|
|
targets: ring, scaleX: 2.2, scaleY: 2.2, alpha: 0,
|
|
duration: 400, onComplete: () => ring.destroy(),
|
|
});
|
|
|
|
scene.cameras.main.shake(180, 0.01);
|
|
|
|
// Build shards: each is a Phaser Triangle game object
|
|
const shards = [];
|
|
for (let i = 0; i < SHARD_COUNT; i++) {
|
|
const angle = (i / SHARD_COUNT) * Math.PI * 2;
|
|
// Triangle pointing "up" in local space: tip at top, base at bottom
|
|
const tri = scene.add.triangle(
|
|
ox, oy,
|
|
0, -10, // tip
|
|
7, 7, // base-right
|
|
-7, 7, // base-left
|
|
0xff4400,
|
|
).setDepth(22).setRotation(angle); // rotate to face travel direction
|
|
|
|
// Add a neon outline
|
|
tri.setStrokeStyle(1, 0xffaa00);
|
|
|
|
shards.push({
|
|
obj: tri,
|
|
vx: Math.cos(angle) * shardSpeed,
|
|
vy: Math.sin(angle) * shardSpeed,
|
|
rotSpeed: (Math.random() > 0.5 ? 1 : -1) * (6 + Math.random() * 6), // rad/s
|
|
alive: true,
|
|
});
|
|
}
|
|
|
|
// Manual update loop — avoids tween onComplete reliability issues
|
|
let elapsed = 0;
|
|
const playerDamaged = new Set();
|
|
|
|
const ticker = scene.time.addEvent({
|
|
delay: TICK_MS,
|
|
repeat: Math.ceil(SHARD_DURATION / TICK_MS),
|
|
callback: () => {
|
|
elapsed += TICK_MS;
|
|
const dt = TICK_MS / 1000;
|
|
const fade = Math.max(0, 1 - elapsed / SHARD_DURATION);
|
|
|
|
for (const shard of shards) {
|
|
if (!shard.alive) continue;
|
|
|
|
shard.obj.x += shard.vx * dt;
|
|
shard.obj.y += shard.vy * dt;
|
|
shard.obj.rotation += shard.rotSpeed * dt;
|
|
shard.obj.alpha = 0.4 + fade * 0.6;
|
|
|
|
// Player hit
|
|
if (!playerDamaged.has(shard)) {
|
|
const d = Phaser.Math.Distance.Between(shard.obj.x, shard.obj.y, player.x, player.y);
|
|
if (d < SHARD_HIT_RADIUS + 16) {
|
|
player.takeDamage(SHARD_DAMAGE);
|
|
playerDamaged.add(shard);
|
|
shard.alive = false;
|
|
shard.obj.destroy();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Interior barrier hit
|
|
if (barriers?.destroyInteriorBarrierAt(shard.obj.x, shard.obj.y)) {
|
|
shard.alive = false;
|
|
shard.obj.destroy();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (elapsed >= SHARD_DURATION) {
|
|
shards.forEach(s => { if (s.alive) s.obj.destroy(); });
|
|
ticker.remove();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
}
|