264 lines
7.6 KiB
TypeScript
264 lines
7.6 KiB
TypeScript
import type Phaser from 'phaser'
|
|
import { GameManager } from '../GameManager'
|
|
import { getCellSize } from '../mapRenderer'
|
|
import { COFFEE_COST } from '../constants'
|
|
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'
|
|
import { PMTower } from './PMTower'
|
|
import { OpsTower } from './OpsTower'
|
|
import { OutsourceTower } from './OutsourceTower'
|
|
|
|
export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' | 'pm' | 'ops' | 'outsource'
|
|
|
|
export const TOWER_COSTS: Record<TowerType, number> = {
|
|
outsource: 30,
|
|
intern: 50,
|
|
hrbp: 80,
|
|
ops: 90,
|
|
ppt: 100,
|
|
senior: 120,
|
|
pm: 160,
|
|
}
|
|
|
|
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}`
|
|
// 用 occupiedCells 判断,路径判断由 GameScene 传入的 currentPathTiles 负责
|
|
return !this.occupiedCells.has(key)
|
|
}
|
|
|
|
setCurrentPathTiles(tiles: Set<string>): void {
|
|
this._pathTiles = tiles
|
|
}
|
|
private _pathTiles: Set<string> = new Set()
|
|
|
|
canPlaceWithPath(gridX: number, gridY: number): boolean {
|
|
const key = `${gridX},${gridY}`
|
|
return !this._pathTiles.has(key) && !this.occupiedCells.has(key)
|
|
}
|
|
|
|
placeTower(gridX: number, gridY: number, type: TowerType): boolean {
|
|
if (!this.canPlaceWithPath(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)
|
|
case 'pm': return new PMTower(this.scene, gridX, gridY)
|
|
case 'ops': return new OpsTower(this.scene, gridX, gridY)
|
|
case 'outsource': return new OutsourceTower(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 { cellW } = getCellSize()
|
|
const hasTarget = enemies.some((e) => {
|
|
if (e.isDead) return false
|
|
const dx = e.x - tower['px']
|
|
const dy = e.y - tower['py']
|
|
return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * cellW
|
|
})
|
|
|
|
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 { cellW, cellH } = getCellSize()
|
|
const { x: cx, y: cy } = tower.getPixelCenter()
|
|
void cx // suppress unused warning (used in label position below)
|
|
|
|
const label = this.scene.add
|
|
.text(
|
|
cx,
|
|
cy - 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
|
|
}
|
|
}
|