import type Phaser from 'phaser' import { GameManager } from '../GameManager' import { HUD_HEIGHT } from '../constants' import { getCellSize } from '../mapRenderer' import { ALL_MAPS } from '../data/mapConfigs' export interface PathPoint { x: number; y: number } function gridToPixel(gx: number, gy: number, cellW: number, cellH: number): PathPoint { return { x: gx * cellW + cellW / 2, y: gy * cellH + cellH / 2 + HUD_HEIGHT, } } /** * 将地图路径折线坐标转换为像素路径点序列 * @param waypoints 折线关键坐标点(默认使用地图1) */ export function buildFullPath( waypoints?: readonly { x: number; y: number }[] ): PathPoint[] { const { cellW, cellH } = getCellSize() const pts = waypoints ?? ALL_MAPS[0].waypoints const points: PathPoint[] = [] for (let i = 0; i < pts.length - 1; i++) { const from = gridToPixel(pts[i].x, pts[i].y, cellW, cellH) const to = gridToPixel(pts[i + 1].x, pts[i + 1].y, cellW, cellH) points.push(from) points.push(to) } return points.filter( (p, i, arr) => i === 0 || p.x !== arr[i - 1].x || p.y !== arr[i - 1].y ) } export interface DotEffect { damage: number; duration: number; timer: number } export abstract class EnemyBase { protected scene: Phaser.Scene protected imageSprite!: Phaser.GameObjects.Image public x: number = 0 public y: number = 0 protected healthBar!: Phaser.GameObjects.Graphics protected quoteText!: Phaser.GameObjects.Text public maxHp: number public hp: number public speed: number public readonly kpiDamage: number public readonly hcReward: number protected spriteKey: string protected pathPoints: PathPoint[] protected currentPathIndex: number = 0 public isDead: boolean = false public isActive: boolean = true get pathProgress(): number { return this.currentPathIndex } public dotEffects: DotEffect[] = [] public slowEffect: number = 0 public slowTimer: number = 0 public shieldCount: number = 0 public hcRewardBonus: boolean = false // 运营专员「增长黑客」双倍HC标记 protected cellW: number protected cellH: number constructor( scene: Phaser.Scene, pathPoints: PathPoint[], maxHp: number, speed: number, kpiDamage: number, hcReward: number, spriteKey: string, speedMultiplier: number = 1.0, hpMultiplier: number = 1.0 ) { this.scene = scene this.pathPoints = pathPoints this.maxHp = Math.ceil(maxHp * hpMultiplier) this.hp = this.maxHp this.speed = speed * speedMultiplier this.kpiDamage = kpiDamage this.hcReward = hcReward this.spriteKey = spriteKey const { cellW, cellH } = getCellSize() this.cellW = cellW this.cellH = cellH if (pathPoints.length > 0) { this.x = pathPoints[0].x this.y = pathPoints[0].y } const enemySize = cellW * 0.75 this.imageSprite = scene.add.image(this.x, this.y, spriteKey) this.imageSprite.setDisplaySize(enemySize, enemySize) this.imageSprite.setDepth(10) this.healthBar = scene.add.graphics() this.quoteText = scene.add .text(this.x, this.y - 30, this.getQuote(), { fontFamily: 'VT323, monospace', fontSize: '13px', color: '#FFFFFF', backgroundColor: 'rgba(0,0,0,0.7)', padding: { x: 4, y: 2 }, }) .setOrigin(0.5, 1) .setDepth(15) .setAlpha(0) this.drawHealthBar() scene.time.delayedCall(500, () => { if (!this.isDead) this.showQuote() }) } update(delta: number): void { if (this.isDead || !this.isActive) return this.processDOT(delta) if (this.slowTimer > 0) { this.slowTimer -= delta if (this.slowTimer <= 0) this.slowEffect = 0 } this.moveAlongPath(delta) this.imageSprite.setPosition(this.x, this.y) this.drawHealthBar() if (this.quoteText?.alpha > 0) { this.quoteText.setPosition(this.x, this.y - (this.cellH * 0.45)) } } protected processDOT(delta: number): void { for (let i = this.dotEffects.length - 1; i >= 0; i--) { const dot = this.dotEffects[i] dot.timer -= delta this.hp -= (dot.damage / 1000) * delta if (this.hp <= 0) { this.die(); return } if (dot.timer <= 0) this.dotEffects.splice(i, 1) } } protected moveAlongPath(delta: number): void { if (this.currentPathIndex >= this.pathPoints.length - 1) { this.reachEnd(); return } const target = this.pathPoints[this.currentPathIndex + 1] const currentSpeed = this.speed * (1 - this.slowEffect) const distance = (currentSpeed * delta) / 1000 const dx = target.x - this.x const dy = target.y - this.y const dist = Math.sqrt(dx * dx + dy * dy) if (dist <= distance) { this.x = target.x; this.y = target.y this.currentPathIndex++ } else { this.x += (dx / dist) * distance this.y += (dy / dist) * distance } if (this.slowEffect > 0) { this.imageSprite.setTint(0x93c5fd) } else { this.imageSprite.clearTint() } } protected drawHealthBar(): void { this.healthBar.clear() const bw = this.cellW * 0.5 const bh = 5 const bx = this.x - bw / 2 const by = this.y - this.cellH * 0.45 const ratio = Math.max(0, this.hp / this.maxHp) const color = ratio > 0.5 ? 0x22c55e : ratio > 0.25 ? 0xf59e0b : 0xef4444 this.healthBar.fillStyle(0x1f2937, 1) this.healthBar.fillRect(bx, by, bw, bh) this.healthBar.fillStyle(color, 1) this.healthBar.fillRect(bx, by, bw * ratio, bh) this.healthBar.setDepth(14) } takeDamage(damage: number): void { if (this.isDead) return if (this.shieldCount > 0) { this.shieldCount-- this.showShieldBlock() return } this.hp -= damage this.drawHealthBar() if (this.hp <= 0) this.die() } private showShieldBlock(): void { const txt = this.scene.add .text(this.x, this.y - 35, '护盾!', { fontFamily: 'VT323, monospace', fontSize: '14px', color: '#93C5FD', }).setOrigin(0.5, 1).setDepth(20) this.scene.tweens.add({ targets: txt, y: this.y - 55, alpha: 0, duration: 800, onComplete: () => txt.destroy(), }) } addDOT(damage: number, duration: number): void { this.dotEffects.push({ damage, duration, timer: duration }) } addSlow(percent: number, duration: number): void { this.slowEffect = Math.max(this.slowEffect, percent) this.slowTimer = Math.max(this.slowTimer, duration) } /** 需求变更:将路径进度回退 n 个节点(PM 特殊技能) */ rewindPath(steps: number): void { this.currentPathIndex = Math.max(0, this.currentPathIndex - steps) const target = this.pathPoints[this.currentPathIndex] if (target) { this.x = target.x; this.y = target.y } } protected die(): void { if (this.isDead) return this.isDead = true const reward = this.hcRewardBonus ? this.hcReward * 2 : this.hcReward GameManager.getInstance().addHC(reward) this.onDeath() this.destroy() } protected reachEnd(): void { if (this.isDead) return GameManager.getInstance().reduceKPI(this.kpiDamage) this.isDead = true this.destroy() } protected onDeath(): void {} showQuote(): void { if (this.isDead || !this.quoteText) return this.quoteText.setText(this.getQuote()) this.quoteText.setPosition(this.x, this.y - this.cellH * 0.45) this.quoteText.setAlpha(1) this.scene.tweens.add({ targets: this.quoteText, alpha: 0, duration: 800, delay: 2200, ease: 'Linear', }) } abstract getQuote(): string destroy(): void { this.imageSprite?.destroy() this.healthBar?.destroy() this.quoteText?.destroy() } }