160 lines
4.5 KiB
JavaScript
160 lines
4.5 KiB
JavaScript
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();
|
||
}
|
||
}
|