187 lines
5.7 KiB
TypeScript
187 lines
5.7 KiB
TypeScript
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 { AudioEngine } from '../AudioEngine'
|
||
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
|
||
private readonly _attackDamage: number
|
||
// 实际伤害 = 基础伤害 × PUA倍率
|
||
get attackDamage(): number {
|
||
return this._attackDamage * this._puaDmgMult
|
||
}
|
||
public readonly attackSpeed: number
|
||
public readonly maxStamina: number = STAMINA_MAX
|
||
public stamina: number = STAMINA_MAX
|
||
public isFrozen: boolean = false
|
||
// PUA buff 动态倍率(由 GameScene.applyPuaBuff 设置)
|
||
public _puaSpeedMult: number = 1.0 // 攻速倍率
|
||
public _puaDmgMult: number = 1.0 // 伤害倍率
|
||
|
||
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 -= 8 // 精力消耗:5→8,更快进入摸鱼状态
|
||
// 应用 PUA 攻速倍率(倍率越高冷却越短)
|
||
this.attackCooldown = 1000 / (this.attackSpeed * this._puaSpeedMult)
|
||
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()
|
||
// 购买咖啡音效
|
||
AudioEngine.getInstance().playDingTalk()
|
||
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()
|
||
}
|
||
}
|