feat(towers): 实现防御塔基类与4种塔及塔管理器
This commit is contained in:
90
game/towers/HRBPTower.ts
Normal file
90
game/towers/HRBPTower.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { TowerBase } from './TowerBase'
|
||||||
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
import type { TowerBase as TowerBaseType } from './TowerBase'
|
||||||
|
|
||||||
|
const BUFF_ATTACK_SPEED_BONUS = 0.2
|
||||||
|
|
||||||
|
export class HRBPTower extends TowerBase {
|
||||||
|
private buffCooldown: number = 0
|
||||||
|
private readonly BUFF_INTERVAL = 500
|
||||||
|
private nearbyTowersBuff: Set<TowerBaseType> = new Set()
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
|
super(scene, gridX, gridY, 80, 1, 0, 0)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 菱形(粉色)
|
||||||
|
this.sprite.fillStyle(0xec4899, 1)
|
||||||
|
this.sprite.fillTriangle(0, -16, 16, 0, 0, 16)
|
||||||
|
this.sprite.fillTriangle(0, -16, -16, 0, 0, 16)
|
||||||
|
this.sprite.setPosition(this.px, this.py)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
override update(delta: number, enemies: EnemyBase[]): void {
|
||||||
|
// HRBP 没有攻击逻辑,只做 BUFF
|
||||||
|
void enemies
|
||||||
|
|
||||||
|
if (!this.isActive) {
|
||||||
|
this.stamina = Math.min(
|
||||||
|
this.maxStamina,
|
||||||
|
this.stamina + (this.staminaRegen * delta) / 1000
|
||||||
|
)
|
||||||
|
if (this.stamina > 20) this.isActive = true
|
||||||
|
this.updateStaminaBar()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffCooldown -= delta
|
||||||
|
if (this.buffCooldown <= 0) {
|
||||||
|
this.buffCooldown = this.BUFF_INTERVAL
|
||||||
|
this.applyBuffToNearby()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setNearbyTowers(towers: TowerBaseType[]): void {
|
||||||
|
this.nearbyTowersBuff = new Set(towers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyBuffToNearby(): void {
|
||||||
|
if (this.nearbyTowersBuff.size === 0) return
|
||||||
|
if (this.stamina < 5) {
|
||||||
|
this.isActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.stamina -= 5
|
||||||
|
this.updateStaminaBar()
|
||||||
|
// BUFF 效果通过 attackSpeedMultiplier 外部读取
|
||||||
|
// 这里显示一个粉色光圈效果
|
||||||
|
this.showBuffEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private showBuffEffect(): void {
|
||||||
|
const g = this.scene.add.graphics()
|
||||||
|
g.lineStyle(2, 0xec4899, 0.6)
|
||||||
|
g.strokeCircle(this.px, this.py, 90)
|
||||||
|
g.setDepth(8)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: g,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 400,
|
||||||
|
onComplete: () => g.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getBuffedTowers(): Set<TowerBaseType> {
|
||||||
|
return this.nearbyTowersBuff
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttackSpeedBonus(): number {
|
||||||
|
return BUFF_ATTACK_SPEED_BONUS
|
||||||
|
}
|
||||||
|
|
||||||
|
// HRBP 无直接攻击
|
||||||
|
attack(_target: EnemyBase): void {}
|
||||||
|
}
|
||||||
91
game/towers/InternTower.ts
Normal file
91
game/towers/InternTower.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { TowerBase } from './TowerBase'
|
||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
|
||||||
|
export class InternTower extends TowerBase {
|
||||||
|
private selfDestroyTimer: number = 0
|
||||||
|
private readonly SELF_DESTROY_INTERVAL = 1000
|
||||||
|
private destroyed: boolean = false
|
||||||
|
public onSelfDestroy?: (tower: InternTower) => void
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
|
super(scene, gridX, gridY, 50, 1, 15, 1.5)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 绿色小人(圆头+十字身体)
|
||||||
|
this.sprite.fillStyle(0x22c55e, 1)
|
||||||
|
this.sprite.fillCircle(0, -12, 8)
|
||||||
|
// 身体
|
||||||
|
this.sprite.fillRect(-3, -4, 6, 14)
|
||||||
|
// 手臂
|
||||||
|
this.sprite.fillRect(-12, -2, 24, 4)
|
||||||
|
this.sprite.setPosition(this.px, this.py)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
override update(delta: number, enemies: EnemyBase[]): void {
|
||||||
|
if (this.destroyed) return
|
||||||
|
|
||||||
|
super.update(delta, enemies)
|
||||||
|
|
||||||
|
// 被动:每秒1%概率离场
|
||||||
|
this.selfDestroyTimer += delta
|
||||||
|
if (this.selfDestroyTimer >= this.SELF_DESTROY_INTERVAL) {
|
||||||
|
this.selfDestroyTimer -= this.SELF_DESTROY_INTERVAL
|
||||||
|
if (Math.random() < 0.01) {
|
||||||
|
// 退还 25 HC
|
||||||
|
GameManager.getInstance().addHC(25)
|
||||||
|
this.showMessage('实习生跑路!+25HC')
|
||||||
|
this.destroyed = true
|
||||||
|
this.onSelfDestroy?.(this)
|
||||||
|
this.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attack(target: EnemyBase): void {
|
||||||
|
// 整顿职场:5% 概率秒杀 HP < 500 的怪物
|
||||||
|
if (Math.random() < 0.05 && target.hp < 500) {
|
||||||
|
target.takeDamage(9999)
|
||||||
|
this.showMessage('整顿职场!秒杀!')
|
||||||
|
} else {
|
||||||
|
target.takeDamage(this.attackDamage)
|
||||||
|
}
|
||||||
|
// 近战效果(闪光)
|
||||||
|
this.showMeleeEffect(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
private showMeleeEffect(target: EnemyBase): void {
|
||||||
|
const g = this.scene.add.graphics()
|
||||||
|
g.fillStyle(0x22c55e, 0.7)
|
||||||
|
g.fillCircle(target.sprite.x, target.sprite.y, 10)
|
||||||
|
g.setDepth(15)
|
||||||
|
this.scene.time.delayedCall(150, () => g.destroy())
|
||||||
|
}
|
||||||
|
|
||||||
|
private showMessage(msg: string): void {
|
||||||
|
const txt = this.scene.add
|
||||||
|
.text(this.px, this.py - 30, msg, {
|
||||||
|
fontFamily: 'VT323, monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#22C55E',
|
||||||
|
backgroundColor: '#14532D',
|
||||||
|
padding: { x: 4, y: 2 },
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 1)
|
||||||
|
.setDepth(20)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: txt,
|
||||||
|
y: this.py - 55,
|
||||||
|
alpha: 0,
|
||||||
|
duration: 1200,
|
||||||
|
onComplete: () => txt.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
60
game/towers/PPTMasterTower.ts
Normal file
60
game/towers/PPTMasterTower.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { TowerBase } from './TowerBase'
|
||||||
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
|
||||||
|
export class PPTMasterTower extends TowerBase {
|
||||||
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
|
super(scene, gridX, gridY, 100, 3, 5, 1.5)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 橙色圆形
|
||||||
|
this.sprite.fillStyle(0xf59e0b, 1)
|
||||||
|
this.sprite.fillCircle(0, 0, 16)
|
||||||
|
// 圆心白点
|
||||||
|
this.sprite.fillStyle(0xffffff, 0.9)
|
||||||
|
this.sprite.fillCircle(0, 0, 5)
|
||||||
|
this.sprite.setPosition(this.px, this.py)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
attack(target: EnemyBase): void {
|
||||||
|
// AOE 攻击:对射程内所有怪物造成伤害 + 减速
|
||||||
|
this.showAoeEffect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该方法由 TowerManager 调用,传入全体敌人 */
|
||||||
|
attackAoe(enemies: EnemyBase[]): void {
|
||||||
|
const rangePx = this.attackRange * 80 // TILE_SIZE
|
||||||
|
this.showAoeEffect()
|
||||||
|
for (const e of enemies) {
|
||||||
|
if (e.isDead) continue
|
||||||
|
const dx = e.sprite.x - this.px
|
||||||
|
const dy = e.sprite.y - this.py
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
if (dist <= rangePx) {
|
||||||
|
e.takeDamage(this.attackDamage)
|
||||||
|
// 黑话领域:减速40%持续2秒
|
||||||
|
e.addSlow(0.4, 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAoeEffect(): void {
|
||||||
|
const g = this.scene.add.graphics()
|
||||||
|
g.lineStyle(2, 0xf59e0b, 0.8)
|
||||||
|
g.strokeCircle(this.px, this.py, this.attackRange * 80)
|
||||||
|
g.setDepth(12)
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: g,
|
||||||
|
alpha: 0,
|
||||||
|
scaleX: 1.2,
|
||||||
|
scaleY: 1.2,
|
||||||
|
duration: 400,
|
||||||
|
onComplete: () => g.destroy(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
76
game/towers/SeniorDevTower.ts
Normal file
76
game/towers/SeniorDevTower.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { TowerBase } from './TowerBase'
|
||||||
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
|
||||||
|
export class SeniorDevTower extends TowerBase {
|
||||||
|
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||||
|
super(scene, gridX, gridY, 120, 5, 30, 1.0)
|
||||||
|
this.drawSprite()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSprite(): void {
|
||||||
|
if (!this.sprite) return
|
||||||
|
this.sprite.clear()
|
||||||
|
// 深蓝色方块
|
||||||
|
this.sprite.fillStyle(0x3b82f6, 1)
|
||||||
|
this.sprite.fillRect(-12, -12, 24, 24)
|
||||||
|
this.sprite.lineStyle(1, 0x93c5fd, 1)
|
||||||
|
this.sprite.strokeRect(-12, -12, 24, 24)
|
||||||
|
this.sprite.setPosition(this.px, this.py)
|
||||||
|
this.sprite.setDepth(10)
|
||||||
|
|
||||||
|
// 顶部 </> 符号文字
|
||||||
|
if (this.scene) {
|
||||||
|
const existing = this.scene.children.getByName(`dev_label_${this.gridX}_${this.gridY}`)
|
||||||
|
if (!existing) {
|
||||||
|
this.scene.add
|
||||||
|
.text(this.px, this.py, '</>', {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#DBEAFE',
|
||||||
|
})
|
||||||
|
.setOrigin(0.5, 0.5)
|
||||||
|
.setDepth(11)
|
||||||
|
.setName(`dev_label_${this.gridX}_${this.gridY}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attack(target: EnemyBase): void {
|
||||||
|
// 发射绿色代码块子弹
|
||||||
|
this.fireBullet(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireBullet(target: EnemyBase): void {
|
||||||
|
const bullet = this.scene.add.graphics()
|
||||||
|
bullet.fillStyle(0x22c55e, 1)
|
||||||
|
bullet.fillRect(-4, -4, 8, 8)
|
||||||
|
bullet.setPosition(this.px, this.py)
|
||||||
|
bullet.setDepth(13)
|
||||||
|
|
||||||
|
const startX = this.px
|
||||||
|
const startY = this.py
|
||||||
|
const targetX = target.sprite.x
|
||||||
|
const targetY = target.sprite.y
|
||||||
|
|
||||||
|
const dx = targetX - startX
|
||||||
|
const dy = targetY - startY
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
const duration = (dist / 400) * 1000
|
||||||
|
|
||||||
|
this.scene.tweens.add({
|
||||||
|
targets: bullet,
|
||||||
|
x: targetX,
|
||||||
|
y: targetY,
|
||||||
|
duration,
|
||||||
|
onComplete: () => {
|
||||||
|
bullet.destroy()
|
||||||
|
if (!target.isDead) {
|
||||||
|
target.takeDamage(this.attackDamage)
|
||||||
|
// 代码屎山:附加 DOT
|
||||||
|
target.addDOT(10, 3000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
145
game/towers/TowerBase.ts
Normal file
145
game/towers/TowerBase.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST, STAMINA_MAX, STAMINA_REGEN } from '../constants'
|
||||||
|
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||||
|
|
||||||
|
export abstract class TowerBase {
|
||||||
|
protected scene: Phaser.Scene
|
||||||
|
public gridX: number
|
||||||
|
public gridY: number
|
||||||
|
protected sprite!: Phaser.GameObjects.Graphics
|
||||||
|
protected staminaBar!: 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
|
||||||
|
|
||||||
|
protected attackCooldown: number = 0
|
||||||
|
protected staminaRegen: number = STAMINA_REGEN
|
||||||
|
protected isActive: boolean = true
|
||||||
|
|
||||||
|
// Pixel center position
|
||||||
|
protected px: number
|
||||||
|
protected py: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
scene: Phaser.Scene,
|
||||||
|
gridX: number,
|
||||||
|
gridY: number,
|
||||||
|
cost: number,
|
||||||
|
attackRange: number,
|
||||||
|
attackDamage: number,
|
||||||
|
attackSpeed: number
|
||||||
|
) {
|
||||||
|
this.scene = scene
|
||||||
|
this.gridX = gridX
|
||||||
|
this.gridY = gridY
|
||||||
|
this.cost = cost
|
||||||
|
this.attackRange = attackRange
|
||||||
|
this.attackDamage = attackDamage
|
||||||
|
this.attackSpeed = attackSpeed
|
||||||
|
|
||||||
|
this.px = gridX * TILE_SIZE + TILE_SIZE / 2
|
||||||
|
this.py = gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT
|
||||||
|
|
||||||
|
this.sprite = scene.add.graphics()
|
||||||
|
this.staminaBar = scene.add.graphics()
|
||||||
|
|
||||||
|
this.drawSprite()
|
||||||
|
this.updateStaminaBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta: number, enemies: EnemyBase[]): void {
|
||||||
|
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()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const rangePx = this.attackRange * TILE_SIZE
|
||||||
|
let best: EnemyBase | null = null
|
||||||
|
let bestProgress = -1
|
||||||
|
|
||||||
|
for (const e of enemies) {
|
||||||
|
if (e.isDead) continue
|
||||||
|
const dx = e.sprite.x - this.px
|
||||||
|
const dy = e.sprite.y - this.py
|
||||||
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||||
|
if (dist <= rangePx) {
|
||||||
|
// 优先选取路径进度最深的(模拟血量最多/威胁最大)
|
||||||
|
const progress = e.pathProgress
|
||||||
|
if (progress > bestProgress) {
|
||||||
|
bestProgress = progress
|
||||||
|
best = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateStaminaBar(): void {
|
||||||
|
this.staminaBar.clear()
|
||||||
|
const bw = 40
|
||||||
|
const bh = 4
|
||||||
|
const bx = this.px - bw / 2
|
||||||
|
const by = this.py + TILE_SIZE / 2 - 8
|
||||||
|
|
||||||
|
this.staminaBar.fillStyle(0x374151, 1)
|
||||||
|
this.staminaBar.fillRect(bx, by, bw, bh)
|
||||||
|
this.staminaBar.fillStyle(0xf59e0b, 1)
|
||||||
|
this.staminaBar.fillRect(bx, by, bw * (this.stamina / this.maxStamina), bh)
|
||||||
|
this.staminaBar.setDepth(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
buyCoffee(): boolean {
|
||||||
|
const manager = GameManager.getInstance()
|
||||||
|
if (manager.spendHC(COFFEE_COST)) {
|
||||||
|
this.stamina = this.maxStamina
|
||||||
|
this.isActive = true
|
||||||
|
this.updateStaminaBar()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getPixelCenter(): { x: number; y: number } {
|
||||||
|
return { x: this.px, y: this.py }
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract attack(target: EnemyBase): void
|
||||||
|
abstract drawSprite(): void
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.sprite?.destroy()
|
||||||
|
this.staminaBar?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
246
game/towers/TowerManager.ts
Normal file
246
game/towers/TowerManager.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import type Phaser from 'phaser'
|
||||||
|
import { GameManager } from '../GameManager'
|
||||||
|
import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST } from '../constants'
|
||||||
|
import { PATH_TILES } from '../mapRenderer'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp'
|
||||||
|
|
||||||
|
export const TOWER_COSTS: Record<TowerType, number> = {
|
||||||
|
intern: 50,
|
||||||
|
senior: 120,
|
||||||
|
ppt: 100,
|
||||||
|
hrbp: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
|
return !PATH_TILES.has(key) && !this.occupiedCells.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
placeTower(gridX: number, gridY: number, type: TowerType): boolean {
|
||||||
|
if (!this.canPlace(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)
|
||||||
|
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 hasTarget = enemies.some((e) => {
|
||||||
|
if (e.isDead) return false
|
||||||
|
const dx = e.sprite.x - tower['px']
|
||||||
|
const dy = e.sprite.y - tower['py']
|
||||||
|
return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * TILE_SIZE
|
||||||
|
})
|
||||||
|
|
||||||
|
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 px = tower.gridX * TILE_SIZE + TILE_SIZE / 2
|
||||||
|
const py = tower.gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT
|
||||||
|
|
||||||
|
const label = this.scene.add
|
||||||
|
.text(
|
||||||
|
px,
|
||||||
|
py - 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user