docs: 添加PRD需求文档、任务计划和设计系统
This commit is contained in:
226
.docs/tasks/task-1ab-game-foundation.md
Normal file
226
.docs/tasks/task-1ab-game-foundation.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Task 1A+1B: 游戏基础框架搭建
|
||||
|
||||
## 任务概要
|
||||
在现有 Next.js 项目中,安装 Phaser.js,改造主页(封面),创建游戏页面,实现 Phaser 游戏框架挂载 + S型地图渲染。
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### Step 1: 安装 Phaser.js
|
||||
```bash
|
||||
pnpm add phaser
|
||||
```
|
||||
|
||||
### Step 2: 改造 `app/page.tsx`(主页/游戏封面)
|
||||
完全重写主页,展示游戏封面。注意:**必须覆盖模板原有 UI,不保留任何模板样式**。
|
||||
|
||||
设计要求(参考 `design-system/大厂保卫战/MASTER.md`):
|
||||
- 背景色:`#0F0F23`(深夜宇宙蓝)
|
||||
- 标题字体:Press Start 2P(像素字体),颜色 `#A78BFA`(霓虹紫)
|
||||
- 副标题:VT323 字体
|
||||
- 主色 CTA 按钮(进入游戏):`#F43F5E` 红色
|
||||
- 效果:CRT 扫描线叠加层(`::before` 伪元素,repeating-linear-gradient 细黑线)
|
||||
- 霓虹发光效果:标题 `text-shadow` 双层发光
|
||||
- 装饰性文字:背景中随机排列大厂黑话(低透明度)
|
||||
|
||||
主页内容结构:
|
||||
```
|
||||
[全屏黑色背景]
|
||||
[CRT扫描线叠加层]
|
||||
[装饰黑话背景文字]
|
||||
[中央卡片]
|
||||
标题:大厂保卫战
|
||||
副标题:最后的打工人
|
||||
英文副标:THE LAST GRINDER
|
||||
描述:保住KPI,战胜空降VP
|
||||
[开始游戏] 按钮 → 跳转 /game
|
||||
[底部版权:像素风格]
|
||||
```
|
||||
|
||||
### Step 3: 创建 `app/game/page.tsx`(游戏页面)
|
||||
```typescript
|
||||
'use client'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function GamePage() {
|
||||
const gameRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let game: any = null
|
||||
|
||||
const initGame = async () => {
|
||||
const Phaser = (await import('phaser')).default
|
||||
const { createGameConfig } = await import('@/game/config')
|
||||
|
||||
if (gameRef.current && !game) {
|
||||
game = new Phaser.Game(createGameConfig('game-container'))
|
||||
}
|
||||
}
|
||||
|
||||
initGame()
|
||||
|
||||
return () => {
|
||||
game?.destroy(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen bg-[#0F0F23] overflow-hidden">
|
||||
<div id="game-container" className="w-full h-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: 创建游戏核心文件
|
||||
|
||||
**`game/constants.ts`**(游戏常量):
|
||||
```typescript
|
||||
export const MAP_COLS = 16
|
||||
export const MAP_ROWS = 12
|
||||
export const TILE_SIZE = 80 // 每格80px,总计 1280x960,Phaser会缩放
|
||||
export const GAME_WIDTH = 1280
|
||||
export const GAME_HEIGHT = 720
|
||||
|
||||
// S型路径格子坐标
|
||||
export const PATH_WAYPOINTS = [
|
||||
{ x: 0, y: 2 },
|
||||
{ x: 11, y: 2 },
|
||||
{ x: 11, y: 9 },
|
||||
{ x: 15, y: 9 },
|
||||
]
|
||||
|
||||
// 游戏初始数值
|
||||
export const INITIAL_HC = 200
|
||||
export const INITIAL_KPI = 100
|
||||
export const STAMINA_MAX = 100
|
||||
export const STAMINA_REGEN = 5 // 每秒恢复量
|
||||
export const COFFEE_COST = 10 // 瑞幸咖啡 HC 成本
|
||||
|
||||
// 颜色常量
|
||||
export const COLOR_PATH = 0x3d2b1f
|
||||
export const COLOR_BUILDABLE = 0x1e3a5f
|
||||
export const COLOR_HOVER = 0x2d5a8e
|
||||
export const COLOR_BORDER = 0x0a1628
|
||||
```
|
||||
|
||||
**`game/config.ts`**(Phaser 配置):
|
||||
```typescript
|
||||
import Phaser from 'phaser'
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from './constants'
|
||||
|
||||
export function createGameConfig(containerId: string): Phaser.Types.Core.GameConfig {
|
||||
return {
|
||||
type: Phaser.AUTO,
|
||||
width: GAME_WIDTH,
|
||||
height: GAME_HEIGHT,
|
||||
parent: containerId,
|
||||
backgroundColor: '#0a1628',
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||
},
|
||||
scene: [], // 在 GameScene 中动态加载
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: { debug: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`game/GameScene.ts`**(主游戏场景):
|
||||
|
||||
核心地图渲染逻辑:
|
||||
- 创建 16x12 格子网格
|
||||
- 根据 PATH_WAYPOINTS 计算路径格子集合(路径上的每个格子都是路径格)
|
||||
- 路径格子:深褐色(`0x3d2b1f`)
|
||||
- 可建塔格子:深蓝色(`0x1e3a5f`)
|
||||
- 格子间有1px间距
|
||||
- 鼠标悬停非路径格子时高亮
|
||||
- 点击非路径格子:触发建塔逻辑(Phase 1先只做UI提示)
|
||||
- 添加地图装饰性文字("面试间"、"财务室")
|
||||
- 场景中显示:顶部 HUD 区域(KPI进度条 + HC数值)
|
||||
|
||||
地图路径计算方式(不能只用 4 个坐标点,需要填充中间格子):
|
||||
```typescript
|
||||
// 将折线坐标展开为完整路径格子集合
|
||||
function buildPathTiles(waypoints): Set<string> {
|
||||
const tiles = new Set<string>()
|
||||
for (let i = 0; i < waypoints.length - 1; i++) {
|
||||
const from = waypoints[i]
|
||||
const to = waypoints[i + 1]
|
||||
// 水平或垂直连线
|
||||
if (from.x === to.x) {
|
||||
for (let y = Math.min(from.y, to.y); y <= Math.max(from.y, to.y); y++) {
|
||||
tiles.add(`${from.x},${y}`)
|
||||
}
|
||||
} else {
|
||||
for (let x = Math.min(from.x, to.x); x <= Math.max(from.x, to.x); x++) {
|
||||
tiles.add(`${x},${from.y}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return tiles
|
||||
}
|
||||
```
|
||||
|
||||
HUD 设计(Phaser Text/Graphics 实现,而非 HTML 层):
|
||||
- 顶部黑色半透明条(全宽,高度60px)
|
||||
- 左:游戏标题 "大厂保卫战" (Press Start 2P,小字号)
|
||||
- 中:KPI 进度条(宽300px,绿→黄→红渐变)
|
||||
- 右:HC 数值 "HC: 200"
|
||||
|
||||
**`game/GameManager.ts`**(游戏状态管理器):
|
||||
```typescript
|
||||
export class GameManager {
|
||||
private static instance: GameManager
|
||||
public hc: number = INITIAL_HC
|
||||
public kpi: number = INITIAL_KPI
|
||||
public currentWave: number = 0
|
||||
public gameState: 'idle' | 'playing' | 'paused' | 'victory' | 'defeat' = 'idle'
|
||||
|
||||
static getInstance(): GameManager { ... }
|
||||
|
||||
spendHC(amount: number): boolean { ... } // 扣除HC,不足返回false
|
||||
addHC(amount: number): void { ... }
|
||||
reduceKPI(amount: number): void { ... } // 减少KPI,归零触发失败
|
||||
|
||||
// 事件系统(用于 Scene 与 UI 通信)
|
||||
onHCChange: ((hc: number) => void)[] = []
|
||||
onKPIChange: ((kpi: number) => void)[] = []
|
||||
onGameOver: (() => void)[] = []
|
||||
onVictory: (() => void)[] = []
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: 更新 `app/layout.tsx`
|
||||
在 layout 中引入 Google Fonts(Press Start 2P + VT323):
|
||||
```tsx
|
||||
// 在 <head> 中添加
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323:wght@400&display=swap" rel="stylesheet" />
|
||||
```
|
||||
|
||||
### Step 6: 更新 `app/globals.css`
|
||||
添加游戏全局样式:
|
||||
- CSS 变量(颜色 token)
|
||||
- CRT 扫描线效果(供主页使用)
|
||||
- 霓虹发光 keyframes 动画
|
||||
- 像素字体 class
|
||||
|
||||
## 验收标准
|
||||
1. `pnpm dev` 启动成功,无编译错误
|
||||
2. 访问 `/`:显示游戏封面,有进入游戏按钮,霓虹效果正常
|
||||
3. 点击进入游戏跳转至 `/game`
|
||||
4. 访问 `/game`:渲染 16x12 格子地图,S型路径可见(深褐色路径,深蓝色可建区域)
|
||||
5. 鼠标悬停可建塔格子时高亮
|
||||
6. 顶部 HUD 显示 KPI 进度条和 HC 数值
|
||||
|
||||
## 相关上下文
|
||||
- 设计系统:`design-system/大厂保卫战/MASTER.md`
|
||||
- PRD:`.docs/prd-tower-defense-game.md`
|
||||
- 计划文档:`.docs/plans/2026-03-21-tower-defense-game.md`
|
||||
- 开发规范:`@rules/dev-best-practices.md`(**必须先阅读**)
|
||||
|
||||
完成开发后,按 `@rules/atomic-commit.md` 规范,将变更按逻辑模块分组提交(禁止 `git add .`),确保所有文件均已 commit 后再结束任务。
|
||||
427
.docs/tasks/task-1cd-towers-enemies.md
Normal file
427
.docs/tasks/task-1cd-towers-enemies.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# 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 后再结束任务。
|
||||
223
.docs/tasks/task-2ab-gameplay-completion.md
Normal file
223
.docs/tasks/task-2ab-gameplay-completion.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Task 2A+2B: 周报结算系统 + 特殊技能完善 + 胜利失败界面 + 怪物语录
|
||||
|
||||
## 任务概要
|
||||
在已完成的游戏基础框架上,实现周报结算弹窗(每3波触发)、完善各塔特殊技能、实现胜利/失败结算界面、以及怪物头顶语录飘字系统。
|
||||
|
||||
**前置条件**:Task 1A+1B+1C+1D 已完成,完整的游戏框架已运行。
|
||||
|
||||
## 需要了解的已有文件
|
||||
|
||||
先读取以下文件了解现有实现:
|
||||
- `game/GameManager.ts`(游戏状态,了解事件系统接口)
|
||||
- `game/GameScene.ts`(主场景,了解如何集成新UI)
|
||||
- `game/ui/HUD.ts`(已有HUD,了解胜利/失败通知已有实现)
|
||||
- `game/enemies/WaveManager.ts`(了解波次完成事件是如何触发的)
|
||||
|
||||
## 实现内容
|
||||
|
||||
### 1. 周报结算弹窗(`game/ui/WeeklyReportModal.ts`)
|
||||
|
||||
**触发时机**:第3波结束后(WaveManager 已有 `onWeeklyReport` 回调)
|
||||
|
||||
**弹窗设计**(全屏覆盖 DOM 层,仿钉钉/飞书全屏通知):
|
||||
```
|
||||
[全屏黑色半透明背景(z-index: 9999)]
|
||||
[中央弹窗卡片,深色背景 #1a1a2e,霓虹紫边框]
|
||||
标题:"📊 周报提交时间!"(用 img/SVG 图标代替 emoji,或纯文字)
|
||||
副标题:"请选择正确的互联网黑话,提交本周工作总结"
|
||||
[10秒倒计时进度条] — 顶部红色进度条,从满到空
|
||||
|
||||
3个选项卡片(随机从黑话题库选3个,其中1个是"正确答案"):
|
||||
[选项A] [选项B] [选项C]
|
||||
|
||||
答题结果区域(答题后显示):
|
||||
- 选对:绿色提示 "正确!获得奖励:+80 HC" 或 "全场精力已补满"
|
||||
- 选错/超时:红色提示 "答错了!老板视察3秒!全场塔禁锢!"
|
||||
```
|
||||
|
||||
**黑话题库**(在 `game/data/buzzwords.ts` 中定义100条):
|
||||
```typescript
|
||||
// 分为两类:正确答案类(互联网常见黑话)+ 干扰项
|
||||
export const BUZZWORDS = [
|
||||
// 正确答案(真实大厂黑话)
|
||||
"赋能", "对齐", "颗粒度", "打通", "闭环", "抓手", "生态", "赛道",
|
||||
"护城河", "降维打击", "底层逻辑", "顶层设计", "方法论", "复盘",
|
||||
"迭代", "串联", "拉齐", "落地", "沉淀", "跑通", "MVP", "ROI",
|
||||
"私域流量", "增量市场", "存量竞争", "心智占位", "势能积累",
|
||||
"组织赋能", "生态打通", "战略对齐", "价值共创", "协同增效",
|
||||
"降本增效", "提质增效", "精益管理", "敏捷开发", "极致体验",
|
||||
"用户心智", "品牌溢价", "流量变现", "私域运营", "全渠道",
|
||||
"中台建设", "数字化转型", "智能升级", "生态圈", "护城河",
|
||||
"弯道超车", "换道超车", "破圈", "出圈", "刷屏", "爆款",
|
||||
"现象级", "标杆案例", "对标", "copy from", "跑模型",
|
||||
// 干扰项(故意写错/不通顺的)
|
||||
"增熵赋能", "横向闭环", "负向对齐", "反底层逻辑",
|
||||
]
|
||||
|
||||
// 每次从 BUZZWORDS 随机取3个,其中第一个是"正确答案"
|
||||
// 随机打乱顺序后展示
|
||||
export function generateWeeklyOptions(): { options: string[], correctIndex: number } {
|
||||
const shuffled = [...BUZZWORDS].sort(() => Math.random() - 0.5)
|
||||
const correct = shuffled[0]
|
||||
const options = [correct, shuffled[1], shuffled[2]].sort(() => Math.random() - 0.5)
|
||||
return {
|
||||
options,
|
||||
correctIndex: options.indexOf(correct)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**倒计时逻辑**:
|
||||
- 10秒倒计时,使用 `setInterval`
|
||||
- 超时自动触发"选错"结果
|
||||
- 选择后立即停止倒计时,显示结果 2 秒后自动关闭弹窗
|
||||
|
||||
**结果逻辑**:
|
||||
```typescript
|
||||
// 选对:随机50%概率给HC,50%概率补满精力
|
||||
if (Math.random() > 0.5) {
|
||||
gameManager.addHC(50 + Math.floor(Math.random() * 50))
|
||||
showResult('success', `获得 ${reward} HC!继续加油!`)
|
||||
} else {
|
||||
// 通知所有塔补满精力(通过 GameScene 的回调)
|
||||
onFullStamina()
|
||||
showResult('success', '全场精力已补满!')
|
||||
}
|
||||
|
||||
// 选错/超时:触发全场塔禁锢3秒
|
||||
onBossInspection() // 调用 GameScene 的回调,禁锢所有塔
|
||||
showResult('error', '老板来视察了!全场禁锢3秒!')
|
||||
```
|
||||
|
||||
### 2. 完善胜利/失败界面(更新 `game/ui/HUD.ts` 或单独文件)
|
||||
|
||||
当前 HUD 可能已有简单的胜利/失败提示,需要升级为完整的模态弹窗。
|
||||
|
||||
**失败弹窗**(KPI 归零时触发,仿钉钉退群通知):
|
||||
```
|
||||
[弹窗容器,白色背景,仿钉钉卡片样式]
|
||||
[顶部蓝色条] 系统通知
|
||||
[钉钉风格图标]
|
||||
|
||||
正文:
|
||||
鉴于您近期的表现未能达成业务闭环,
|
||||
经公司管理层研究决定,
|
||||
对您进行"毕业"处理。
|
||||
请于5分钟内归还工牌,不要带走办公文具。
|
||||
|
||||
[领取 N+1] (重新开始,刷新页面)
|
||||
[申请仲裁] (返回主页,跳转 /)
|
||||
```
|
||||
|
||||
**胜利弹窗**(全6波清空后,仿绩效评级系统):
|
||||
```
|
||||
[弹窗容器,深色背景,金色边框]
|
||||
标题:绩效评级公示
|
||||
|
||||
最终KPI: XX%
|
||||
消灭怪物: XX只
|
||||
剩余HC: XX
|
||||
|
||||
绩效等级:
|
||||
- KPI >= 80%: S级 "超出预期"
|
||||
- KPI >= 60%: A级 "达成预期"
|
||||
- KPI >= 40%: B级 "基本达成"
|
||||
- KPI < 40%: C级 "未达预期"
|
||||
|
||||
等级对应奖励描述(黑色幽默):
|
||||
- S级:"恭喜晋升为高级打工人!福报已在路上..."
|
||||
- A级:"表现良好,明年涨薪3%(扣去通胀后-2%)"
|
||||
- B级:"还需努力,下月开始996"
|
||||
- C级:"已被纳入待优化名单"
|
||||
|
||||
[再战一局] (重新开始)
|
||||
[见好就收] (返回主页)
|
||||
```
|
||||
|
||||
### 3. 怪物头顶语录飘字(更新 `game/enemies/EnemyBase.ts`)
|
||||
|
||||
EnemyBase 中的 `quoteText` 字段已预留。现在实现完整的语录系统:
|
||||
|
||||
**`game/data/quotes.ts`**(100条语录分怪物类型):
|
||||
```typescript
|
||||
export const QUOTES = {
|
||||
FreshGraduate: [
|
||||
"求转正!", "我愿意加班!", "卷!卷!卷!",
|
||||
"内推有名额吗?", "实习补贴够买咖啡吗?",
|
||||
"我会CRUD!", "熟练掌握Word和Excel",
|
||||
"我有五年实习经验!", "大厂梦...", "期待来公司学习!",
|
||||
"我不需要工资,只需要经验!", "比同龄人卷!",
|
||||
],
|
||||
OldEmployee: [
|
||||
"我为公司立过功!", "我有10年经验!",
|
||||
"年龄不是问题!", "那时候还是我搭的架构",
|
||||
"当年我一个人顶三个人!", "这个需求做不了",
|
||||
"怎么可能这么快做完!", "文档?从来没有!",
|
||||
"测试?QA负责的!", "这是历史遗留问题",
|
||||
],
|
||||
TroubleMaker: [
|
||||
"录音笔已开启", "这是违法的!",
|
||||
"我要仲裁!", "劳动法第X条规定...",
|
||||
"我已咨询过律师", "保留证据中...",
|
||||
"你们的违规操作我都记录了", "看我不告你们!",
|
||||
],
|
||||
BossVP: [
|
||||
"我来教大家怎么做事", "你们缺乏战略眼光",
|
||||
"这不是执行力的问题", "格局太小了",
|
||||
"你们都不懂商业本质", "要有全局思维",
|
||||
"小事不过手,大事全拍板", "你们的方案需要颠覆性重构",
|
||||
"我在BAT做过这个", "先对齐一下认知",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**语录飘字动效**:
|
||||
- 怪物出生后 0.5 秒显示语录(随机从该类型的语录库中取)
|
||||
- 字体:VT323,白色
|
||||
- 文字在怪物头顶30px处显示
|
||||
- 3秒后淡出(alpha 从1到0的 Tween 动画)
|
||||
- 文字跟随怪物移动
|
||||
|
||||
### 4. 全场塔禁锢效果(`GameScene` 中添加)
|
||||
|
||||
当周报答错/超时时,触发"老板视察":
|
||||
```typescript
|
||||
// 在 GameScene 中添加
|
||||
freezeAllTowers(duration: number = 3000): void {
|
||||
this.towerManager.getAllTowers().forEach(tower => {
|
||||
tower.isFrozen = true
|
||||
// 视觉:塔变灰色
|
||||
setTimeout(() => {
|
||||
tower.isFrozen = false
|
||||
// 视觉:恢复正常颜色
|
||||
}, duration)
|
||||
})
|
||||
}
|
||||
|
||||
refillAllStamina(): void {
|
||||
this.towerManager.getAllTowers().forEach(tower => {
|
||||
tower.stamina = tower.maxStamina
|
||||
tower.isActive = true
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
在 TowerBase 的 `update()` 中检查 `isFrozen` 状态,如果为 true 则跳过攻击。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 第3波全部怪物消灭后,弹出周报弹窗,有10秒倒计时
|
||||
2. 选对答案:随机给予HC奖励或补满精力
|
||||
3. 选错/超时:全场塔3秒无法攻击(显示灰色)
|
||||
4. KPI归零时:弹出仿钉钉失败弹窗,两个按钮功能正确
|
||||
5. 第6波全部消灭后:弹出绩效评级胜利弹窗,根据最终KPI显示正确等级
|
||||
6. 所有怪物头顶有飘字语录,随怪物移动,3秒后淡出
|
||||
|
||||
## 相关上下文
|
||||
- 设计系统:`design-system/大厂保卫战/MASTER.md`
|
||||
- PRD:`.docs/prd-tower-defense-game.md`
|
||||
- 计划文档:`.docs/plans/2026-03-21-tower-defense-game.md`
|
||||
- 开发规范:`@rules/dev-best-practices.md`(**必须先阅读**)
|
||||
- 原子提交规范:`@rules/atomic-commit.md`
|
||||
|
||||
完成开发后,按 `@rules/atomic-commit.md` 规范,将变更按逻辑模块分组提交(禁止 `git add .`),确保所有文件均已 commit 后再结束任务。
|
||||
Reference in New Issue
Block a user