Files
test1/game/enemies/EnemyBase.ts

266 lines
7.7 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 { 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()
}
}