428 lines
15 KiB
Markdown
428 lines
15 KiB
Markdown
# Task 1C+1D: 防御塔系统 + 怪物波次系统
|
||
|
||
## 任务概要
|
||
在已完成的 Phaser.js 游戏框架基础上,实现完整的防御塔系统(4种塔+部署逻辑+精力管理)和怪物波次系统(4种怪物+路径寻路+战斗逻辑+KPI扣减)。
|
||
|
||
**前置条件**:Task 1A+1B 已完成,`game/` 目录已存在,`game/GameScene.ts`、`game/constants.ts`、`game/GameManager.ts` 等已实现。
|
||
|
||
## 技术架构
|
||
|
||
```
|
||
game/
|
||
├── constants.ts ← 已存在
|
||
├── config.ts ← 已存在
|
||
├── GameScene.ts ← 已存在,需要集成新系统
|
||
├── GameManager.ts ← 已存在
|
||
├── towers/
|
||
│ ├── TowerBase.ts ← 新建
|
||
│ ├── InternTower.ts ← 新建(00后实习生)
|
||
│ ├── SeniorDevTower.ts ← 新建(P6资深开发)
|
||
│ ├── PPTMasterTower.ts ← 新建(PPT大师)
|
||
│ ├── HRBPTower.ts ← 新建(HRBP辅助塔)
|
||
│ └── TowerManager.ts ← 新建
|
||
├── enemies/
|
||
│ ├── EnemyBase.ts ← 新建
|
||
│ ├── FreshGraduate.ts ← 新建(校招应届生)
|
||
│ ├── OldEmployee.ts ← 新建(35岁老员工)
|
||
│ ├── TroubleMaker.ts ← 新建(职业碰瓷王)
|
||
│ ├── BossVP.ts ← 新建(空降VP)
|
||
│ ├── EnemyPool.ts ← 新建(对象池)
|
||
│ └── WaveManager.ts ← 新建(波次管理器)
|
||
└── ui/
|
||
├── TowerPanel.ts ← 新建(底部塔选择面板)
|
||
└── HUD.ts ← 新建(升级 GameScene 中的 HUD 逻辑)
|
||
```
|
||
|
||
## 实现规范
|
||
|
||
### 防御塔系统
|
||
|
||
**`game/towers/TowerBase.ts`**:
|
||
```typescript
|
||
export abstract class TowerBase {
|
||
protected scene: Phaser.Scene
|
||
public gridX: number
|
||
public gridY: number
|
||
protected sprite: Phaser.GameObjects.Graphics // 用 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 = 100
|
||
public stamina: number = 100
|
||
|
||
protected attackCooldown: number = 0
|
||
protected staminaRegen: number = 5 // 每秒恢复量(不攻击时)
|
||
protected isActive: boolean = true // false = 摸鱼状态
|
||
|
||
constructor(scene, gridX, gridY) { ... }
|
||
|
||
// 每帧更新
|
||
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 > 0) 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()
|
||
}
|
||
}
|
||
|
||
abstract attack(target: EnemyBase): void
|
||
abstract drawSprite(): void // 用 Graphics 绘制塔的外观
|
||
|
||
protected findTarget(enemies: EnemyBase[]): EnemyBase | null {
|
||
// 找射程内血量最多的怪(或最近怪)
|
||
...
|
||
}
|
||
|
||
protected updateStaminaBar(): void {
|
||
// 更新精力条颜色和宽度
|
||
...
|
||
}
|
||
|
||
// 玩家点击塔时调用:花费10HC购买咖啡
|
||
buyCoffee(): boolean {
|
||
const manager = GameManager.getInstance()
|
||
if (manager.spendHC(COFFEE_COST)) {
|
||
this.stamina = this.maxStamina
|
||
this.isActive = true
|
||
this.updateStaminaBar()
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
destroy(): void { ... }
|
||
}
|
||
```
|
||
|
||
**4 种塔的具体实现:**
|
||
|
||
`InternTower`(00后实习生,50HC):
|
||
- 视觉:绿色小人形状(Graphics 画)
|
||
- 攻击:近战(射程1格),单体,15伤害,1.5/s
|
||
- 特殊技能"整顿职场":每次攻击5%概率秒杀(血量<500的)普通怪物
|
||
- 被动:每秒1%概率直接离场(调用 destroy,退还 25HC)
|
||
- 攻击方式:直接扣血(近战,不需要弹幕)
|
||
|
||
`SeniorDevTower`(P6资深开发,120HC):
|
||
- 视觉:深蓝色方形,顶部有 `</>` 符号
|
||
- 攻击:远程直线,30伤害,1.0/s,射程5格
|
||
- 特殊技能"代码屎山":子弹命中后附加 DOT(10伤害/秒,持续3秒)
|
||
- 攻击方式:发射子弹(Graphics 绘制的绿色代码块),直线飞行
|
||
|
||
`PPTMasterTower`(PPT大师,100HC):
|
||
- 视觉:橙色圆形,有 PPT 图标
|
||
- 攻击:范围AOE,5伤害,1.5/s,射程3格(圆形区域)
|
||
- 特殊技能"黑话领域":攻击命中的怪物移速降低40%,持续2秒
|
||
- 攻击方式:爆炸效果(Graphics 绘制的扩散圆圈),范围内所有怪物受伤
|
||
|
||
`HRBPTower`(HRBP,80HC):
|
||
- 视觉:粉色,心形图案
|
||
- 攻击:无直接伤害(辅助塔)
|
||
- 特殊技能"打鸡血":每0.5秒给周围1格内的塔攻速+20%(叠加BUFF),每次BUFF消耗自身5精力
|
||
- 辅助方式:周期性扫描周围格子,找到塔就给BUFF
|
||
|
||
**`game/towers/TowerManager.ts`**:
|
||
- 维护所有已部署塔的数组
|
||
- 处理建塔请求:检查格子合法性(非路径+未占用),扣除HC
|
||
- 每帧调用所有塔的 update(delta, enemies)
|
||
- 处理塔的点击事件(显示购买咖啡的提示)
|
||
|
||
**`game/ui/TowerPanel.ts`**(底部塔选择面板,在 Phaser Scene 内用 DOM 或 Phaser.GameObjects 实现):
|
||
- 优先使用 HTML DOM 覆盖层(`document.createElement`),绝对定位在 Canvas 底部
|
||
- 4个塔卡片,显示:像素风格图标 + 名称 + 成本
|
||
- 当前选中的塔有高亮边框
|
||
- HC 不足时显示灰色不可点击状态
|
||
- 点击塔卡片后,鼠标进入"建塔模式",下一次点击地图格子时放置塔
|
||
|
||
---
|
||
|
||
### 怪物波次系统
|
||
|
||
**`game/enemies/EnemyBase.ts`**:
|
||
```typescript
|
||
export abstract class EnemyBase {
|
||
protected scene: Phaser.Scene
|
||
protected sprite: Phaser.GameObjects.Graphics
|
||
protected healthBar: Phaser.GameObjects.Graphics
|
||
protected quoteText: Phaser.GameObjects.Text // 头顶语录
|
||
|
||
public maxHp: number
|
||
public hp: number
|
||
public speed: number // 像素/秒
|
||
public readonly kpiDamage: number // 到达终点扣除的 KPI
|
||
public readonly hcReward: number // 死亡奖励的 HC
|
||
|
||
// 路径相关
|
||
protected pathPoints: { x: number, y: number }[] // 像素坐标路径
|
||
protected currentPathIndex: number = 0
|
||
protected distanceTraveled: number = 0
|
||
|
||
// 状态
|
||
public isDead: boolean = false
|
||
public isActive: boolean = true
|
||
public dotEffects: { damage: number, duration: number, timer: number }[] = [] // DOT效果
|
||
public slowEffect: number = 0 // 减速百分比(0~1)
|
||
public slowTimer: number = 0
|
||
public shieldCount: number = 0 // 护盾层数(老员工专用)
|
||
|
||
constructor(scene, pathPoints) { ... }
|
||
|
||
update(delta: number): void {
|
||
if (this.isDead || !this.isActive) return
|
||
|
||
// 处理DOT效果
|
||
this.processDOT(delta)
|
||
|
||
// 处理减速
|
||
if (this.slowTimer > 0) {
|
||
this.slowTimer -= delta
|
||
if (this.slowTimer <= 0) this.slowEffect = 0
|
||
}
|
||
|
||
// 沿路径移动
|
||
this.moveAlongPath(delta)
|
||
|
||
// 更新血条位置
|
||
this.updateHealthBar()
|
||
this.updateQuoteText()
|
||
}
|
||
|
||
protected moveAlongPath(delta: number): void {
|
||
if (this.currentPathIndex >= this.pathPoints.length - 1) {
|
||
// 到达终点
|
||
this.reachEnd()
|
||
return
|
||
}
|
||
|
||
const target = this.pathPoints[this.currentPathIndex + 1]
|
||
const currentSpeed = this.speed * (1 - this.slowEffect)
|
||
const distance = currentSpeed * delta / 1000
|
||
|
||
// 向目标移动
|
||
...向 target 方向移动 distance 像素...
|
||
|
||
// 到达当前路径点后切换到下一个
|
||
...
|
||
}
|
||
|
||
takeDamage(damage: number): void {
|
||
if (this.shieldCount > 0) {
|
||
this.shieldCount--
|
||
return // 护盾吸收
|
||
}
|
||
this.hp -= damage
|
||
this.updateHealthBar()
|
||
if (this.hp <= 0) {
|
||
this.die()
|
||
}
|
||
}
|
||
|
||
addDOT(damage: number, duration: number): void {
|
||
this.dotEffects.push({ damage, duration, timer: duration })
|
||
}
|
||
|
||
addSlow(percent: number, duration: number): void {
|
||
this.slowEffect = Math.max(this.slowEffect, percent)
|
||
this.slowTimer = Math.max(this.slowTimer, duration)
|
||
}
|
||
|
||
protected die(): void {
|
||
this.isDead = true
|
||
const manager = GameManager.getInstance()
|
||
manager.addHC(this.hcReward)
|
||
this.onDeath() // 子类可重写(碰瓷王扣HC)
|
||
this.destroy()
|
||
}
|
||
|
||
protected reachEnd(): void {
|
||
const manager = GameManager.getInstance()
|
||
manager.reduceKPI(this.kpiDamage)
|
||
this.isDead = true
|
||
this.destroy()
|
||
}
|
||
|
||
protected onDeath(): void {} // 子类重写
|
||
|
||
abstract drawSprite(): void
|
||
abstract getQuote(): string // 返回该怪物的语录
|
||
|
||
destroy(): void {
|
||
this.sprite?.destroy()
|
||
this.healthBar?.destroy()
|
||
this.quoteText?.destroy()
|
||
}
|
||
}
|
||
```
|
||
|
||
**4 种怪物:**
|
||
|
||
`FreshGraduate`(校招应届生):
|
||
- HP: 30, 速度: 120px/s, KPI扣减: 2%, HC奖励: 10
|
||
- 护盾: 0
|
||
- 视觉:小绿点(Graphics 小圆)
|
||
- 语录随机:["求转正!", "我愿意加班!", "卷!卷!卷!"]
|
||
|
||
`OldEmployee`(35岁老员工):
|
||
- HP: 150, 速度: 50px/s, KPI扣减: 8%, HC奖励: 30
|
||
- 护盾: 3层(shieldCount=3,前3次攻击无效)
|
||
- 视觉:大蓝方块
|
||
- 语录:["我为公司立过功!", "我有10年经验!", "年龄不是问题!"]
|
||
|
||
`TroubleMaker`(职业碰瓷王):
|
||
- HP: 80, 速度: 80px/s, KPI扣减: 5%, HC奖励: 20
|
||
- 特殊:死亡时重写 `onDeath()` 扣玩家20 HC(劳动仲裁)
|
||
- 视觉:红色叹号形状
|
||
- 语录:["录音笔已开启", "这是违法的!", "我要仲裁!"]
|
||
|
||
`BossVP`(空降VP):
|
||
- HP: 800, 速度: 40px/s, KPI扣减: 30%, HC奖励: 150
|
||
- 技能"组织架构调整":每20秒触发一次,随机摧毁场上一个防御塔
|
||
- 视觉:大金色六边形,周围有特效
|
||
- 语录:["我来教大家怎么做事", "你们缺乏战略眼光", "这不是执行力的问题"]
|
||
- BOSS出现时:全屏红色闪光效果(Phaser Camera flash)
|
||
|
||
**`game/enemies/EnemyPool.ts`**(对象池):
|
||
简单实现:维护一个 inactive 数组,acquire 时取出复用,release 时放回。
|
||
|
||
**`game/enemies/WaveManager.ts`**(波次管理器):
|
||
```typescript
|
||
// 6波配置
|
||
const WAVE_CONFIG = [
|
||
{ enemies: [{ type: 'FreshGraduate', count: 10, interval: 800 }] }, // 第1波
|
||
{ enemies: [{ type: 'FreshGraduate', count: 8, interval: 800 }, { type: 'OldEmployee', count: 3, interval: 2000 }] },
|
||
{ enemies: [{ type: 'OldEmployee', count: 5, interval: 2000 }, { type: 'TroubleMaker', count: 3, interval: 1500 }] }, // 周报前
|
||
{ enemies: [{ type: 'FreshGraduate', count: 12, interval: 600 }, { type: 'TroubleMaker', count: 2, interval: 1500 }] },
|
||
{ enemies: [{ type: 'OldEmployee', count: 6, interval: 1500 }, { type: 'TroubleMaker', count: 3, interval: 1500 }] },
|
||
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 4, interval: 2000 }] }, // Boss波
|
||
]
|
||
|
||
export class WaveManager {
|
||
private currentWave: number = 0
|
||
private isSpawning: boolean = false
|
||
private waveComplete: boolean = false
|
||
|
||
// 周报触发:第3波结束后
|
||
private onWave3Complete: () => void
|
||
// 胜利:第6波全部消灭
|
||
private onAllWavesComplete: () => void
|
||
|
||
startNextWave(): void { ... }
|
||
private spawnEnemy(type: string): EnemyBase { ... }
|
||
update(delta: number): void { ... } // 每帧更新所有活跃怪物
|
||
|
||
getAllActiveEnemies(): EnemyBase[] { ... }
|
||
}
|
||
```
|
||
|
||
**路径像素坐标计算**:
|
||
PATH_WAYPOINTS 是格子坐标,需要转换为 Phaser Canvas 像素坐标:
|
||
```typescript
|
||
// 格子坐标转像素坐标(格子中心)
|
||
function gridToPixel(gridX, gridY): { x, y } {
|
||
return {
|
||
x: gridX * TILE_SIZE + TILE_SIZE / 2,
|
||
y: gridY * TILE_SIZE + TILE_SIZE / 2 + HUD_HEIGHT // 加上顶部HUD高度
|
||
}
|
||
}
|
||
```
|
||
|
||
完整路径像素坐标通过对 PATH_WAYPOINTS 相邻点之间插值生成:
|
||
```typescript
|
||
// 将 PATH_WAYPOINTS 扩展为完整的转折路径(每个转折点都在路径中)
|
||
function buildFullPath(): { x, y }[] {
|
||
const points = []
|
||
for (let i = 0; i < PATH_WAYPOINTS.length - 1; i++) {
|
||
const from = gridToPixel(PATH_WAYPOINTS[i].x, PATH_WAYPOINTS[i].y)
|
||
const to = gridToPixel(PATH_WAYPOINTS[i+1].x, PATH_WAYPOINTS[i+1].y)
|
||
points.push(from)
|
||
points.push(to)
|
||
}
|
||
// 去重相邻相同点
|
||
return points.filter((p, i, arr) =>
|
||
i === 0 || p.x !== arr[i-1].x || p.y !== arr[i-1].y
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### HUD 与战斗整合
|
||
|
||
更新 `game/GameScene.ts`,集成两个新系统:
|
||
1. 在场景 `create()` 中初始化 TowerManager 和 WaveManager
|
||
2. 在场景 `update(time, delta)` 中:
|
||
- 调用 `towerManager.update(delta, waveManager.getAllActiveEnemies())`
|
||
- 调用 `waveManager.update(delta)`
|
||
- 更新 HUD(KPI条、HC显示)
|
||
3. 处理格子点击事件:若处于"建塔模式",调用 `towerManager.placeTower(gridX, gridY, selectedTowerType)`
|
||
4. 添加"开始下一波"按钮(按钮文字"召唤下一波")
|
||
5. 波次开始时显示波次提示文字("第X波来袭!")
|
||
|
||
---
|
||
|
||
### 视觉规范(Graphics 绘制指南,无美术资源)
|
||
|
||
所有游戏对象用 Phaser.GameObjects.Graphics 绘制:
|
||
|
||
| 对象 | 形状 | 颜色 |
|
||
|------|------|------|
|
||
| 00后实习生 | 小圆(r=12) + 十字形身体 | `#22C55E`(绿) |
|
||
| P6资深开发 | 方块(24x24) + `</>` 文字 | `#3B82F6`(蓝) |
|
||
| PPT大师 | 圆(r=16) + 圆心白点 | `#F59E0B`(橙) |
|
||
| HRBP | 菱形(16x16) | `#EC4899`(粉) |
|
||
| 校招应届生 | 小圆(r=8) | `#86EFAC`(浅绿) |
|
||
| 35岁老员工 | 方块(20x20) + 护盾外框 | `#93C5FD`(浅蓝) |
|
||
| 职业碰瓷王 | 三角形 | `#FCA5A5`(浅红) |
|
||
| 空降VP | 六边形(r=30) + 金色外框 | `#FBBF24`(金色) |
|
||
|
||
塔的精力条:宽 40px,高 4px,黄色 `#F59E0B`,底部深灰 `#374151`
|
||
怪物血条:宽 30px,高 4px,绿→红渐变(按血量百分比)
|
||
|
||
---
|
||
|
||
## 验收标准
|
||
|
||
1. 可以在地图格子上部署 4 种塔,扣除正确 HC
|
||
2. 点击已部署的塔显示精力信息,可以花 10HC 购买咖啡
|
||
3. 点击"召唤下一波"按钮,怪物从起点出现并沿 S 型路径移动到终点
|
||
4. 塔会自动攻击射程内的怪物
|
||
5. 怪物被消灭后奖励 HC,到达终点后扣除 KPI
|
||
6. KPI 条和 HC 显示实时更新
|
||
7. 35岁老员工前 3 次攻击不掉血(护盾)
|
||
8. 职业碰瓷王死亡时扣除玩家 20 HC
|
||
9. 完整 6 波可以正常触发
|
||
|
||
## 相关上下文
|
||
- 设计系统:`design-system/大厂保卫战/MASTER.md`
|
||
- PRD:`.docs/prd-tower-defense-game.md`
|
||
- 计划文档:`.docs/plans/2026-03-21-tower-defense-game.md`
|
||
- Task 1A+1B 的产出(已在 `game/` 目录中)
|
||
- 开发规范:`@rules/dev-best-practices.md`(**必须先阅读**)
|
||
|
||
完成开发后,按 `@rules/atomic-commit.md` 规范,将变更按逻辑模块分组提交(禁止 `git add .`),确保所有文件均已 commit 后再结束任务。
|