import type Phaser from 'phaser' import { GameManager } from '../GameManager' import { getCellSize } from '../mapRenderer' import { COFFEE_COST } from '../constants' import { AudioEngine } from '../AudioEngine' 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 = { 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 = 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): void { this._pathTiles = tiles } private _pathTiles: Set = 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}`) // 建塔音效(钉钉消息音) AudioEngine.getInstance().playNotify() // 监听实习生自毁事件 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) // 塔被摧毁音效 AudioEngine.getInstance().playTowerDestroyed() } 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 { return this.occupiedCells } }