# 塔防战斗 — 项目层接入指南 > **旧 BattleCore 已移除**,逻辑内核为 MX `BattleSession` + 项目层 `BattleFacade`。 > 战斗 UI 脚本:`assets/script/ui/BattleGame/UIBattleGame.ts`(保留 `@ccclass('UIBattleGame')` 以兼容预制体)。 > 怪物表现组件仍在 `ui/BattleGame/Monster.ts`(prefab 绑定),通过 `IBattleMonsterView` 契约与 battle 层解耦。 > 外围 UI(结算/技能选择/机制等)直接读写 `BattleRuntime` 上的状态字段与桥接方法。 本文说明:**如何配地图、如何配表、如何初始化战斗**。 逻辑内核在 MX 通用模块(`assets/mx/module/battle`),项目层在 `assets/script/battle`。 **需求与架构约束**:[`docs/需求说明.md`](./docs/需求说明.md) **技能逻辑层方案**:[`docs/方案-战斗技能逻辑层.md`](../../../docs/方案-战斗技能逻辑层.md) ### 术语速查 | 说法 | 配表 | 含义 | |------|------|------| | **技能** | `Skill` / `ITableSkill` | 战斗技能(普攻/主动/被动) | | **普攻** | `Hero.atk_id[level-1]` | 随合成等级 | | **英雄技能** | `Hero.act_skill[level-1]` | 可无 | | **武器技能** | `Weapon.act_skill` | 局外装备;`type=1` 主动 / `type=2` 被动 | --- ## 一、架构概览 ``` assets/mx/module/battle/ ← 通用战斗内核(可跨项目复用,无 Cocos 依赖) assets/script/battle/ ← 本项目适配层 BattleFacade.ts ← 唯一对外入口 config/ ← 配表 → MX 配置 runtime/ ← 战斗运行时 map/ ← 地图规格 & 预制体加载 view/ ← Cocos 表现桥接(Presenter、契约接口) assets/script/ui/BattleGame/ ← 战斗 UI + 怪物 prefab 脚本(Monster) ``` **数据流:** ``` Excel/JSON 配表 → gg.data.table → TableBattleConfigProvider + ChapterLevelBuilder → MX BattleSession(放置、出怪、技能、伤害) → BattleEventBus(UnitSpawned / WaveStart / …) → CocosBattleMonsterPresenter → Monster + MapAgent ``` **出怪与移动(表现层):** ``` WaveSpawner.tick → UnitSpawned → CocosBattleMonsterPresenter.spawnVisual → spawnPoint_N 世界坐标(精确,不偏移) → Monster.bindPathTarget → MapAgent.setTarget → MapManager 8 方向距离场(getPathShortestTargetGrid) → MapAgent.move(COMBAT 阶段 CanMove=true) ``` 地图须满足制作规范(`MapManager.validateMapPoints` 在 `initMapGrids` 后校验): - 出怪口 `spawnPoint_N` 落在 **可走格** 上 - 攻击点(目标点)可映射到最近可走格(城墙节点常在不可走格,属正常设计) - 地图就绪后广播 `GEvent.MapGridsReady`,禁止用定时器重试寻路 --- ## 二、如何配地图 ### 2.1 格子规格(全局常量) | 项 | 值 | 说明 | |----|-----|------| | 行数 | **12** | 竖向 | | 列数 | **10** | 横向 | | 格大小 | **75×75** px | 与 `BattleMapSpec` 一致 | 代码常量:`assets/script/battle/map/BattleMapSpec.ts` ### 2.2 地图预制体 - **路径**:`assets/bundles/battleMap/prefab/地图/mapLayer{mapId}.prefab` - **命名**:`mapLayer1`、`mapLayer2` … 与 **Chapter 表 `mapId`** 对应 - **根节点**:挂 `MapManager` 组件,字段必须为: - `mapRows = 12` - `mapCols = 10` - `mapGridSize = 75` ### 2.3 预制体节点结构 ``` mapLayer{N} ├── 格子 ← 120 个子节点,每个挂 MapGrid ├── 出怪口 ← spawnPoint_1 … spawnPoint_N(顶部出怪) ├── 目标点 ← 怪物攻击终点(城墙侧) └── …(装饰层可选) ``` **MapGrid 常用标记:** | 字段 | 单人闯关 | 说明 | |------|----------|------| | `isCanPutObstacle` / 可建造 | 全图可建区域 | 塔防改为 `isCanPutUnit` 语义(后续 MapGrid 扩展) | | `isCanWalk` | 出怪路径可走 | 放塔默认 **不挡路** | | `isCanRemove` | 是否可铲除 | 按关卡设计 | ### 2.4 单人 vs 双阵营 | 模式 | 可编辑区域 | 显示 | |------|------------|------| | **solo** | 全图 | 逻辑坐标即显示坐标 | | **pvp** | 全图(落子按 `CampAreaID` 隔离) | 逻辑坐标即显示坐标;双方各一玩家槽 | 阵营对抗用同一张全图 + `CampAreaPolicy`,不再做半图镜像。 ### 2.5 新建地图步骤 1. 复制 `mapLayerTemplate.prefab` → `mapLayer{新id}.prefab` 2. 设置 `MapManager`:12 / 10 / 75 3. 在「格子」下摆放 120 个 `mapGrid` 子预制体(12 行 × 10 列) 4. 配置出怪口、目标点位置 5. 在 **Chapter 表** 把 `mapId` 设为该 id ### 2.6 运行时加载 ```typescript import { CocosBattleMapHost } from 'db://assets/script/battle'; // 组件方式:挂 CocosBattleMapHost,调用 loadMap(mapId) mapHost.loadMap(chapterMapId); ``` 或由 `CocosBattleRoot` 在 `onLoad` 时自动按当前章节 `mapId` 加载。 --- ## 三、如何配表 ### 3.1 必须注册的表名 在 `ConfigTableData.ts` → `TableNames`: | 表名 | 文件 | 用途 | |------|------|------| | `Chapter` | `Chapter.json` | 章节、mapId、出怪串、经济 | | `Weapon1` | `Weapon1.json` | **塔单位数值**(攻击、CD、射程) | | `Weapon2` | `Weapon2.json` | 棋子/字块类型(背包 UI,type 1/2/3/4) | | `BagWeapon` | `BagWeapon.json` | 背包棋子池、权重 | | `Monster` | `Monster.json` | 敌人属性(HP、速度、模型) | ### 3.2 Weapon1 — 塔战斗数值 每个可放置的攻击单位一条: ```json { "id": 1, "name": "刀", "weapon_para": 1, "cd": 1, "range": 200, "critical_chance": 1000, "critical_dam": 20000, "size": 1, "group": [1] } ``` **映射规则(`TableBattleConfigProvider`):** | 字段 | 战斗含义 | |------|----------| | `weapon_para` | 攻击力基数 ×10 | | `cd` | 普攻间隔(秒) | | `range` | 索敌射程(**像素**,与 `base_atk.range` 一致;-1=全屏) | | `critical_*` | 暴击率 / 暴击伤害 | 运行时 unitId:`tower_w1_{id}`,技能 id:`ability_w1_{id}`。 ### 3.3 Weapon2 — 棋子/字块(背包) ```json { "id": 1, "type": 1, "resource": "刀", "name": "刀", "weight": 10, "unlock": 1 } ``` | type | 含义 | |------|------| | 1 | 单字武器 | | 2 | 组合字 | | 3 | 格子(扩展可建造区域) | | 4 | 金手指 | 背包逻辑在 UI/背包模块扩展;战斗放置时传入 **Weapon1 id**。 ### 3.4 BagWeapon — 背包池 ```json { "id": 11, "type": 1, "weaponid": 1, "weaponlevel": 1, "weight": 10 } ``` - `weaponid` → 对应 **Weapon1.id** - `weaponlevel` → 等级(后续扩展属性倍率) ### 3.5 Chapter — 关卡与出怪 关键字段: | 字段 | 说明 | |------|------| | `mapId` | 对应 `mapLayer{mapId}.prefab` | | `base_coin` | 初始银币(按难度数组或单值) | | `monster_group` / `hp_multiple` | 难度系数(项目层扩展 HP 用) | | `monster1` | 出怪端口 1(主端口) | | `monster2_1` … | 端口 2 按难度 | | `monster3_1` … | 端口 3 按难度 | **出怪串语法(`MonsterWaveParser`):** ``` 波1 | 波2 | 波3 ↑ 竖线分隔波次 波内:101,10,1;102,5,2 ↑ 分号 = 同波顺序段(前段出完再出下段) ↑ 逗号 = monsterId, 数量, 难度行(Monster2表) ``` 示例: ``` |13,6,10|12,3,10;10012,3,10 ``` - 第 1 波:空(`|` 开头) - 第 2 波:怪物 13 ×6,难度行 10 - 第 3 波:先 12×3,再 10012×3(顺序段) 敌人 unitId:`monster_{Monster表id}`,开战前自动从 `Monster.json` 生成 UnitDef。 ### 3.6 扩展技能 / Buff mx 内核只提供通用塔防原语(`DealDamage` / `Heal` / `ApplyBuff` / `SpawnProjectile` / `ModifyAttribute` 等,由 `registerBuiltinBattleEffects` 注册)。**业务特有效果在业务层注册,不改 mx**: ```typescript // assets/script/battle/registry/ProjectEffectTypes.ts export const ProjectEffect = { ChainLightning: 'ChainLightning', } as const; // assets/script/battle/registry/registerProjectEffects.ts import { EffectRegistry } from 'db://assets/mx/module/battle'; import { ProjectEffect } from './ProjectEffectTypes'; EffectRegistry.register(ProjectEffect.ChainLightning, (services, ctx, params) => { // services: BattleServices 聚合(world / damage / buffs / targets ...) // ctx: AbilityContext(caster / targets / rng ...) // params: 配表 effect.params const amount = services.damage.calc(ctx, params); for (const targetId of ctx.targets) { services.damage.apply(ctx.caster, targetId, amount, 'electric', ctx); } }); ``` `registerProjectEffects()` 在 `createBattleRuntime` 时自动调用(幂等)。配表 `effect.type` 填 `ProjectEffect` 中声明的字符串即可。 --- ## 四、如何初始化战斗 ### 4.1 游戏启动(一次) ```typescript import { BattleFacade } from 'db://assets/script/battle'; BattleFacade.init(); // 加载 Weapon1 → 塔单位映射 ``` 建议放在进入大厅前或 `GameController` 初始化处。 ### 4.2 进入关卡 ```typescript import { BattleFacade } from 'db://assets/script/battle'; import { ChapterDifficulty } from 'db://assets/script/manager/ChapterDataManager'; const battle = BattleFacade.start({ chapterId: 1, difficulty: ChapterDifficulty.Normal, mode: 'solo', // 或 'pvp' seed: 12345, // 可选,对战/回放 }); if (!battle) { // 章节不存在或配表错误 return; } ``` 内部流程: 1. `ChapterLevelBuilder.build` → 生成 `LevelDef`(波次、金币、12×10) 2. `TableBattleConfigProvider.registerLevel` → 注册敌人 Unit 3. `BattleFactory.create` → 创建 MX `BattleSession` 4. 默认进入 **PREP** 阶段 ### 4.3 加载地图(Cocos 场景) **方式 A — 场景挂组件(推荐)** 1. 战斗场景建空节点 `BattleRoot` 2. 挂 `CocosBattleRoot` + `CocosBattleMapHost` 3. Inspector 设置 `chapterId`、`mode` 4. 可选:`autoStartCombat`、`debugDeployWeapon1Id` 做快速测试 **方式 B — 代码** ```typescript import { CocosBattleMapHost } from 'db://assets/script/battle'; const battle = BattleFacade.start({ chapterId: 1, mode: 'solo' }); mapHost.loadMap(battle.mapId); ``` ### 4.4 部署阶段(PREP) ```typescript // 放置塔:Weapon1.id = 1 的「刀」在 (row=8, col=4) BattleFacade.deploy({ weapon1Id: 1, row: 8, col: 4, }); ``` ### 4.5 波次制流程(单人闯关) ``` 进关 → PREP(放置单位) → 玩家点「继续游戏」→ COMBAT(出当前波怪) → 本波怪出完且全部击杀 → PREP → 再点「继续游戏」→ 下一波 COMBAT → … 最后一波清空 → BattleEnd(win) ``` - **PREP**:可 `BattleFacade.deploy()` 放置塔(`PrepPhaseOnlyRule` 限制仅备战可放) - **COMBAT**:`WaveSpawner` 出怪;塔自动攻击(待表现层接入) - **胜利**:所有波次怪物击杀完毕(`onWaveCleared` → 无下一波 → `endBattle(true)`) ```typescript // 备战结束,玩家主动开战 BattleFacade.startCombat(); // 仅 PREP 有效,开始下一波 ``` ### 4.6 每帧驱动 ```typescript // CocosBattleRoot.update 已自动调用 BattleFacade.tick(dt); ``` ### 4.7 监听事件(UI / 表现) ```typescript BattleFacade.current?.on('UnitPlaced', (e) => { /* 创建塔 Node */ }); BattleFacade.current?.on('DamageDealt', (e) => { /* 飘字 */ }); BattleFacade.current?.on('WaveStart', (e) => { /* 波次 UI */ }); BattleFacade.current?.on('BattleEnd', (e) => { /* 结算 */ }); ``` ### 4.8 退出战斗 ```typescript BattleFacade.end(); ``` --- ## 五、目录速查 | 路径 | 职责 | |------|------| | `battle/BattleFacade.ts` | 对外 API | | `battle/runtime/BattleRuntime.ts` | 单次战斗生命周期 | | `battle/config/TableBattleConfigProvider.ts` | Weapon1/Monster → MX Unit | | `battle/config/ChapterLevelBuilder.ts` | Chapter → LevelDef | | `battle/config/MonsterWaveParser.ts` | 出怪串解析 | | `battle/map/BattleMapSpec.ts` | 12×10×75 常量 | | `battle/map/CocosBattleMapHost.ts` | 加载 mapLayer 预制体 | | `battle/view/CocosBattleMonsterPresenter.ts` | UnitSpawned → 怪物 Node | | `battle/view/IBattleMonsterView.ts` | 怪物视图组件契约 | | `battle/runtime/BattleRuntime.ts` | 运行时 + UI/NewMechanism 状态桥接 | | `ui/BattleGame/UIBattleGame.ts` | 战斗 HUD、地图加载、tick | | `ui/BattleGame/Monster.ts` | 怪物 prefab 视图 + 寻路绑定 | | `ui/BattleGame/MapAgent.ts` | 格子寻路移动 | --- ## 六、快速验证清单 - [ ] `Weapon1.json` 已打入 bundle,`TableNames.Weapon1` 已注册 - [ ] `Chapter.json` 中 `mapId` 指向存在的 `mapLayer{id}.prefab` - [ ] 地图 prefab `MapManager` = 12 / 10 / 75 - [ ] 战斗场景已挂 `CocosBattleRoot` - [ ] 调用 `BattleFacade.init()` 后再 `start()` - [ ] `deploy` 使用 **Weapon1.id**,不是 Weapon2.id --- ## 七、与 MX 通用模块关系 - **项目层只做**:读表、拼 Level、加载地图 prefab、Cocos 表现 - **MX 层负责**:ECS、放置规则、Ability/Effect、波次、伤害、单人/对战模式 跨项目复用时:**复制 `assets/mx/module/battle`**,在新项目实现各自的 `TableBattleConfigProvider` 等价物即可。 --- ## 八、编码规范(MX / 项目层 / UI 统一) | 规则 | 做法 | |------|------| | **禁止魔法字符串** | 事件用 `BattleEventType` + `BattleEventFactory`;标签用 `EntityTag`;Effect/Condition/Trigger 类型用业务层 `ProjectEffect` / `ProjectCondition` / `ProjectTrigger` 常量集(mx 内置原语直接用字符串字面量如 `'DealDamage'`) | | **配置 ID** | 使用 `UnitDefId` 等类型别名,构造 ID 用 `makeMonsterUnitDefId()` | | **import 路径** | MX 模块内跨目录用 `db://assets/mx/module/battle/...`,避免 IDE 报红 | | **文档** | 每个 public 类/模块须有文件头 JSDoc;复杂流程写接入文档数据流 | | **失败处理** | 配表/地图错误 `console.error` 并中止,不用静默 fallback |