diff --git a/.docs/tasks/task-multi-map-difficulty.md b/.docs/tasks/task-multi-map-difficulty.md new file mode 100644 index 0000000..a13e547 --- /dev/null +++ b/.docs/tasks/task-multi-map-difficulty.md @@ -0,0 +1,292 @@ +# 任务:多地图 + 难度系统 + 场景化改造 + +## 目标 +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 = { + 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 .`。