feat(enemies): 实现怪物基类与4种怪物及波次管理器
This commit is contained in:
113
game/enemies/BossVP.ts
Normal file
113
game/enemies/BossVP.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { EnemyBase, type PathPoint } from './EnemyBase'
|
||||||
|
|
||||||
|
const QUOTES = [
|
||||||
|
'我来教大家怎么做事',
|
||||||
|
'你们缺乏战略眼光',
|
||||||
|
'这不是执行力的问题',
|
||||||
|
]
|
||||||
|
|
||||||
|
export class BossVP extends EnemyBase {
|
||||||
|
private skillTimer: number = 20000
|
||||||
|
private onDestroyTower?: () => void
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
pathPoints: PathPoint[],
|
||||||
|
onDestroyTower?: () => void
|
||||||
|
) {
|
||||||
|
super(scene, pathPoints, 800, 40, 30, 150)
|
||||||
|
this.onDestroyTower = onDestroyTower
|
||||||
|
this.drawSprite()
|
||||||
|
// BOSS 出现时全屏红色闪光
|
||||||
|
scene.cameras.main.flash(800, 255, 0, 0, false)
|
||||||
|
this.showBossAlert()
|
||||||
|
}
|
||||||
|
|
||||||
|
private showBossAlert(): void {
|
||||||
|
const alert = this.scene.add
|
||||||
|
.text(640, 360, '⚠ 空降VP来袭!⚠', {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '36px',
|
||||||
|
color: '#FBBF24',
|
||||||
|
backgroundColor: '#7F1D1D',
|
||||||
|
padding: { x: 16, y: 8 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(50)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: alert,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 2500,
|
||||||
|
delay: 500,
|
||||||
|
onComplete: () => alert.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 金色六边形
|
||||||
|
this.sprite.fillStyle(0xfbbf24, 1)
|
||||||
|
const r = 22
|
||||||
|
this.sprite.fillPoints(this.hexPoints(r), true)
|
||||||
|
// 金色外框
|
||||||
|
this.sprite.lineStyle(3, 0xf59e0b, 1)
|
||||||
|
this.sprite.strokePoints(this.hexPoints(r + 4), false)
|
||||||
|
// 内部颜色
|
||||||
|
this.sprite.fillStyle(0xd97706, 1)
|
||||||
|
this.sprite.fillCircle(0, 0, 8)
|
||||||
|
this.sprite.setDepth(12)
|
||||||
|
this.sprite.setPosition(this.x, this.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hexPoints(r: number): Phaser.Types.Math.Vector2Like[] {
|
||||||
|
const pts: Phaser.Types.Math.Vector2Like[] = []
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const angle = (Math.PI / 3) * i - Math.PI / 6
|
||||||
|
pts.push({ x: Math.cos(angle) * r, y: Math.sin(angle) * r })
|
||||||
|
}
|
||||||
|
return pts
|
||||||
|
}
|
||||||
|
|
||||||
|
override update(delta: number): void {
|
||||||
|
if (this.isDead) return
|
||||||
|
super.update(delta)
|
||||||
|
|
||||||
|
this.skillTimer -= delta
|
||||||
|
if (this.skillTimer <= 0) {
|
||||||
|
this.skillTimer = 20000
|
||||||
|
this.triggerOrgRestructure()
|
||||||
|
}
|
||||||
|
// 重绘六边形到新位置
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerOrgRestructure(): void {
|
||||||
|
// 组织架构调整:随机摧毁一个防御塔
|
||||||
|
if (this.onDestroyTower) {
|
||||||
|
this.onDestroyTower()
|
||||||
|
}
|
||||||
|
const txt = this.scene.add
|
||||||
|
.text(this.x, this.y - 40, '组织架构调整!', {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '18px',
|
||||||
|
color: '#FBBF24',
|
||||||
|
backgroundColor: '#7C2D12',
|
||||||
|
padding: { x: 6, y: 3 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 1)
|
||||||
|
.setDepth(25)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: txt,
|
||||||
|
y: this.y - 70,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 2000,
|
||||||
|
onComplete: () => txt.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuote(): string {
|
||||||
|
return QUOTES[Math.floor(Math.random() * QUOTES.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
249
game/enemies/EnemyBase.ts
Normal file
249
game/enemies/EnemyBase.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
24
game/enemies/FreshGraduate.ts
Normal file
24
game/enemies/FreshGraduate.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { EnemyBase, type PathPoint } from './EnemyBase'
|
||||||
|
|
||||||
|
const QUOTES = ['求转正!', '我愿意加班!', '卷!卷!卷!']
|
||||||
|
|
||||||
|
export class FreshGraduate extends EnemyBase {
|
||||||
|
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
||||||
|
super(scene, pathPoints, 30, 120, 2, 10)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
this.sprite.fillStyle(0x86efac, 1)
|
||||||
|
this.sprite.fillCircle(0, 0, 8)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
this.sprite.setPosition(this.x, this.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuote(): string {
|
||||||
|
return QUOTES[Math.floor(Math.random() * QUOTES.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
39
game/enemies/OldEmployee.ts
Normal file
39
game/enemies/OldEmployee.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { EnemyBase, type PathPoint } from './EnemyBase'
|
||||||
|
|
||||||
|
const QUOTES = ['我为公司立过功!', '我有10年经验!', '年龄不是问题!']
|
||||||
|
|
||||||
|
export class OldEmployee extends EnemyBase {
|
||||||
|
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
||||||
|
super(scene, pathPoints, 150, 50, 8, 30)
|
||||||
|
this.shieldCount = 3
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 大蓝方块
|
||||||
|
this.sprite.fillStyle(0x93c5fd, 1)
|
||||||
|
this.sprite.fillRect(-10, -10, 20, 20)
|
||||||
|
// 护盾外框(金色)
|
||||||
|
this.sprite.lineStyle(2, 0xfbbf24, 0.8)
|
||||||
|
this.sprite.strokeRect(-12, -12, 24, 24)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
this.sprite.setPosition(this.x, this.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override drawHealthBar(): void {
|
||||||
|
super.drawHealthBar()
|
||||||
|
// 绘制护盾数量标记
|
||||||
|
if (!this.healthBar) return
|
||||||
|
for (let i = 0; i < this.shieldCount; i++) {
|
||||||
|
this.healthBar.fillStyle(0xfbbf24, 1)
|
||||||
|
this.healthBar.fillRect(this.x - 15 + i * 11, this.y - 28, 8, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuote(): string {
|
||||||
|
return QUOTES[Math.floor(Math.random() * QUOTES.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
54
game/enemies/TroubleMaker.ts
Normal file
54
game/enemies/TroubleMaker.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { EnemyBase, type PathPoint } from './EnemyBase'
|
||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
|
||||||
|
const QUOTES = ['录音笔已开启', '这是违法的!', '我要仲裁!']
|
||||||
|
|
||||||
|
export class TroubleMaker extends EnemyBase {
|
||||||
|
constructor(scene: Phaser.Scene, pathPoints: PathPoint[]) {
|
||||||
|
super(scene, pathPoints, 80, 80, 5, 20)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 三角形(叹号形状)
|
||||||
|
this.sprite.fillStyle(0xfca5a5, 1)
|
||||||
|
this.sprite.fillTriangle(0, -14, -12, 10, 12, 10)
|
||||||
|
// 叹号
|
||||||
|
this.sprite.fillStyle(0x7f1d1d, 1)
|
||||||
|
this.sprite.fillRect(-2, -6, 4, 8)
|
||||||
|
this.sprite.fillRect(-2, 6, 4, 4)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
this.sprite.setPosition(this.x, this.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onDeath(): void {
|
||||||
|
// 劳动仲裁:死亡时扣玩家 20 HC
|
||||||
|
const manager = GameManager.getInstance()
|
||||||
|
manager.spendHC(20)
|
||||||
|
// 显示提示文字
|
||||||
|
const txt = this.scene.add
|
||||||
|
.text(this.x, this.y - 20, '劳动仲裁! -20HC', {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#FCA5A5',
|
||||||
|
backgroundColor: '#7F1D1D',
|
||||||
|
padding: { x: 4, y: 2 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 1)
|
||||||
|
.setDepth(25)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: txt,
|
||||||
|
y: this.y - 50,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 1500,
|
||||||
|
onComplete: () => txt.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuote(): string {
|
||||||
|
return QUOTES[Math.floor(Math.random() * QUOTES.length)]
|
||||||
|
}
|
||||||
|
}
|
||||||
182
game/enemies/WaveManager.ts
Normal file
182
game/enemies/WaveManager.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { EnemyBase, buildFullPath, type PathPoint } from './EnemyBase'
|
||||||
|
import { FreshGraduate } from './FreshGraduate'
|
||||||
|
import { OldEmployee } from './OldEmployee'
|
||||||
|
import { TroubleMaker } from './TroubleMaker'
|
||||||
|
import { BossVP } from './BossVP'
|
||||||
|
|
||||||
|
interface EnemyGroup {
|
||||||
|
type: 'FreshGraduate' | 'OldEmployee' | 'TroubleMaker' | 'BossVP'
|
||||||
|
count: number
|
||||||
|
interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaveConfig {
|
||||||
|
enemies: EnemyGroup[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const WAVE_CONFIG: WaveConfig[] = [
|
||||||
|
{ enemies: [{ type: 'FreshGraduate', count: 10, interval: 800 }] },
|
||||||
|
{
|
||||||
|
enemies: [
|
||||||
|
{ type: 'FreshGraduate', count: 8, interval: 800 },
|
||||||
|
{ type: 'OldEmployee', count: 3, interval: 2000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enemies: [
|
||||||
|
{ type: 'OldEmployee', count: 5, interval: 2000 },
|
||||||
|
{ type: 'TroubleMaker', count: 3, interval: 1500 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enemies: [
|
||||||
|
{ type: 'FreshGraduate', count: 12, interval: 600 },
|
||||||
|
{ type: 'TroubleMaker', count: 2, interval: 1500 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enemies: [
|
||||||
|
{ type: 'OldEmployee', count: 6, interval: 1500 },
|
||||||
|
{ type: 'TroubleMaker', count: 3, interval: 1500 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enemies: [
|
||||||
|
{ type: 'BossVP', count: 1, interval: 0 },
|
||||||
|
{ type: 'OldEmployee', count: 4, interval: 2000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export class WaveManager {
|
||||||
|
private scene: Phaser.Scene
|
||||||
|
private activeEnemies: EnemyBase[] = []
|
||||||
|
private pathPoints: PathPoint[]
|
||||||
|
private currentWave: number = 0
|
||||||
|
private isSpawning: boolean = false
|
||||||
|
private onWave3Complete?: () => void
|
||||||
|
private onAllWavesComplete?: () => void
|
||||||
|
private onDestroyRandomTower?: () => void
|
||||||
|
private wave3Completed: boolean = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
onWave3Complete?: () => void,
|
||||||
|
onAllWavesComplete?: () => void,
|
||||||
|
onDestroyRandomTower?: () => void
|
||||||
|
) {
|
||||||
|
this.scene = scene
|
||||||
|
this.pathPoints = buildFullPath()
|
||||||
|
this.onWave3Complete = onWave3Complete
|
||||||
|
this.onAllWavesComplete = onAllWavesComplete
|
||||||
|
this.onDestroyRandomTower = onDestroyRandomTower
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalWaves(): number {
|
||||||
|
return WAVE_CONFIG.length
|
||||||
|
}
|
||||||
|
|
||||||
|
startNextWave(): void {
|
||||||
|
if (this.isSpawning || this.currentWave >= WAVE_CONFIG.length) return
|
||||||
|
const config = WAVE_CONFIG[this.currentWave]
|
||||||
|
this.currentWave++
|
||||||
|
this.isSpawning = true
|
||||||
|
this.spawnWave(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnWave(config: WaveConfig): void {
|
||||||
|
// 将所有怪物组展开为按时间排列的生成序列
|
||||||
|
const spawnQueue: { type: EnemyGroup['type']; delay: number }[] = []
|
||||||
|
for (const group of config.enemies) {
|
||||||
|
for (let i = 0; i < group.count; i++) {
|
||||||
|
spawnQueue.push({ type: group.type, delay: group.interval * i })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按 delay 升序排列,同时生成
|
||||||
|
spawnQueue.sort((a, b) => a.delay - b.delay)
|
||||||
|
|
||||||
|
let completed = 0
|
||||||
|
for (const item of spawnQueue) {
|
||||||
|
this.scene.time.delayedCall(item.delay, () => {
|
||||||
|
this.spawnEnemy(item.type)
|
||||||
|
completed++
|
||||||
|
if (completed === spawnQueue.length) {
|
||||||
|
this.isSpawning = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawnQueue.length === 0) this.isSpawning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawnEnemy(type: EnemyGroup['type']): EnemyBase {
|
||||||
|
let enemy: EnemyBase
|
||||||
|
const pts = [...this.pathPoints]
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'OldEmployee':
|
||||||
|
enemy = new OldEmployee(this.scene, pts)
|
||||||
|
break
|
||||||
|
case 'TroubleMaker':
|
||||||
|
enemy = new TroubleMaker(this.scene, pts)
|
||||||
|
break
|
||||||
|
case 'BossVP':
|
||||||
|
enemy = new BossVP(this.scene, pts, this.onDestroyRandomTower)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
enemy = new FreshGraduate(this.scene, pts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机显示语录
|
||||||
|
this.scene.time.delayedCall(1000 + Math.random() * 2000, () => {
|
||||||
|
if (!enemy.isDead) enemy.showQuote()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.activeEnemies.push(enemy)
|
||||||
|
return enemy
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number): void {
|
||||||
|
for (let i = this.activeEnemies.length - 1; i >= 0; i--) {
|
||||||
|
const e = this.activeEnemies[i]
|
||||||
|
if (e.isDead) {
|
||||||
|
this.activeEnemies.splice(i, 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.update(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查第3波完成后触发周报
|
||||||
|
if (
|
||||||
|
this.currentWave === 3 &&
|
||||||
|
!this.isSpawning &&
|
||||||
|
this.activeEnemies.length === 0 &&
|
||||||
|
!this.wave3Completed
|
||||||
|
) {
|
||||||
|
this.wave3Completed = true
|
||||||
|
this.onWave3Complete?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查全部波次完成
|
||||||
|
if (
|
||||||
|
this.currentWave >= WAVE_CONFIG.length &&
|
||||||
|
!this.isSpawning &&
|
||||||
|
this.activeEnemies.length === 0
|
||||||
|
) {
|
||||||
|
this.onAllWavesComplete?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllActiveEnemies(): EnemyBase[] {
|
||||||
|
return this.activeEnemies
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreWaves(): boolean {
|
||||||
|
return this.currentWave < WAVE_CONFIG.length
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWaveNumber(): number {
|
||||||
|
return this.currentWave
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user