import type Phaser from 'phaser' import { GameManager } from '../GameManager' import { HUD_HEIGHT, COFFEE_COST, STAMINA_MAX, STAMINA_REGEN } from '../constants' import { getCellSize } from '../mapRenderer' import type { EnemyBase } from '../enemies/EnemyBase' export abstract class TowerBase { protected scene: Phaser.Scene public gridX: number public gridY: number protected imageSprite!: Phaser.GameObjects.Image protected spriteKey: string protected staminaBar!: Phaser.GameObjects.Graphics private frozenOverlay!: Phaser.GameObjects.Graphics public readonly cost: number public readonly attackRange: number public readonly attackDamage: number public readonly attackSpeed: number public readonly maxStamina: number = STAMINA_MAX public stamina: number = STAMINA_MAX public isFrozen: boolean = false protected attackCooldown: number = 0 protected staminaRegen: number = STAMINA_REGEN protected isActive: boolean = true // Pixel center position (computed from actual cell size) protected px: number protected py: number protected cellW: number protected cellH: number constructor( scene: Phaser.Scene, gridX: number, gridY: number, cost: number, attackRange: number, attackDamage: number, attackSpeed: number, spriteKey: string ) { this.scene = scene this.gridX = gridX this.gridY = gridY this.cost = cost this.attackRange = attackRange this.attackDamage = attackDamage this.attackSpeed = attackSpeed this.spriteKey = spriteKey const { cellW, cellH } = getCellSize() this.cellW = cellW this.cellH = cellH this.px = gridX * cellW + cellW / 2 this.py = HUD_HEIGHT + gridY * cellH + cellH / 2 // 用 AI 图片作为精灵,精确设为格子尺寸的 85%(留边距) const imgSize = cellW * 0.85 this.imageSprite = scene.add.image(this.px, this.py, spriteKey) this.imageSprite.setDisplaySize(imgSize, imgSize) this.imageSprite.setDepth(10) this.staminaBar = scene.add.graphics() this.frozenOverlay = scene.add.graphics() this.updateStaminaBar() } update(delta: number, enemies: EnemyBase[]): void { if (this.isFrozen) { this.drawFrozenOverlay() return } this.clearFrozenOverlay() this.attackCooldown -= delta if (this.stamina <= 0) this.isActive = false if (!this.isActive) { this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000) if (this.stamina > 20) this.isActive = true this.updateStaminaBar() // 摸鱼时图片半透明 this.imageSprite.setAlpha(0.5) return } this.imageSprite.setAlpha(1) const target = this.findTarget(enemies) if (target && this.attackCooldown <= 0) { this.attack(target) this.stamina -= 5 this.attackCooldown = 1000 / this.attackSpeed this.updateStaminaBar() } else if (!target) { this.stamina = Math.min(this.maxStamina, this.stamina + (this.staminaRegen * delta) / 1000) this.updateStaminaBar() } } protected findTarget(enemies: EnemyBase[]): EnemyBase | null { // +0.5格容忍量,避免怪物恰好在射程边界时因浮点误差漏判 const rangePx = (this.attackRange + 0.5) * this.cellW let best: EnemyBase | null = null let bestProgress = -1 for (const e of enemies) { if (e.isDead) continue const dx = e.x - this.px const dy = e.y - this.py const dist = Math.sqrt(dx * dx + dy * dy) if (dist <= rangePx) { if (e.pathProgress > bestProgress) { bestProgress = e.pathProgress best = e } } } return best } protected updateStaminaBar(): void { this.staminaBar.clear() const bw = this.cellW * 0.7 const bh = 5 const bx = this.px - bw / 2 const by = this.py + this.cellH / 2 - 10 this.staminaBar.fillStyle(0x1f2937, 1) this.staminaBar.fillRect(bx, by, bw, bh) const ratio = this.stamina / this.maxStamina const color = ratio > 0.5 ? 0xf59e0b : ratio > 0.25 ? 0xfb923c : 0xef4444 this.staminaBar.fillStyle(color, 1) this.staminaBar.fillRect(bx, by, bw * ratio, bh) this.staminaBar.setDepth(11) } buyCoffee(): boolean { const manager = GameManager.getInstance() if (manager.spendHC(COFFEE_COST)) { this.stamina = this.maxStamina this.isActive = true this.imageSprite.setAlpha(1) this.updateStaminaBar() return true } return false } getPixelCenter(): { x: number; y: number } { return { x: this.px, y: this.py } } protected drawFrozenOverlay(): void { this.frozenOverlay.clear() const hw = this.cellW / 2 const hh = this.cellH / 2 this.frozenOverlay.fillStyle(0x6b7280, 0.55) this.frozenOverlay.fillRect(this.px - hw, this.py - hh, this.cellW, this.cellH) this.frozenOverlay.setDepth(13) this.imageSprite.setTint(0x9ca3af) } protected clearFrozenOverlay(): void { this.frozenOverlay.clear() this.imageSprite.clearTint() } abstract attack(target: EnemyBase): void destroy(): void { this.imageSprite?.destroy() this.staminaBar?.destroy() this.frozenOverlay?.destroy() } }