feat(towers): 实现防御塔基类与4种塔及塔管理器
This commit is contained in:
246
game/towers/TowerManager.ts
Normal file
246
game/towers/TowerManager.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import type Phaser from 'phaser'
|
||||
import { GameManager } from '../GameManager'
|
||||
import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST } from '../constants'
|
||||
import { PATH_TILES } from '../mapRenderer'
|
||||
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||
import { TowerBase } from './TowerBase'
|
||||
import { InternTower } from './InternTower'
|
||||
import { SeniorDevTower } from './SeniorDevTower'
|
||||
import { PPTMasterTower } from './PPTMasterTower'
|
||||
import { HRBPTower } from './HRBPTower'
|
||||
|
||||
export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp'
|
||||
|
||||
export const TOWER_COSTS: Record<TowerType, number> = {
|
||||
intern: 50,
|
||||
senior: 120,
|
||||
ppt: 100,
|
||||
hrbp: 80,
|
||||
}
|
||||
|
||||
export class TowerManager {
|
||||
private scene: Phaser.Scene
|
||||
private towers: TowerBase[] = []
|
||||
private occupiedCells: Set<string> = new Set()
|
||||
private infoLabel: Phaser.GameObjects.Text | null = null
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
this.scene = scene
|
||||
}
|
||||
|
||||
canPlace(gridX: number, gridY: number): boolean {
|
||||
const key = `${gridX},${gridY}`
|
||||
return !PATH_TILES.has(key) && !this.occupiedCells.has(key)
|
||||
}
|
||||
|
||||
placeTower(gridX: number, gridY: number, type: TowerType): boolean {
|
||||
if (!this.canPlace(gridX, gridY)) return false
|
||||
|
||||
const cost = TOWER_COSTS[type]
|
||||
const manager = GameManager.getInstance()
|
||||
if (!manager.spendHC(cost)) return false
|
||||
|
||||
const tower = this.createTower(type, gridX, gridY)
|
||||
this.towers.push(tower)
|
||||
this.occupiedCells.add(`${gridX},${gridY}`)
|
||||
|
||||
// 监听实习生自毁事件
|
||||
if (tower instanceof InternTower) {
|
||||
tower.onSelfDestroy = (t) => this.removeTower(t)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private createTower(type: TowerType, gridX: number, gridY: number): TowerBase {
|
||||
switch (type) {
|
||||
case 'senior':
|
||||
return new SeniorDevTower(this.scene, gridX, gridY)
|
||||
case 'ppt':
|
||||
return new PPTMasterTower(this.scene, gridX, gridY)
|
||||
case 'hrbp':
|
||||
return new HRBPTower(this.scene, gridX, gridY)
|
||||
default:
|
||||
return new InternTower(this.scene, gridX, gridY)
|
||||
}
|
||||
}
|
||||
|
||||
update(delta: number, enemies: EnemyBase[]): void {
|
||||
// 更新 HRBP 的周围塔列表
|
||||
for (const tower of this.towers) {
|
||||
if (tower instanceof HRBPTower) {
|
||||
const nearby = this.getTowersNear(tower.gridX, tower.gridY, 1, tower)
|
||||
tower.setNearbyTowers(nearby)
|
||||
}
|
||||
}
|
||||
|
||||
for (const tower of this.towers) {
|
||||
if (tower instanceof PPTMasterTower) {
|
||||
// PPT 大师特殊更新:如果可以攻击就 AOE
|
||||
this.updatePPTTower(tower, delta, enemies)
|
||||
} else {
|
||||
tower.update(delta, enemies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updatePPTTower(
|
||||
tower: PPTMasterTower,
|
||||
delta: number,
|
||||
enemies: EnemyBase[]
|
||||
): void {
|
||||
// 直接调用 super 逻辑(手动处理冷却)
|
||||
tower['attackCooldown'] -= delta
|
||||
|
||||
if (tower['stamina'] <= 0) tower['isActive'] = false
|
||||
|
||||
if (!tower['isActive']) {
|
||||
tower['stamina'] = Math.min(
|
||||
tower.maxStamina,
|
||||
tower['stamina'] + (tower['staminaRegen'] * delta) / 1000
|
||||
)
|
||||
if (tower['stamina'] > 20) tower['isActive'] = true
|
||||
tower['updateStaminaBar']()
|
||||
return
|
||||
}
|
||||
|
||||
const hasTarget = enemies.some((e) => {
|
||||
if (e.isDead) return false
|
||||
const dx = e.sprite.x - tower['px']
|
||||
const dy = e.sprite.y - tower['py']
|
||||
return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * TILE_SIZE
|
||||
})
|
||||
|
||||
if (hasTarget && tower['attackCooldown'] <= 0) {
|
||||
tower.attackAoe(enemies)
|
||||
tower['stamina'] -= 5
|
||||
tower['attackCooldown'] = 1000 / tower.attackSpeed
|
||||
tower['updateStaminaBar']()
|
||||
} else if (!hasTarget) {
|
||||
tower['stamina'] = Math.min(
|
||||
tower.maxStamina,
|
||||
tower['stamina'] + (tower['staminaRegen'] * delta) / 1000
|
||||
)
|
||||
tower['updateStaminaBar']()
|
||||
}
|
||||
}
|
||||
|
||||
private getTowersNear(
|
||||
gx: number,
|
||||
gy: number,
|
||||
range: number,
|
||||
exclude: TowerBase
|
||||
): TowerBase[] {
|
||||
return this.towers.filter(
|
||||
(t) =>
|
||||
t !== exclude &&
|
||||
Math.abs(t.gridX - gx) <= range &&
|
||||
Math.abs(t.gridY - gy) <= range
|
||||
)
|
||||
}
|
||||
|
||||
/** 点击格子时检查是否有塔,显示信息 */
|
||||
handleTileClick(gridX: number, gridY: number): boolean {
|
||||
const tower = this.getTowerAt(gridX, gridY)
|
||||
if (!tower) return false
|
||||
this.showTowerInfo(tower)
|
||||
return true
|
||||
}
|
||||
|
||||
private showTowerInfo(tower: TowerBase): void {
|
||||
this.infoLabel?.destroy()
|
||||
|
||||
const px = tower.gridX * TILE_SIZE + TILE_SIZE / 2
|
||||
const py = tower.gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT
|
||||
|
||||
const label = this.scene.add
|
||||
.text(
|
||||
px,
|
||||
py - 40,
|
||||
`精力: ${Math.floor(tower.stamina)}% [点击购买咖啡 ${COFFEE_COST}HC]`,
|
||||
{
|
||||
fontFamily: 'VT323, monospace',
|
||||
fontSize: '14px',
|
||||
color: '#F59E0B',
|
||||
backgroundColor: '#0A1628',
|
||||
padding: { x: 6, y: 3 },
|
||||
}
|
||||
)
|
||||
.setOrigin(0.5, 1)
|
||||
.setDepth(30)
|
||||
.setInteractive()
|
||||
|
||||
label.on('pointerdown', () => {
|
||||
const ok = tower.buyCoffee()
|
||||
if (ok) {
|
||||
label.setText(`☕ 喝了咖啡!精力满了!`)
|
||||
this.scene.time.delayedCall(1000, () => label.destroy())
|
||||
} else {
|
||||
label.setText('HC 不足!')
|
||||
this.scene.time.delayedCall(800, () => label.destroy())
|
||||
}
|
||||
})
|
||||
|
||||
this.infoLabel = label
|
||||
this.scene.time.delayedCall(3000, () => {
|
||||
if (this.infoLabel === label) this.infoLabel = null
|
||||
label.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
getTowerAt(gridX: number, gridY: number): TowerBase | null {
|
||||
return (
|
||||
this.towers.find((t) => t.gridX === gridX && t.gridY === gridY) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
removeTower(tower: TowerBase): void {
|
||||
const idx = this.towers.indexOf(tower)
|
||||
if (idx !== -1) {
|
||||
this.towers.splice(idx, 1)
|
||||
this.occupiedCells.delete(`${tower.gridX},${tower.gridY}`)
|
||||
}
|
||||
}
|
||||
|
||||
removeRandomTower(): void {
|
||||
if (this.towers.length === 0) return
|
||||
const idx = Math.floor(Math.random() * this.towers.length)
|
||||
const tower = this.towers[idx]
|
||||
this.showDestroyEffect(tower)
|
||||
tower.destroy()
|
||||
this.removeTower(tower)
|
||||
}
|
||||
|
||||
private showDestroyEffect(tower: TowerBase): void {
|
||||
const { x, y } = tower.getPixelCenter()
|
||||
const txt = this.scene.add
|
||||
.text(x, y - 20, '☠ 组织架构调整!被裁了!', {
|
||||
fontFamily: 'VT323, monospace',
|
||||
fontSize: '16px',
|
||||
color: '#EF4444',
|
||||
backgroundColor: '#7F1D1D',
|
||||
padding: { x: 6, y: 3 },
|
||||
})
|
||||
.setOrigin(0.5, 1)
|
||||
.setDepth(30)
|
||||
this.scene.tweens.add({
|
||||
targets: txt,
|
||||
y: y - 60,
|
||||
alpha: 0,
|
||||
duration: 2000,
|
||||
onComplete: () => txt.destroy(),
|
||||
})
|
||||
}
|
||||
|
||||
getAllTowers(): TowerBase[] {
|
||||
return this.towers
|
||||
}
|
||||
|
||||
hasTowerAt(gridX: number, gridY: number): boolean {
|
||||
return this.occupiedCells.has(`${gridX},${gridY}`)
|
||||
}
|
||||
|
||||
getOccupiedCells(): Set<string> {
|
||||
return this.occupiedCells
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user