Files
test1/game/enemies/EnemyBase.ts

261 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
// 语录文字跟随怪物移动
if (this.quoteText && this.quoteText.alpha > 0) {
this.quoteText.setPosition(this.x, this.y - 30)
}
}
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 {}
/** 显示头顶语录出生后随机触发3秒后淡出 */
showQuote(): void {
if (this.isDead || !this.quoteText) return
this.quoteText.setText(this.getQuote())
this.quoteText.setPosition(this.x, this.y - 30)
this.quoteText.setAlpha(1)
// 3秒后淡出动画
this.scene.tweens.add({
targets: this.quoteText,
alpha: 0,
duration: 800,
delay: 2200,
ease: 'Linear',
})
}
abstract drawSprite(): void
abstract getQuote(): string
destroy(): void {
this.sprite?.destroy()
this.healthBar?.destroy()
this.quoteText?.destroy()
}
}