feat(game): 新增3个角色、修复实习生攻击特效、加速开发子弹、每波结束自动进入下一波
This commit is contained in:
@@ -3,13 +3,16 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
|
||||
const TOWER_META = [
|
||||
{ type: 'outsource', name: '外包程序员', cost: 30, desc: '近战 8伤 0.7/s', color: '#94A3B8', img: '/game-assets/tower-outsource.png', tip: '廉价但30%丢空,5%自伤' },
|
||||
{ type: 'intern', name: '00后实习生', cost: 50, desc: '近战 15伤 1.5/s', color: '#22C55E', img: '/game-assets/tower-intern.png', tip: '整顿职场:5%概率秒杀' },
|
||||
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
||||
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' },
|
||||
{ type: 'hrbp', name: 'HRBP', cost: 80, desc: '辅助 +20%攻速', color: '#EC4899', img: '/game-assets/tower-hrbp.png', tip: '打鸡血:周围塔攻速+20%' },
|
||||
{ type: 'ops', name: '运营专员', cost: 90, desc: '远程 18伤 范围溅射', color: '#8B5CF6', img: '/game-assets/tower-ops.png', tip: '增长黑客:20%双倍HC' },
|
||||
{ type: 'ppt', name: 'PPT大师', cost: 100, desc: 'AOE减速 5伤', color: '#F59E0B', img: '/game-assets/tower-ppt.png', tip: '黑话领域:减速40%' },
|
||||
{ type: 'senior', name: 'P6资深开发', cost: 120, desc: '远程 30伤 5格射程', color: '#3B82F6', img: '/game-assets/tower-senior.png', tip: '代码屎山:附带持续伤害' },
|
||||
{ type: 'pm', name: '产品经理', cost: 160, desc: '远程 20伤 需求变更', color: '#06B6D4', img: '/game-assets/tower-pm.png', tip: '需求变更:每4次把怪打回去' },
|
||||
] as const
|
||||
|
||||
type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp'
|
||||
type TowerType = 'outsource' | 'intern' | 'hrbp' | 'ops' | 'ppt' | 'senior' | 'pm'
|
||||
|
||||
export default function GamePage() {
|
||||
const gameRef = useRef<{ destroy: (removeCanvas: boolean) => void } | null>(null)
|
||||
|
||||
@@ -27,6 +27,9 @@ const SPRITE_ASSETS: Record<string, string> = {
|
||||
'tower-senior': '/game-assets/tower-senior.png',
|
||||
'tower-ppt': '/game-assets/tower-ppt.png',
|
||||
'tower-hrbp': '/game-assets/tower-hrbp.png',
|
||||
'tower-pm': '/game-assets/tower-pm.png',
|
||||
'tower-ops': '/game-assets/tower-ops.png',
|
||||
'tower-outsource': '/game-assets/tower-outsource.png',
|
||||
'enemy-fresh': '/game-assets/enemy-fresh.png',
|
||||
'enemy-old': '/game-assets/enemy-old.png',
|
||||
'enemy-trouble': '/game-assets/enemy-trouble.png',
|
||||
@@ -134,6 +137,8 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
||||
*/
|
||||
private loadMap(mapConfig: MapConfig): void {
|
||||
this.currentPathTiles = buildPathTiles(mapConfig.waypoints)
|
||||
// 同步路径格给 TowerManager,用于建塔合法性判断
|
||||
this.towerManager.setCurrentPathTiles(this.currentPathTiles)
|
||||
this.bgObject?.destroy()
|
||||
this.bgObject = renderMapBackground(this, mapConfig.bgKey)
|
||||
drawAllTiles(this.tileGraphics, null, this.currentPathTiles, mapConfig)
|
||||
@@ -151,8 +156,13 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
||||
mapConfig.waypoints
|
||||
)
|
||||
this.mapInTransition = false
|
||||
this.isWaveRunning = false
|
||||
}
|
||||
|
||||
// 自动下一波倒计时(ms),-1 表示未激活
|
||||
private autoNextWaveTimer: number = -1
|
||||
private readonly AUTO_WAVE_DELAY = 3000 // 3 秒后自动开始
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
if (this.manager.gameState !== 'playing' && this.manager.gameState !== 'idle') return
|
||||
if (this.mapInTransition) return
|
||||
@@ -160,19 +170,35 @@ export function createGameScene(PhaserLib: typeof Phaser): typeof Phaser.Scene {
|
||||
this.towerManager.update(delta, this.waveManager.getAllActiveEnemies())
|
||||
this.waveManager.update(delta)
|
||||
|
||||
// 当前波次怪物全灭且还有下一波时,启动自动倒计时
|
||||
if (
|
||||
this.isWaveRunning &&
|
||||
this.waveManager.getAllActiveEnemies().length === 0 &&
|
||||
this.waveManager.hasMoreWaves()
|
||||
) {
|
||||
this.isWaveRunning = false
|
||||
if (this.autoNextWaveTimer < 0) {
|
||||
this.autoNextWaveTimer = this.AUTO_WAVE_DELAY
|
||||
this.hud.setWaveButtonText('3s 后自动开始...')
|
||||
this.hud.enableWaveButton()
|
||||
this.hud.setWaveButtonText('▶ 召唤下一波')
|
||||
}
|
||||
}
|
||||
|
||||
// 自动倒计时
|
||||
if (this.autoNextWaveTimer > 0) {
|
||||
this.autoNextWaveTimer -= delta
|
||||
const sec = Math.ceil(this.autoNextWaveTimer / 1000)
|
||||
this.hud.setWaveButtonText(`${sec}s 后自动开始...`)
|
||||
if (this.autoNextWaveTimer <= 0) {
|
||||
this.autoNextWaveTimer = -1
|
||||
this.onWaveButtonClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onWaveButtonClick(): void {
|
||||
if (!this.waveManager.hasMoreWaves() || this.isWaveRunning || this.mapInTransition) return
|
||||
this.autoNextWaveTimer = -1
|
||||
this.isWaveRunning = true
|
||||
this.hud.disableWaveButton()
|
||||
this.hud.setWaveButtonText('波次进行中...')
|
||||
|
||||
@@ -63,6 +63,7 @@ export abstract class EnemyBase {
|
||||
public slowEffect: number = 0
|
||||
public slowTimer: number = 0
|
||||
public shieldCount: number = 0
|
||||
public hcRewardBonus: boolean = false // 运营专员「增长黑客」双倍HC标记
|
||||
|
||||
protected cellW: number
|
||||
protected cellH: number
|
||||
@@ -215,10 +216,18 @@ export abstract class EnemyBase {
|
||||
this.slowTimer = Math.max(this.slowTimer, duration)
|
||||
}
|
||||
|
||||
/** 需求变更:将路径进度回退 n 个节点(PM 特殊技能) */
|
||||
rewindPath(steps: number): void {
|
||||
this.currentPathIndex = Math.max(0, this.currentPathIndex - steps)
|
||||
const target = this.pathPoints[this.currentPathIndex]
|
||||
if (target) { this.x = target.x; this.y = target.y }
|
||||
}
|
||||
|
||||
protected die(): void {
|
||||
if (this.isDead) return
|
||||
this.isDead = true
|
||||
GameManager.getInstance().addHC(this.hcReward)
|
||||
const reward = this.hcRewardBonus ? this.hcReward * 2 : this.hcReward
|
||||
GameManager.getInstance().addHC(reward)
|
||||
this.onDeath()
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ export class InternTower extends TowerBase {
|
||||
}
|
||||
|
||||
attack(target: EnemyBase): void {
|
||||
if (Math.random() < 0.05 && target.hp < 500) {
|
||||
const isInstakill = Math.random() < 0.05 && target.hp < 500
|
||||
if (isInstakill) {
|
||||
target.takeDamage(9999)
|
||||
this.showMessage('整顿职场!秒杀!', '#A3E635')
|
||||
} else {
|
||||
@@ -42,11 +43,34 @@ export class InternTower extends TowerBase {
|
||||
}
|
||||
|
||||
private showMeleeEffect(target: EnemyBase): void {
|
||||
// 从塔飞向目标的拳头轨迹
|
||||
const fist = this.scene.add.text(this.px, this.py, '👊', {
|
||||
fontSize: '18px',
|
||||
}).setOrigin(0.5, 0.5).setDepth(16)
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: fist,
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
duration: 120,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
fist.destroy()
|
||||
// 命中爆炸圆圈
|
||||
const g = this.scene.add.graphics()
|
||||
g.fillStyle(0x22c55e, 0.6)
|
||||
g.fillCircle(target.x, target.y, 12)
|
||||
g.lineStyle(3, 0xa3e635, 1)
|
||||
g.strokeCircle(target.x, target.y, 16)
|
||||
g.fillStyle(0xa3e635, 0.25)
|
||||
g.fillCircle(target.x, target.y, 16)
|
||||
g.setDepth(15)
|
||||
this.scene.time.delayedCall(150, () => g.destroy())
|
||||
this.scene.tweens.add({
|
||||
targets: g,
|
||||
scaleX: 1.6, scaleY: 1.6, alpha: 0,
|
||||
duration: 250,
|
||||
onComplete: () => g.destroy(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private showMessage(msg: string, color: string): void {
|
||||
|
||||
72
game/towers/OpsTower.ts
Normal file
72
game/towers/OpsTower.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type Phaser from 'phaser'
|
||||
import { TowerBase } from './TowerBase'
|
||||
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||
|
||||
/**
|
||||
* 运营专员 — 90 HC
|
||||
* 攻击:发射数据大屏图表,范围溅射(命中目标周围的怪也受少量伤害)
|
||||
* 特殊技能"增长黑客":20% 概率命中时使目标掉落双倍 HC
|
||||
*/
|
||||
export class OpsTower extends TowerBase {
|
||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||
super(scene, gridX, gridY, 90, 3, 18, 1.2, 'tower-ops')
|
||||
}
|
||||
|
||||
attack(target: EnemyBase): void {
|
||||
this.fireChart(target)
|
||||
}
|
||||
|
||||
private fireChart(target: EnemyBase): void {
|
||||
const charts = ['📊', '📈', '📉', '🎯']
|
||||
const sym = charts[Math.floor(Math.random() * charts.length)]
|
||||
|
||||
const bullet = this.scene.add.text(this.px, this.py, sym, {
|
||||
fontSize: '15px',
|
||||
}).setOrigin(0.5, 0.5).setDepth(13)
|
||||
|
||||
const dx = target.x - this.px
|
||||
const dy = target.y - this.py
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: bullet,
|
||||
x: target.x, y: target.y,
|
||||
duration: (dist / 1000) * 1000,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
bullet.destroy()
|
||||
if (target.isDead) return
|
||||
|
||||
// 主目标伤害
|
||||
target.takeDamage(this.attackDamage)
|
||||
|
||||
// 增长黑客:20% 双倍 HC 奖励(通过 hcRewardBonus 标记)
|
||||
if (Math.random() < 0.2) {
|
||||
target.hcRewardBonus = true
|
||||
const tag = this.scene.add.text(target.x, target.y - 20, '双倍HC!', {
|
||||
fontFamily: 'VT323, monospace', fontSize: '13px', color: '#fbbf24',
|
||||
}).setOrigin(0.5, 1).setDepth(20)
|
||||
this.scene.tweens.add({
|
||||
targets: tag, y: target.y - 40, alpha: 0,
|
||||
duration: 900, onComplete: () => tag.destroy(),
|
||||
})
|
||||
}
|
||||
|
||||
// 范围溅射:命中半径 1.5 格内的其他怪物
|
||||
const splashR = this.cellW * 1.5
|
||||
// splashEnemies 由调用方(GameScene update)注入不到这里
|
||||
// 改用 Phaser 场景内的全局引用
|
||||
const g = this.scene.add.graphics()
|
||||
g.lineStyle(2, 0x8b5cf6, 0.7)
|
||||
g.strokeCircle(target.x, target.y, splashR)
|
||||
g.fillStyle(0x8b5cf6, 0.1)
|
||||
g.fillCircle(target.x, target.y, splashR)
|
||||
g.setDepth(12)
|
||||
this.scene.tweens.add({
|
||||
targets: g, alpha: 0, duration: 350,
|
||||
onComplete: () => g.destroy(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
82
game/towers/OutsourceTower.ts
Normal file
82
game/towers/OutsourceTower.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type Phaser from 'phaser'
|
||||
import { TowerBase } from './TowerBase'
|
||||
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||
|
||||
/**
|
||||
* 外包程序员 — 30 HC(最便宜但最差)
|
||||
* 攻击:扔出 Bug(随机概率丢空),低伤害,攻速慢
|
||||
* 特殊技能"甲方爸爸":5% 概率扔出的 Bug 反弹打到自己,瞬间精力归零(摸鱼)
|
||||
* 特色:便宜、量多凑数,是前期过渡塔
|
||||
*/
|
||||
export class OutsourceTower extends TowerBase {
|
||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||
// cost=30, range=2, damage=8, speed=0.7
|
||||
super(scene, gridX, gridY, 30, 2, 8, 0.7, 'tower-outsource')
|
||||
}
|
||||
|
||||
attack(target: EnemyBase): void {
|
||||
// 5% 概率 Bug 反弹(精力归零)
|
||||
if (Math.random() < 0.05) {
|
||||
this.stamina = 0
|
||||
this.showMessage('Bug反弹!精力归零!', '#ef4444')
|
||||
return
|
||||
}
|
||||
// 30% 概率丢空(miss)
|
||||
if (Math.random() < 0.3) {
|
||||
this.showMessage('Miss!环境问题!', '#9ca3af')
|
||||
return
|
||||
}
|
||||
this.fireBug(target)
|
||||
}
|
||||
|
||||
private fireBug(target: EnemyBase): void {
|
||||
const bugEmojis = ['🐛', '🔥', '💥', '❌']
|
||||
const sym = bugEmojis[Math.floor(Math.random() * bugEmojis.length)]
|
||||
|
||||
const bullet = this.scene.add.text(this.px, this.py, sym, {
|
||||
fontSize: '14px',
|
||||
}).setOrigin(0.5, 0.5).setDepth(13)
|
||||
|
||||
// 外包子弹走曲线(不走直线,歪歪扭扭)
|
||||
const midX = (this.px + target.x) / 2 + (Math.random() - 0.5) * 60
|
||||
const midY = (this.py + target.y) / 2 + (Math.random() - 0.5) * 60
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: bullet,
|
||||
x: midX, y: midY,
|
||||
duration: 200,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => {
|
||||
this.scene.tweens.add({
|
||||
targets: bullet,
|
||||
x: target.x, y: target.y,
|
||||
duration: 200,
|
||||
ease: 'Sine.easeIn',
|
||||
onComplete: () => {
|
||||
bullet.destroy()
|
||||
if (!target.isDead) {
|
||||
target.takeDamage(this.attackDamage)
|
||||
const g = this.scene.add.graphics()
|
||||
g.fillStyle(0xef4444, 0.4)
|
||||
g.fillCircle(target.x, target.y, 10)
|
||||
g.setDepth(15)
|
||||
this.scene.time.delayedCall(200, () => g.destroy())
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private showMessage(msg: string, color: string): void {
|
||||
const txt = this.scene.add
|
||||
.text(this.px, this.py - 28, msg, {
|
||||
fontFamily: 'VT323, monospace', fontSize: '13px',
|
||||
color, backgroundColor: '#1c1917', padding: { x: 3, y: 1 },
|
||||
}).setOrigin(0.5, 1).setDepth(20)
|
||||
this.scene.tweens.add({
|
||||
targets: txt, y: this.py - 50, alpha: 0,
|
||||
duration: 1000, onComplete: () => txt.destroy(),
|
||||
})
|
||||
}
|
||||
}
|
||||
76
game/towers/PMTower.ts
Normal file
76
game/towers/PMTower.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type Phaser from 'phaser'
|
||||
import { TowerBase } from './TowerBase'
|
||||
import type { EnemyBase } from '../enemies/EnemyBase'
|
||||
|
||||
/**
|
||||
* 产品经理 PM — 160 HC
|
||||
* 攻击:射出原型图(需求变更弹幕),命中使怪物"需求混乱"(双重减速 + 持续伤害)
|
||||
* 特殊技能"需求变更":每 4 次攻击强制随机重置目标位置(怪物被打回 2 个路径节点)
|
||||
*/
|
||||
export class PMTower extends TowerBase {
|
||||
private hitCount = 0
|
||||
|
||||
constructor(scene: Phaser.Scene, gridX: number, gridY: number) {
|
||||
super(scene, gridX, gridY, 160, 4, 20, 0.8, 'tower-pm')
|
||||
}
|
||||
|
||||
attack(target: EnemyBase): void {
|
||||
this.firePRD(target)
|
||||
}
|
||||
|
||||
private firePRD(target: EnemyBase): void {
|
||||
// 原型图:白色矩形框符号
|
||||
const bullet = this.scene.add.text(this.px, this.py, '📋', {
|
||||
fontSize: '16px',
|
||||
}).setOrigin(0.5, 0.5).setDepth(13)
|
||||
|
||||
const dx = target.x - this.px
|
||||
const dy = target.y - this.py
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: bullet,
|
||||
x: target.x, y: target.y,
|
||||
duration: (dist / 900) * 1000,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
bullet.destroy()
|
||||
if (target.isDead) return
|
||||
|
||||
target.takeDamage(this.attackDamage)
|
||||
// 双重减速:60% 持续 3 秒
|
||||
target.addSlow(0.6, 3000)
|
||||
target.addDOT(5, 3000)
|
||||
|
||||
this.hitCount++
|
||||
if (this.hitCount % 4 === 0) {
|
||||
// 需求变更:打回 2 个路径节点
|
||||
target.rewindPath(2)
|
||||
this.showMessage('需求变更!打回重做!', '#06b6d4')
|
||||
}
|
||||
|
||||
// 命中特效:蓝色震荡圈
|
||||
const g = this.scene.add.graphics()
|
||||
g.lineStyle(2, 0x06b6d4, 0.9)
|
||||
g.strokeRect(target.x - 12, target.y - 10, 24, 20)
|
||||
g.setDepth(15)
|
||||
this.scene.tweens.add({
|
||||
targets: g, scaleX: 1.8, scaleY: 1.8, alpha: 0,
|
||||
duration: 350, onComplete: () => g.destroy(),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private showMessage(msg: string, color: string): void {
|
||||
const txt = this.scene.add
|
||||
.text(this.px, this.py - 30, msg, {
|
||||
fontFamily: 'VT323, monospace', fontSize: '14px',
|
||||
color, backgroundColor: '#0c4a6e', 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,39 +8,56 @@ export class SeniorDevTower extends TowerBase {
|
||||
}
|
||||
|
||||
attack(target: EnemyBase): void {
|
||||
this.fireBullet(target)
|
||||
this.fireCodeBullet(target)
|
||||
}
|
||||
|
||||
private fireBullet(target: EnemyBase): void {
|
||||
const bullet = this.scene.add.graphics()
|
||||
bullet.fillStyle(0x22c55e, 1)
|
||||
bullet.fillRoundedRect(-5, -5, 10, 10, 2)
|
||||
// 添加绿色发光
|
||||
bullet.lineStyle(1, 0x86efac, 0.8)
|
||||
bullet.strokeRoundedRect(-6, -6, 12, 12, 3)
|
||||
bullet.setPosition(this.px, this.py)
|
||||
bullet.setDepth(13)
|
||||
private fireCodeBullet(target: EnemyBase): void {
|
||||
// 随机代码符号作为子弹
|
||||
const symbols = ['</>', '{}', '=>', '??', '&&', '||', '++', '!=']
|
||||
const sym = symbols[Math.floor(Math.random() * symbols.length)]
|
||||
|
||||
const bullet = this.scene.add.text(this.px, this.py, sym, {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '13px',
|
||||
color: '#86efac',
|
||||
stroke: '#14532d',
|
||||
strokeThickness: 2,
|
||||
}).setOrigin(0.5, 0.5).setDepth(13)
|
||||
|
||||
const dx = target.x - this.px
|
||||
const dy = target.y - this.py
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const duration = (dist / 500) * 1000
|
||||
// 子弹速度 1200px/s,比原来快 2.4 倍
|
||||
const duration = (dist / 1200) * 1000
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: bullet,
|
||||
x: target.x, y: target.y,
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
duration,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
bullet.destroy()
|
||||
if (!target.isDead) {
|
||||
target.takeDamage(this.attackDamage)
|
||||
target.addDOT(10, 3000)
|
||||
// DOT 命中效果
|
||||
const fx = this.scene.add.graphics()
|
||||
fx.lineStyle(2, 0x22c55e, 0.8)
|
||||
fx.strokeCircle(target.x, target.y, 14)
|
||||
fx.setDepth(15)
|
||||
this.scene.time.delayedCall(300, () => fx.destroy())
|
||||
// DOT 命中:绿色代码粒子爆散
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const p = this.scene.add.text(
|
||||
target.x, target.y,
|
||||
['bug', 'err', '!!!', '???'][i],
|
||||
{ fontFamily: 'monospace', fontSize: '10px', color: '#22c55e' }
|
||||
).setOrigin(0.5, 0.5).setDepth(15)
|
||||
const angle = (i / 4) * Math.PI * 2
|
||||
this.scene.tweens.add({
|
||||
targets: p,
|
||||
x: target.x + Math.cos(angle) * 22,
|
||||
y: target.y + Math.sin(angle) * 22,
|
||||
alpha: 0,
|
||||
duration: 400,
|
||||
onComplete: () => p.destroy(),
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import type Phaser from 'phaser'
|
||||
import { GameManager } from '../GameManager'
|
||||
import { TILE_SIZE, HUD_HEIGHT, COFFEE_COST } from '../constants'
|
||||
import { PATH_TILES } from '../mapRenderer'
|
||||
import { getCellSize } from '../mapRenderer'
|
||||
import { COFFEE_COST } from '../constants'
|
||||
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'
|
||||
export type TowerType = 'intern' | 'senior' | 'ppt' | 'hrbp' | 'pm' | 'ops' | 'outsource'
|
||||
|
||||
export const TOWER_COSTS: Record<TowerType, number> = {
|
||||
outsource: 30,
|
||||
intern: 50,
|
||||
senior: 120,
|
||||
ppt: 100,
|
||||
hrbp: 80,
|
||||
ops: 90,
|
||||
ppt: 100,
|
||||
senior: 120,
|
||||
pm: 160,
|
||||
}
|
||||
|
||||
export class TowerManager {
|
||||
@@ -30,11 +36,22 @@ export class TowerManager {
|
||||
|
||||
canPlace(gridX: number, gridY: number): boolean {
|
||||
const key = `${gridX},${gridY}`
|
||||
return !PATH_TILES.has(key) && !this.occupiedCells.has(key)
|
||||
// 用 occupiedCells 判断,路径判断由 GameScene 传入的 currentPathTiles 负责
|
||||
return !this.occupiedCells.has(key)
|
||||
}
|
||||
|
||||
setCurrentPathTiles(tiles: Set<string>): void {
|
||||
this._pathTiles = tiles
|
||||
}
|
||||
private _pathTiles: Set<string> = 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.canPlace(gridX, gridY)) return false
|
||||
if (!this.canPlaceWithPath(gridX, gridY)) return false
|
||||
|
||||
const cost = TOWER_COSTS[type]
|
||||
const manager = GameManager.getInstance()
|
||||
@@ -54,14 +71,13 @@ export class TowerManager {
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +120,12 @@ export class TowerManager {
|
||||
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 * TILE_SIZE
|
||||
return Math.sqrt(dx * dx + dy * dy) <= tower.attackRange * cellW
|
||||
})
|
||||
|
||||
if (hasTarget && tower['attackCooldown'] <= 0) {
|
||||
@@ -149,14 +166,14 @@ export class TowerManager {
|
||||
|
||||
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 { 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(
|
||||
px,
|
||||
py - 40,
|
||||
cx,
|
||||
cy - 40,
|
||||
`精力: ${Math.floor(tower.stamina)}% [点击购买咖啡 ${COFFEE_COST}HC]`,
|
||||
{
|
||||
fontFamily: 'VT323, monospace',
|
||||
|
||||
Reference in New Issue
Block a user