250 lines
6.2 KiB
TypeScript
250 lines
6.2 KiB
TypeScript
import type Phaser from 'phaser'
|
|
import { GameManager } from '../GameManager'
|
|
import { TILE_SIZE, HUD_HEIGHT, PATH_WAYPOINTS } from '../constants'
|
|
|
|
export interface PathPoint {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
/** 将格子坐标转换为像素坐标(格子中心) */
|
|
function gridToPixel(gx: number, gy: number): PathPoint {
|
|
return {
|
|
x: gx * TILE_SIZE + TILE_SIZE / 2,
|
|
y: gy * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT,
|
|
}
|
|
}
|
|
|
|
/** 将 PATH_WAYPOINTS 扩展为完整转折路径(去重相邻相同点) */
|
|
export function buildFullPath(): PathPoint[] {
|
|
const points: PathPoint[] = []
|
|
for (let i = 0; i < PATH_WAYPOINTS.length - 1; i++) {
|
|
const from = gridToPixel(PATH_WAYPOINTS[i].x, PATH_WAYPOINTS[i].y)
|
|
const to = gridToPixel(PATH_WAYPOINTS[i + 1].x, PATH_WAYPOINTS[i + 1].y)
|
|
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
|
|
public sprite!: Phaser.GameObjects.Graphics
|
|
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 pathPoints: PathPoint[]
|
|
protected currentPathIndex: number = 0
|
|
protected x: number = 0
|
|
protected y: 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
|
|
|
|
constructor(
|
|
scene: Phaser.Scene,
|
|
pathPoints: PathPoint[],
|
|
maxHp: number,
|
|
speed: number,
|
|
kpiDamage: number,
|
|
hcReward: number
|
|
) {
|
|
this.scene = scene
|
|
this.pathPoints = pathPoints
|
|
this.maxHp = maxHp
|
|
this.hp = maxHp
|
|
this.speed = speed
|
|
this.kpiDamage = kpiDamage
|
|
this.hcReward = hcReward
|
|
|
|
if (pathPoints.length > 0) {
|
|
this.x = pathPoints[0].x
|
|
this.y = pathPoints[0].y
|
|
}
|
|
|
|
this.sprite = scene.add.graphics()
|
|
this.healthBar = scene.add.graphics()
|
|
this.quoteText = scene.add
|
|
.text(this.x, this.y - 30, this.getQuote(), {
|
|
fontFamily: 'VT323, monospace',
|
|
fontSize: '12px',
|
|
color: '#FFFFFF',
|
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
padding: { x: 3, y: 2 },
|
|
})
|
|
.setOrigin(0.5, 1)
|
|
.setDepth(15)
|
|
.setAlpha(0)
|
|
|
|
this.drawSprite()
|
|
this.drawHealthBar()
|
|
}
|
|
|
|
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.drawHealthBar()
|
|
this.sprite.setPosition(this.x, this.y)
|
|
}
|
|
|
|
protected processDOT(delta: number): void {
|
|
for (let i = this.dotEffects.length - 1; i >= 0; i--) {
|
|
const dot = this.dotEffects[i]
|
|
dot.timer -= delta
|
|
const tickDamage = (dot.damage / 1000) * delta
|
|
this.hp -= tickDamage
|
|
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
|
|
}
|
|
}
|
|
|
|
protected drawHealthBar(): void {
|
|
this.healthBar.clear()
|
|
const bw = 30
|
|
const bh = 4
|
|
const bx = this.x - bw / 2
|
|
const by = this.y - 20
|
|
const ratio = Math.max(0, this.hp / this.maxHp)
|
|
const color = ratio > 0.5 ? 0x22c55e : ratio > 0.25 ? 0xf59e0b : 0xef4444
|
|
|
|
this.healthBar.fillStyle(0x374151, 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)
|
|
}
|
|
|
|
protected die(): void {
|
|
if (this.isDead) return
|
|
this.isDead = true
|
|
const manager = GameManager.getInstance()
|
|
manager.addHC(this.hcReward)
|
|
this.onDeath()
|
|
this.destroy()
|
|
}
|
|
|
|
protected reachEnd(): void {
|
|
if (this.isDead) return
|
|
const manager = GameManager.getInstance()
|
|
manager.reduceKPI(this.kpiDamage)
|
|
this.isDead = true
|
|
this.destroy()
|
|
}
|
|
|
|
protected onDeath(): void {}
|
|
|
|
/** 显示头顶语录(短暂) */
|
|
showQuote(): void {
|
|
this.quoteText.setText(this.getQuote())
|
|
this.quoteText.setAlpha(1)
|
|
this.scene.time.delayedCall(1500, () => {
|
|
if (this.quoteText) this.quoteText.setAlpha(0)
|
|
})
|
|
}
|
|
|
|
abstract drawSprite(): void
|
|
abstract getQuote(): string
|
|
|
|
destroy(): void {
|
|
this.sprite?.destroy()
|
|
this.healthBar?.destroy()
|
|
this.quoteText?.destroy()
|
|
}
|
|
}
|