Files
test1/game/towers/TowerBase.ts

187 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 -= 5
// 应用 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()
}
}