overrun/js/entities/enemies/BomberEnemy.js

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();
}
},
});
}
}