266 lines
7.7 KiB
TypeScript
266 lines
7.7 KiB
TypeScript
import type Phaser from 'phaser'
|
||
import { GameManager } from '../GameManager'
|
||
import { HUD_HEIGHT } from '../constants'
|
||
import { getCellSize } from '../mapRenderer'
|
||
import { AudioEngine } from '../AudioEngine'
|
||
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)
|
||
// 怪物死亡音效(碎纸机)
|
||
AudioEngine.getInstance().playEnemyDeath()
|
||
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()
|
||
}
|
||
}
|