Files
test1/.docs/tasks/task-multi-map-difficulty.md

293 lines
12 KiB
Markdown
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.
# 任务:多地图 + 难度系统 + 场景化改造
## 目标
1. 主页新增难度选择(低/中/高),点击后带参数跳转游戏
2. 实现3张地图每5波切换一次地图1→地图2→地图3
3. 每张地图有专属背景图、路径形状、装饰物
4. 波次配置根据难度动态调整怪物数量、速度、Boss频率
## 素材已就绪
`/public/game-assets/` 目录中已有:
- `map1-bg.png``map2-bg.png``map3-bg.png` — 三张地图背景
- `deco-coffee.png``deco-monitor.png``deco-desk.png` — 装饰物
## 文件改动清单
### 1. `game/data/mapConfigs.ts`(新建)
定义3张地图的配置
```typescript
export type DifficultyLevel = 'easy' | 'normal' | 'hard'
export interface MapConfig {
id: number
name: string // 地图名称
bgKey: string // Phaser 加载的背景图 key
bgPath: string // 背景图路径
pathColor: number // 路径格颜色
buildColor: number // 可建格颜色
waypoints: readonly { x: number; y: number }[] // 路径折线坐标
decorations: { // 装饰物(固定摆放的非路径格)
key: string
col: number
row: number
}[]
labels: { col: number; row: number; text: string }[]
waveCount: number // 每张地图波次数固定5
// 每波配置(相对于 normal 难度的基础配置)
waves: WaveConfigEntry[]
}
export interface WaveConfigEntry {
enemies: {
type: 'FreshGraduate' | 'OldEmployee' | 'TroubleMaker' | 'BossVP'
count: number
interval: number // ms
}[]
}
// 难度倍率
export const DIFFICULTY_MULTIPLIER: Record<DifficultyLevel, {
enemyCount: number // 数量倍率
enemySpeed: number // 速度倍率
bossHp: number // Boss HP 倍率
hcReward: number // HC 奖励倍率(难度高奖励少)
}> = {
easy: { enemyCount: 0.7, enemySpeed: 0.8, bossHp: 0.7, hcReward: 1.2 },
normal: { enemyCount: 1.0, enemySpeed: 1.0, bossHp: 1.0, hcReward: 1.0 },
hard: { enemyCount: 1.4, enemySpeed: 1.3, bossHp: 1.5, hcReward: 0.8 },
}
// 地图1人力资源部HR走廊S型路径
// 路径:(0,2)→(11,2)→(11,9)→(15,9)
const MAP1: MapConfig = {
id: 1,
name: '人力资源部 · 降本增效',
bgKey: 'map1-bg',
bgPath: '/game-assets/map1-bg.png',
pathColor: 0x3d2b1f,
buildColor: 0x1e3a5f,
waypoints: [
{ x: 0, y: 2 },
{ x: 11, y: 2 },
{ x: 11, y: 9 },
{ x: 15, y: 9 },
],
decorations: [
{ key: 'deco-coffee', col: 3, row: 0 },
{ key: 'deco-monitor', col: 14, row: 1 },
{ key: 'deco-desk', col: 5, row: 5 },
{ key: 'deco-coffee', col: 9, row: 11 },
{ key: 'deco-monitor', col: 1, row: 7 },
{ key: 'deco-desk', col: 13, row: 4 },
],
labels: [
{ col: 0, row: 0, text: '面试间' },
{ col: 14, row: 10, text: '财务室' },
{ col: 6, row: 5, text: 'HR办公区' },
],
waveCount: 5,
waves: [
{ enemies: [{ type: 'FreshGraduate', count: 10, interval: 800 }] },
{ enemies: [{ type: 'FreshGraduate', count: 8, interval: 700 }, { type: 'OldEmployee', count: 2, interval: 2000 }] },
{ enemies: [{ type: 'OldEmployee', count: 4, interval: 1800 }, { type: 'TroubleMaker', count: 3, interval: 1500 }] },
{ enemies: [{ type: 'FreshGraduate', count: 12, interval: 600 }, { type: 'TroubleMaker', count: 3, interval: 1200 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 3, interval: 2000 }, { type: 'FreshGraduate', count: 5, interval: 800 }] },
],
}
// 地图2技术研发部Z型路径更复杂
// 路径:(0,1)→(8,1)→(8,5)→(3,5)→(3,10)→(15,10)
const MAP2: MapConfig = {
id: 2,
name: '技术研发部 · 996福报',
bgKey: 'map2-bg',
bgPath: '/game-assets/map2-bg.png',
pathColor: 0x1a3a2a,
buildColor: 0x0f2040,
waypoints: [
{ x: 0, y: 1 },
{ x: 8, y: 1 },
{ x: 8, y: 5 },
{ x: 3, y: 5 },
{ x: 3, y: 10 },
{ x: 15, y: 10 },
],
decorations: [
{ key: 'deco-monitor', col: 1, row: 3 },
{ key: 'deco-monitor', col: 10, row: 0 },
{ key: 'deco-coffee', col: 6, row: 7 },
{ key: 'deco-desk', col: 11, row: 3 },
{ key: 'deco-monitor', col: 5, row: 8 },
{ key: 'deco-coffee', col: 14, row: 7 },
{ key: 'deco-desk', col: 0, row: 6 },
{ key: 'deco-monitor', col: 13, row: 5 },
],
labels: [
{ col: 0, row: 0, text: '研发中心' },
{ col: 14, row: 11, text: '服务器机房' },
{ col: 5, row: 3, text: '格子间' },
{ col: 10, row: 8, text: 'Bug池' },
],
waveCount: 5,
waves: [
{ enemies: [{ type: 'FreshGraduate', count: 12, interval: 700 }, { type: 'OldEmployee', count: 2, interval: 2000 }] },
{ enemies: [{ type: 'OldEmployee', count: 5, interval: 1500 }, { type: 'TroubleMaker', count: 3, interval: 1200 }] },
{ enemies: [{ type: 'FreshGraduate', count: 15, interval: 500 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'OldEmployee', count: 4, interval: 1500 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 10, interval: 600 }, { type: 'TroubleMaker', count: 5, interval: 1000 }] },
],
}
// 地图3高管会议室W型路径最难
// 路径:(0,0)→(5,0)→(5,6)→(10,6)→(10,2)→(15,2) 先上后下再上
const MAP3: MapConfig = {
id: 3,
name: '高管会议室 · 最后决战',
bgKey: 'map3-bg',
bgPath: '/game-assets/map3-bg.png',
pathColor: 0x3d1f2b,
buildColor: 0x2d1a40,
waypoints: [
{ x: 0, y: 0 },
{ x: 5, y: 0 },
{ x: 5, y: 6 },
{ x: 10, y: 6 },
{ x: 10, y: 2 },
{ x: 15, y: 2 },
],
decorations: [
{ key: 'deco-desk', col: 2, row: 2 },
{ key: 'deco-desk', col: 8, row: 0 },
{ key: 'deco-coffee', col: 12, row: 5 },
{ key: 'deco-desk', col: 7, row: 9 },
{ key: 'deco-monitor', col: 2, row: 9 },
{ key: 'deco-coffee', col: 13, row: 9 },
{ key: 'deco-desk', col: 1, row: 4 },
{ key: 'deco-monitor', col: 14, row: 4 },
{ key: 'deco-desk', col: 8, row: 8 },
],
labels: [
{ col: 0, row: 1, text: '董事会' },
{ col: 14, row: 3, text: '董事长室' },
{ col: 7, row: 4, text: '会议室' },
],
waveCount: 5,
waves: [
{ enemies: [{ type: 'OldEmployee', count: 6, interval: 1500 }, { type: 'TroubleMaker', count: 4, interval: 1000 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'FreshGraduate', count: 12, interval: 600 }, { type: 'OldEmployee', count: 3, interval: 1500 }] },
{ enemies: [{ type: 'BossVP', count: 1, interval: 0 }, { type: 'TroubleMaker', count: 6, interval: 800 }, { type: 'OldEmployee', count: 4, interval: 1500 }] },
{ enemies: [{ type: 'BossVP', count: 2, interval: 5000 }, { type: 'OldEmployee', count: 5, interval: 1200 }, { type: 'FreshGraduate', count: 8, interval: 600 }] },
{ enemies: [{ type: 'BossVP', count: 2, interval: 3000 }, { type: 'TroubleMaker', count: 8, interval: 600 }, { type: 'OldEmployee', count: 6, interval: 1000 }] },
],
}
export const ALL_MAPS: MapConfig[] = [MAP1, MAP2, MAP3]
```
### 2. `game/constants.ts`(修改)
- 移除硬编码的 `PATH_WAYPOINTS``MAP_LABELS`(改为从 mapConfigs 动态读取)
- 保留其他常量不变
- 新增 `WAVES_PER_MAP = 5`
### 3. `game/GameManager.ts`(修改)
新增字段:
```typescript
public difficulty: DifficultyLevel = 'normal'
public currentMapIndex: number = 0 // 0=地图1, 1=地图2, 2=地图3
public totalWaveCleared: number = 0 // 总计已清理波次
```
`reset()` 时保留 difficulty重置其他字段。
新增方法:`setDifficulty(d: DifficultyLevel)`
### 4. `game/mapRenderer.ts`(修改)
- `buildPathTiles``PATH_TILES` 改为接受参数(不再使用全局常量)
- `drawAllTiles` 接受 `pathTiles` 参数
- 新增 `renderMapBackground(scene, bgKey)` — 绘制地图背景图(铺满地图区域)
- 新增 `renderDecorations(scene, decorations, pathTiles)` — 在非路径格随机摆放装饰物
- `getCellSize()` 不变
### 5. `game/enemies/WaveManager.ts`(重构)
接受动态波次配置,而非硬编码 WAVE_CONFIG
```typescript
constructor(
scene,
waveConfigs: WaveConfigEntry[], // 从外部传入,支持多地图
difficultyMultiplier, // 难度倍率
callbacks: { onWaveComplete, onAllWavesComplete, onDestroyRandomTower }
)
```
难度影响:
- `enemyCount *= multiplier.enemyCount`(向上取整)
- 怪物创建时将速度和HP通过参数传入子类
- Boss HP 乘以 `multiplier.bossHp`
### 6. `game/enemies/EnemyBase.ts`(修改)
构造函数新增 `speedMultiplier = 1.0``hpMultiplier = 1.0` 参数可选默认1子类创建时传入。
### 7. `game/GameScene.ts`(重构)
支持多地图流程:
```typescript
private currentMapIndex = 0
private mapConfigs = ALL_MAPS
// create() 时加载当前地图
// 5波清空后showMapClearModal → 延迟2s → loadNextMap()
// loadNextMap():销毁当前地图装饰物,重新 renderMapBackground/renderDecorations重置 waveManager
```
新增方法:
- `loadMap(mapConfig: MapConfig)` — 初始化地图背景、路径、装饰物、重置 WaveManager
- `onMapCleared()` — 5波清场后显示过关弹窗然后切换下一张地图
- 地图切换时:重新计算 PATH_TILES传给 tileGraphics 重绘)、清除装饰物、加载新背景
### 8. `game/ui/MapTransitionModal.ts`(新建)
地图切换过渡弹窗Phaser DOM或Canvas Text
```
过关!人力资源部已清场
KPI: 87% | 获得奖励: +100 HC
[下一关:技术研发部 996福报] (3秒自动关闭)
```
3秒后自动关闭并触发下一张地图加载。
### 9. `app/page.tsx`(修改)— 主页加难度选择
在「开始游戏」按钮前增加难度选择器(三个按钮:简单/普通/困难):
```tsx
// 选择难度后存入 localStorage游戏页面读取
localStorage.setItem('game-difficulty', selectedDifficulty)
```
难度卡片设计:
- 简单:绿色 — "带薪摸鱼,准点下班"
- 普通:蓝色 — "常规打工,偶有加班"
- 困难:红色 — "地狱模式007全开"
### 10. `app/game/page.tsx`(修改)
`useEffect` 初始化时从 localStorage 读取难度,通过 `window.__gameDifficulty` 传给场景。
## 注意事项
- 每张地图preload对应背景图key不同场景重用时用 `this.textures.exists(key)` 检查避免重复加载
- PATH_TILES 不再是模块级常量,改为每次渲染时动态计算后传参
- `mapRenderer.ts` 导出的 `PATH_TILES` 保持向后兼容先用地图1的配置初始化GameScene 内部用局部变量覆盖
- 地图背景图用 `scene.add.image(x, y, key).setDisplaySize(mapW, mapH).setDepth(-1).setOrigin(0,0)` 铺在地图区域HUD 下方)
- 装饰物图片 `setDisplaySize(cellW*0.7, cellH*0.7)``setDepth(1)``setAlpha(0.6)`,不影响塔的放置逻辑(只是视觉装饰)
- 地图切换时必须 destroy 掉所有装饰物 Phaser 对象,避免内存泄漏
## 验收标准
1. 主页有三个难度按钮,选中高亮,点击「开始游戏」带难度跳转
2. 游戏开始后可见地图背景图(不是纯色格子)
3. 装饰物(咖啡杯、电脑、桌子)散布在非路径格子上
4. 第5波清场后弹出过关弹窗3秒后自动切换到下一张地图
5. 地图2路径形状与地图1不同Z型 vs S型
6. 困难模式下怪物数量明显增多Boss更频繁出现
7. 三张地图全部通关后显示最终胜利界面
## 相关文件
- 需求文档:`.docs/prd-tower-defense-game.md`
- 设计系统:`design-system/大厂保卫战/MASTER.md`
- 开发规范:`@rules/dev-best-practices.md`**必须先阅读**
- 原子提交:`@rules/atomic-commit.md`
完成后按原子提交规范提交,禁止 `git add .`