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; if (this._deathSound) this.scene.sound.play(this._deathSound, { volume: 0.5 }); 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(); } }