overrun/js/entities/enemies/BaseEnemy.js

160 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const HP_BAR_WIDTH = 30;
const HP_BAR_HEIGHT = 4;
const HP_BAR_OFFSET_Y = -30;
const GRID = 80;
export class BaseEnemy {
constructor(scene, x, y, player, config) {
this.scene = scene;
this.player = player;
this.maxHp = config.hp;
this.hp = config.hp;
this.speed = config.speed;
this.xp = config.xp;
this.contactDamage = config.contactDamage ?? 10;
this.radius = config.radius ?? 14;
this.frameOffset = config.frameOffset ?? 0;
this._dying = false;
this._contactTimer = 0;
this._createAnims();
this._buildSprite(x, y);
this._buildHpBar(x, y);
}
_createAnims() {
const anims = this.scene.anims;
const key = `enemy-walk-${this.frameOffset}`;
if (!anims.exists(key)) {
anims.create({
key,
frames: anims.generateFrameNumbers('enemies', { frames: [this.frameOffset, this.frameOffset + 1] }),
frameRate: 6,
repeat: -1,
});
}
}
_buildSprite(x, y) {
this.sprite = this.scene.add.sprite(x, y, 'enemies', this.frameOffset).setDepth(5);
this.scene.physics.add.existing(this.sprite);
// Circle hitbox centered in the 48×48 frame
const offset = 24 - this.radius;
this.sprite.body.setCircle(this.radius, offset, offset);
this.sprite.play(`enemy-walk-${this.frameOffset}`);
}
_buildHpBar(x, y) {
this._hpBg = this.scene.add.rectangle(x, y + HP_BAR_OFFSET_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT, 0x333333);
this._hpFill = this.scene.add.rectangle(x, y + HP_BAR_OFFSET_Y, HP_BAR_WIDTH, HP_BAR_HEIGHT, 0x00ff44);
}
get x() { return this.sprite.x; }
get y() { return this.sprite.y; }
/** False while dying so WaveManager stops tracking and wave-clear can fire. */
get active() { return !this._dying && (this.sprite?.active ?? false); }
takeDamage(amount) {
if (this._dying) return;
this.hp -= amount;
this._updateHpBar();
if (this.hp <= 0) this._die();
}
_updateHpBar() {
const ratio = Math.max(0, this.hp / this.maxHp);
this._hpFill.width = HP_BAR_WIDTH * ratio;
this._hpFill.x = this.x - (HP_BAR_WIDTH * (1 - ratio)) / 2;
}
_syncBarPosition() {
this._hpBg.setPosition(this.x, this.y + HP_BAR_OFFSET_Y);
this._hpFill.setPosition(
this.x - (HP_BAR_WIDTH * (1 - Math.max(0, this.hp / this.maxHp))) / 2,
this.y + HP_BAR_OFFSET_Y
);
}
_die() {
this._dying = true;
this.scene.events.emit('enemy-killed', { xp: this.xp, x: this.x, y: this.y });
// Stop movement and hide HP bar immediately
this.sprite.body.setVelocity(0, 0);
this._hpBg.destroy();
this._hpFill.destroy();
this._hpBg = null;
this._hpFill = null;
// Show death frame
this.sprite.anims.stop();
this.sprite.setFrame(this.frameOffset + 2);
// After 2 seconds, fade out and destroy
this.scene.time.delayedCall(2000, () => {
if (!this.sprite?.active) return;
this.scene.tweens.add({
targets: this.sprite,
alpha: 0,
duration: 400,
onComplete: () => this.sprite?.destroy(),
});
});
}
_checkContact(delta) {
this._contactTimer -= delta;
if (this._contactTimer > 0) return;
const dist = Phaser.Math.Distance.Between(this.x, this.y, this.player.x, this.player.y);
if (dist < this.radius + 16) {
this.player.takeDamage(this.contactDamage);
this._contactTimer = 800;
}
}
update(delta) {
if (this._dying) return;
this._syncBarPosition();
this._checkContact(delta);
}
_gridDeathPulse() {
if (!this._deathPulseColor || !this.scene._pulseEffects) return;
const scene = this.scene;
const W = scene.scale.width;
const H = scene.scale.height;
const col = Math.floor(this.x / GRID);
const row = Math.floor(this.y / GRID);
const color = this._deathPulseColor;
const makeTile = (c, r, alpha) => {
if (c < 0 || r < 0 || c * GRID >= W || r * GRID >= H) return null;
return scene.add.rectangle(
c * GRID + GRID / 2, r * GRID + GRID / 2,
GRID, GRID, color, alpha,
).setDepth(2);
};
const center = makeTile(col, row, 0.55);
const adjacents = [];
[[-1, 0], [1, 0], [0, -1], [0, 1]].forEach(([dc, dr]) => {
const t = makeTile(col + dc, row + dr, 0.38);
if (t) { t.setVisible(false); adjacents.push(t); }
});
scene._pulseEffects.push({
center, adjacents, elapsed: 0, adjSpawned: false,
ADJ_MS: 80, FADE_MS: 120, END_MS: 400,
});
}
destroy() {
this.sprite?.destroy();
this._hpBg?.destroy();
this._hpFill?.destroy();
}
}