You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
426 lines
14 KiB
426 lines
14 KiB
# 塔防战斗 — 项目层接入指南
|
|
|
|
> **旧 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 |
|
|
|