1442 lines
68 KiB
Markdown
1442 lines
68 KiB
Markdown
# ROK 英雄系统业务规则(1:1 迁移依据)
|
||
|
||
> **来源**:`E:\Game\gmd\ROK` 服务端 Lua(snax `Hero.lua` proxy + `HeroLogic.lua` lualib)+ 客户端 C# Proxy/Mediator/Config + sproto 协议
|
||
>
|
||
> **用途**:作为 Survivors 工程 `cn.etetet.hero` 包实现的业务依据。**所有规则均从 ROK 实代码反推**,禁止凭空发挥。重写时若与本文档冲突,以 ROK 源码为准。
|
||
>
|
||
> **关键文件锚点**:
|
||
>
|
||
> - 服务端业务核心:`Server/server/game_server/logic/lualib/HeroLogic.lua`(923 行)
|
||
> - 服务端 RPC Handler:`Server/server/game_server/logic/service/proxy/Hero.lua`(521 行)
|
||
> - 服务端 Entity 结构:`Server/common/lualib/define/HeroDef.lua`
|
||
> - 服务端枚举:`Server/common/lualib/enum/HeroEnum.lua`
|
||
> - 服务端战力计算:`Server/common/lualib/cacle/HeroCacle.lua`
|
||
> - 协议定义:`Server/common/protocol/Protocol.sproto`(行 968–1107、2619–2627)+ `Common.sproto`(行 318–349 的 `.SkillInfo` / `.HeroInfo` / `.TalentTrees`)
|
||
> - 客户端 Proxy:`Client/Assets/Scripts/Hotfix/MVC/Proxy/HeroProxy.cs`(983 行)
|
||
> - 客户端 Notification:`Client/Assets/Scripts/Hotfix/MVC/CMD/HeroCmd.cs`
|
||
> - 客户端列表+详情:`Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Captain/CaptainMediator.cs`(1350 行)
|
||
> - 客户端配置类:`Client/Assets/Scripts/Hotfix/Config/Hero*Define.cs`(10 个)
|
||
> - 客户端错误码:`Client/Assets/Scripts/Hotfix/MVC/CMD/ErrorCode.cs` 行 149–176(3000–3027)
|
||
|
||
---
|
||
|
||
## 1. 整体业务范围
|
||
|
||
ROK 中英雄称作"统帅"(commander/captain),是 SLG 卡牌养成系统的核心。一个英雄绑定到军队上作为主将/副将出战。系统涵盖以下功能模块:
|
||
|
||
| # | 模块 | 一句话描述 |
|
||
|---|---|---|
|
||
| 1 | 招募(Summon) | 用对应碎片道具 (`getItem` × `getItemNum`) 兑换成统帅 Entity。**不存在直接抽卡获得"英雄本体"**——所有英雄都是"碎片够数即可招募"。 |
|
||
| 2 | 经验升级(Add Exp) | 用经验书道具增加 `exp`,到阈值升 `level`,受当前星级 `starLimit` 封顶。 |
|
||
| 3 | 升星(Star Up) | 用升星材料增加 `starExp`,溢出升 `star`;带幸运双倍机制;解锁高阶技能槽。 |
|
||
| 4 | 技能升级(Skill Level Up) | 消耗英雄招募同 ID 道具(多余碎片可用于升技能);**升级目标技能是随机选择未满级的技能**之一。 |
|
||
| 5 | 觉醒(Awake) | 前 4 个技能全满级时自动解锁第 5 个技能(觉醒技能,等级固定 1)。同时提供独立 RPC 手动触发。 |
|
||
| 6 | 天赋树(Talent Tree) | 3 套独立天赋页(`talentIndex` 1/2/3),逐节点学习;提供专精(Mastery)的累加加成。 |
|
||
| 7 | 天赋页切换 / 重置 / 改名 | 道具或钻石付费切换/重置;天赋页支持自定义名称。 |
|
||
| 8 | 装备穿戴(Equip / Take Off) | 8 个固定部位(头盔/胸甲/武器/手套/裤子/饰品 1/饰品 2/鞋子);同件装备切换英雄时自动从旧英雄卸下。 |
|
||
| 9 | 雕像兑换(Exchange) | 用"通用雕像"兑换为某英雄的招募碎片,前提是技能未全满。 |
|
||
| 10 | 战力计算(Power) | 基础分 + 等级分 + 技能分 + 天赋分(含专精)+ 装备分;任意改动后调 `cacleSyncHistoryPower`。 |
|
||
| 11 | 排序与分组 | 列表按 Rare / Star / Level / Power 排序;分三段:已拥有 / 可召唤 / 未集齐。 |
|
||
| 12 | 同步到部队 | 任意英雄改动会更新 `RoleArmy` 的战斗属性(技能、天赋属性、等级),通知战斗服务器。 |
|
||
| 13 | 行动力减免 | 主将通过技能/天赋影响军队行动力消耗(`vitalityReduction`)。 |
|
||
| 14 | 限时礼包触发 | 招募新英雄、英雄升级会触发 `RechargeLogic.triggerLimitPackage`(与英雄无强耦合,仅作业务关联)。 |
|
||
|
||
> **关键约束**:ROK **没有抽卡(gacha)作为英雄获取入口**。Tavern(酒馆)`Build_Tavern` 协议开宝箱产出的是**道具+英雄碎片**(`RewardInfo.heros: *Heros{heroId, num, isNew}`),玩家攒够碎片后**主动调 `Hero_SummonHero` 招募**。本质是"碎片养成"而非"抽英雄"。
|
||
|
||
---
|
||
|
||
## 2. Hero 数据模型
|
||
|
||
### 2.1 Hero Entity 字段(持久化)
|
||
|
||
来源:`HeroDef.lua` `defaultHeroAttr` + `Common.sproto` `.HeroInfo`。**全部字段持久化到 `MSM.d_hero[rid]`,由 `req.Add / req.Set / req.Get` 接口读写**。
|
||
|
||
| 字段名 | 类型 | 含义 | 默认值 | 持久化 |
|
||
|---|---|---|---|---|
|
||
| `heroId` | int | 统帅配置 ID(对应 `s_Hero` 表主键) | 0 | ✓ |
|
||
| `star` | int | 当前星级 | 0(招募时置为 `sHero.initStar`) | ✓ |
|
||
| `starExp` | int | 当前星级进度(升星经验) | 0 | ✓ |
|
||
| `level` | int | 当前等级 | 0(招募时置 1) | ✓ |
|
||
| `exp` | int | 当前等级经验 | 0 | ✓ |
|
||
| `summonTime` | int | 招募时间戳(os.time) | 0 | ✓ |
|
||
| `soldierKillNum` | int | 累计士兵击杀数 | 0 | ✓ |
|
||
| `savageKillNum` | int | 累计野蛮人击杀数 | 0 | ✓ |
|
||
| `skills` | `[]SkillInfo` | 已解锁技能列表 | `{}`(招募时按星级解锁初始技能) | ✓ |
|
||
| `talentPoint` | int | 天赋点(**遗留字段,实际不使用**,见 §4.6 说明) | 0 | ✓ |
|
||
| `talentTrees` | `map<int,TalentTrees>` | 三套天赋页数据,key = 1/2/3 | `{}` | ✓ |
|
||
| `talentIndex` | int | 当前激活的天赋页 (1/2/3) | 1 | ✓ |
|
||
| `head` | int | 头盔位 itemIndex(背包索引) | 0 | ✓ |
|
||
| `breastPlate` | int | 胸甲位 itemIndex | 0 | ✓ |
|
||
| `weapon` | int | 武器位 itemIndex | 0 | ✓ |
|
||
| `gloves` | int | 手套位 itemIndex | 0 | ✓ |
|
||
| `pants` | int | 裤子位 itemIndex | 0 | ✓ |
|
||
| `accessories1` | int | 饰品 1 位 itemIndex | 0 | ✓ |
|
||
| `accessories2` | int | 饰品 2 位 itemIndex | 0 | ✓ |
|
||
| `shoes` | int | 鞋子位 itemIndex | 0 | ✓ |
|
||
|
||
### 2.2 子结构
|
||
|
||
**`SkillInfo`**(`Common.sproto` 行 318):
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `skillId` | int | 技能配置 ID(对应 `s_HeroSkill`) |
|
||
| `skillLevel` | int | 当前技能等级(1 起) |
|
||
|
||
> 运行时 `getRoleAllHeroSkills` 会临时附加 `talent: true`(天赋衍生技能)、`deputySkill: true`(副将技能)这两个**不持久化**的标记位。
|
||
|
||
**`TalentTrees`**(`Common.sproto` 行 334):
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `index` | int | 分页索引 (1/2/3) |
|
||
| `talentTree` | `[]int` | 已学习的天赋节点 ID 列表(`s_HeroTalentGainTree` 表 ID) |
|
||
| `name` | string | 天赋页自定义名称(玩家可改) |
|
||
|
||
### 2.3 同一玩家可拥有英雄数量上限
|
||
|
||
**无显式上限**。`HeroLogic:addHero` 仅判断"是否已拥有该 heroId"——已拥有则把招募道具转为对应数量的同 ID 道具退回(碎片返还,见 §4.1)。可拥有英雄数 = 配置表 `s_Hero` 中 `listDisplay ≠ 1` 的条目数。
|
||
|
||
实际 `hero.json` 有 **46 条**(其中部分为 `listDisplay=1` 的怪物英雄不参与列表),玩家最多拥有的英雄数 ≈ 配置表中 `listDisplay=0` 的条数。
|
||
|
||
---
|
||
|
||
## 3. 配置表清单
|
||
|
||
> 来源:`Client/Assets/Scripts/Hotfix/Config/Hero*Define.cs`(C# 反序列化 DTO,与 ROK 服务端 `CFG.s_Hero*` 一一对应)+ 实际数据 `gmd\Unity\Assets\Bundles\Config\hero*.json`(46 / 300 / 6 / 49 / 261 / 811 / 16 条)。
|
||
|
||
### 3.1 HeroDefine(英雄基础,`hero.json` = 46 条)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 英雄 ID(主键,4 位,如 1000/1001/2007) |
|
||
| `l_nameID` | int | 名称语言包 ID |
|
||
| `civilization` | int | 所属文明 ID(对应 `s_Civilization`) |
|
||
| `l_appellationID` | int | 称号语言包 ID |
|
||
| `initStar` | int | 初始星级(招募时赋值给 `Hero.star`) |
|
||
| `star` | int | 星级上限(用于 `IsStarMaxLevel` 判断) |
|
||
| `rare` | int | 稀有度 1=普通/2=优秀/3=精英/4=史诗/5=传说 |
|
||
| `l_desID` | int | 英雄描述语言包 |
|
||
| `score` | int | 基础战力 |
|
||
| `getItem` | int | 招募所需道具 ID("招募碎片") |
|
||
| `getItemNum` | int | 招募所需道具数量(如 10) |
|
||
| `exchange` | int | 兑换通用雕像的道具 ID;0 = 不可兑换 |
|
||
| `l_heroLanguage` | `[]int` | 英雄台词语言包列表(点角色随机说话) |
|
||
| `getLimit` | int | 招募解锁的王国开服天数 |
|
||
| `skill` | `[]int` | 该英雄的 5 个技能 ID(索引 1-4 为普通技能,索引 5 为觉醒技能;可能少于 5) |
|
||
| `talent` | `[]int` | 该英雄的天赋节点 ID(3 个,对应 3 棵增益树 `gainTree`) |
|
||
| `listDisplay` | int | 列表显示标志;1=不显示(怪物英雄) |
|
||
| `heroIcon` | string | 头像 atlas 引用 |
|
||
| `heroModel` | string | Spine 模型 |
|
||
| `heroAction` | string | 序列帧动画 |
|
||
| `heroScene` | string | 英雄场景 prefab 名 |
|
||
| `heroScenePoint` | `[]float` | 召唤镜头位置(x/y/z) |
|
||
| `voiceOpening` | string | 开场白语音 |
|
||
| `voiceSelect` | string | 选中语音 |
|
||
| `voiceMove` | string | 移动语音 |
|
||
|
||
样本(ID=1000,文明 101,5 星传说):`getItem=509010001`,`getItemNum=10`(10 个碎片可招募),`star=6`,`skill=[100001,100002,100003,100004,100005]`,`talent=[100001,100002,100003]`。
|
||
|
||
### 3.2 HeroLevelDefine(等级表,`hero_level.json` = 300 条)
|
||
|
||
> 5 种稀有度(`rareGroup` 1-5)× 60 级 = 300 条,ID 公式 = `rare * 10000 + level - 1`(见 `HeroProxy.cs` 行 184)。
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 = `rareGroup * 10000 + lv - 1` |
|
||
| `rareGroup` | int | 稀有度组(1-5) |
|
||
| `lv` | int | 等级(1-60) |
|
||
| `exp` | int | **升至下一级**所需经验(满级时为 0 → `IsMaxLevel` 即 `exp==0`) |
|
||
| `soldiers` | int | 该等级总带兵量 |
|
||
| `score` | int | 该等级**总战力**(直接读取而非累加) |
|
||
| `starEffectData` | int | 该等级获得天赋点 |
|
||
|
||
### 3.3 HeroStarDefine(星级表,`hero_star.json` = 6 条)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 星级 1-6 |
|
||
| `starLimit` | int | 该星级允许的等级上限(1→10,2→20,3→30,4→40,5→50,6→60) |
|
||
| `rare5` | int | 5星英雄升下一星所需经验 |
|
||
| `rare4` | int | 4星英雄升下一星所需经验 |
|
||
| `rare3` | int | 3星英雄升下一星所需经验 |
|
||
| `rare2` | int | 2星英雄升下一星所需经验 |
|
||
| `rare1` | int | 1星英雄升下一星所需经验 |
|
||
| `starEffect` | int | 星级效果类型(1=普通;2=高阶,达到 5 星才解锁额外能力) |
|
||
| `l_starEffectID` | int | 星级效果语言包 |
|
||
| `starEffectData` | int | 星级效果参数(如天赋点:5★给 1 点,6★给 2 点) |
|
||
|
||
> 实际数值(来自 `hero_star.json`):
|
||
> - 1★→2★:所有稀有度 500 经验
|
||
> - 2★→3★:所有稀有度 1000 经验
|
||
> - 3★→4★:5/4 星 6500,3/2/1 星 5000
|
||
> - 4★→5★:5 星 25000,4 星 16000,3 星 12500,2 星 10000,1 星 8000
|
||
> - 5★→6★:5 星 65000,4 星 48000,3 星 30000,2 星 20000,1 星 10000
|
||
> - 6★:满级(rare1-5 均为 0)
|
||
|
||
### 3.4 HeroStarExpDefine(升星材料表,`hero_star_exp.json` = 49 条)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 |
|
||
| `rareGroup` | int | 该材料适用的英雄稀有度(必须与英雄 rare 一致) |
|
||
| `itemID` | int | 材料道具 ID |
|
||
| `exp` | int | 每个材料给的升星经验 |
|
||
| `lucky` | int | 每个材料贡献的幸运值(用于双倍判定) |
|
||
| `sure` | int | 是否为稀有保底材料(1=是,0=否) |
|
||
|
||
### 3.5 HeroSkillDefine(技能表,`hero_skill.json` = 261 条)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 技能 ID(4–6 位,如 100001) |
|
||
| `l_nameID` | int | 名称语言包 |
|
||
| `type` | int | 技能类型(0=被动 74 / 1=主动 40 / 2=触发 117 / 3=持续 13 / 4=觉醒 17) |
|
||
| `l_typeID` | int | 类型语言包 |
|
||
| `skillBattleID` | `[]int` | 战斗效果 ID 列表 |
|
||
| `anger` | int | 怒气需求 |
|
||
| `open` | int | 解锁条件(0=默认;其他参考 `unlockSkill`) |
|
||
| `l_openID` | int | 解锁条件语言包 |
|
||
| `awakenEnhance` | int | 觉醒后增强标志 |
|
||
| `icon` | string | 技能图标 |
|
||
| `levelUpCostItem` | int | 升级所需道具(旧字段,已被 `s_Hero.getItem` 替代) |
|
||
| `l_mesID` | int | 技能描述语言包 |
|
||
| `lvEffectDate1..4` + `l_lvEffect1..4` | `[]string` + int | 4 段升级效果描述与数值 |
|
||
| `armyEffect` | string | 战斗特效路径 |
|
||
| `sound` | string | 技能音效 |
|
||
|
||
### 3.6 HeroSkillEffectDefine(技能效果表,`hero_skill_effect.json` = 811 条)
|
||
|
||
> 主键查找公式:**`ID = skillId * 1000 + skillLevel`**(见 `HeroLogic.lua` 行 158、604)。
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 = `skillId * 1000 + level` |
|
||
| `group` | int | 技能 ID(去除 level 部分) |
|
||
| `level` | int | 技能等级 |
|
||
| `score` | int | **该技能等级提供的总战力** |
|
||
| `attrTypeNew` | `[]attrType` | 客户端属性类型枚举数组 |
|
||
| `attrType` | `[]string` | 服务端属性名数组 |
|
||
| `attrNumber` | `[]int` | 对应属性的数值数组(与 `attrType` 同长度) |
|
||
|
||
### 3.7 HeroSkillLevelDefine(技能升级消耗,`hero_skill_level.json` = 16 条)
|
||
|
||
> ID 100-115,对应"全英雄的总技能等级(去除技能数)+ 100",最大支持升 16 次。
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 (100-115) |
|
||
| `level` | int | 第几次升级 (1-16) |
|
||
| `costItem1` | int | 普通(1星)英雄消耗道具数 |
|
||
| `costItem2` | int | 优秀(2星)英雄消耗道具数 |
|
||
| `costItem3` | int | 精英(3星)英雄消耗道具数 |
|
||
| `costItem4` | int | 史诗(4星)英雄消耗道具数 |
|
||
| `costItem5` | int | 传说(5星)英雄消耗道具数 |
|
||
|
||
> 关键:消耗的道具就是该英雄的招募碎片 (`sHero.getItem`),因此**多余碎片可用于技能升级**。
|
||
|
||
### 3.8 HeroTalentDefine(天赋根节点,每英雄 3 条)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 |
|
||
| `l_talentID` | int | 天赋名语言包 |
|
||
| `icon1` | string | 小图标 |
|
||
| `gainTree` | string | 所属增益树 ID(**注意是字符串,需 `int.Parse`**) |
|
||
| `masteryGroupID` | int | 天赋专精组 ID |
|
||
| `l_tipsID` | int | tips 语言包 |
|
||
| `type` | int | 天赋类型(用于装备专属判定 `checkHeroTalent`) |
|
||
|
||
### 3.9 HeroTalentGainTreeDefine(天赋树节点表)
|
||
|
||
> 每个节点对应天赋页中一个可点的格子;客户端用 `HeroTalentGainTreeDefine.ID` 来标识 `talentTree` 数组里的元素。
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 节点 ID |
|
||
| `gainTree` | int | 所属树组 ID(与 `HeroTalent.gainTree` 对应) |
|
||
| `level` | int | 天赋等级 / 层数(用于前置依赖:必须先点 `level - 1` 同组节点) |
|
||
| `score` | int | 该节点战力 |
|
||
| `icon` | string | 图标 |
|
||
| `nameID` | int | 名字语言包 |
|
||
| `descID` | int | 描述语言包 |
|
||
| `battleSkillID` | int | 战斗内生效技能 ID(>0 时附加给主将) |
|
||
| `attrTypeNew` | `[]attrType` | 客户端属性枚举 |
|
||
| `attrType` | `[]string` | 服务端属性名 |
|
||
| `attrNumber` | `[]int` | 属性值 |
|
||
|
||
### 3.10 HeroTalentMasteryDefine(天赋专精)
|
||
|
||
> 同一 `group` + 累计点数(`needTalentPoint`)达到时激活,提供额外战力 + 属性 + 技能。
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 主键 |
|
||
| `group` | int | 所属增益树组 |
|
||
| `level` | int | 专精层(按累计点数排序) |
|
||
| `score` | int | 激活战力 |
|
||
| `name` | int | 名字语言包 |
|
||
| `descID` | int | 描述语言包 |
|
||
| `needTalentPoint` | int | 激活需要的累计天赋点数 |
|
||
| `battleSkillID` | int | 战斗内生效技能 |
|
||
| `attrType` / `attrTypeNew` / `attrNumber` | 同上 | 属性加成 |
|
||
|
||
### 3.11 HeroTalentTypeDefine(天赋类型,用于装备专属)
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `ID` | int | 类型 ID |
|
||
| `l_talentID` | int | 类型名语言包 |
|
||
| `icon` | string | 图标 |
|
||
| `equipIcon` | string | 装备天赋图标 |
|
||
|
||
### 3.12 全局/辅助配置(来自 `s_ZeroEmpty` / `s_Config`)
|
||
|
||
| Key | 含义 | 引用位置 |
|
||
|---|---|---|
|
||
| `Enum.ZeroEmptyType.HERO_SKILL_OPEN` | `[heroId][star] = skillId`,星级解锁哪个技能 | `unlockSkill` |
|
||
| `Enum.ZeroEmptyType.TALENT` | `[gainTree][nodeId] = true`,节点归属增益树 | `checkHeroStudyTalent` |
|
||
| `Enum.ZeroEmptyType.HERO_LEVEL_TALENT` | `[rare][level] = point`,等级影响的天赋点 | `checkTalentPoint` |
|
||
| `Enum.ZeroEmptyType.HERO_STAR_TALENT` | `[star] = point`,星级影响的天赋点 | `checkTalentPoint` |
|
||
| `s_Config.talentResetItemID` | 切换/重置天赋页的免费道具 ID | `ChangeTalentIndex` / `ResetTalent` |
|
||
| `s_Config.heroNameLimit` | 天赋页名称长度 `[最小, 最大]` | `ModifyTalentName` |
|
||
| `s_Config.equipTalentPromote` | 装备专属属性加成系数 | `getHeroEquipAttr` |
|
||
|
||
---
|
||
|
||
## 4. 业务规则详细(核心)
|
||
|
||
> 标注:`P*` = `Hero.lua` proxy 的 RPC handler;`L*` = `HeroLogic.lua` 函数。
|
||
|
||
### 4.1 招募 / 兑换英雄
|
||
|
||
#### 4.1.1 英雄获取来源(**真实业务流**)
|
||
|
||
ROK 中英雄获取链路如下,**绝非"抽卡"**:
|
||
|
||
```text
|
||
酒馆开宝箱 (Build_Tavern, type=1银箱 / 2金箱)
|
||
↓ 服务端随机算法
|
||
RewardInfo {
|
||
items: [道具] (碎片 / 升星石 / 装备 / 通用资源 ...)
|
||
heros: [Heros{heroId, num, isNew}] // 实际是给"碎片数量",但走专门通道展示
|
||
}
|
||
↓ 玩家攒够 sHero.getItemNum (默认 10) 碎片
|
||
玩家主动调 Hero_SummonHero(heroId)
|
||
↓ 服务端扣碎片 + addHero
|
||
HeroLogic:addHero → 同步 Hero_HeroInfo + 触发限时礼包 + 调整城防主将
|
||
```
|
||
|
||
碎片本身是个普通道具(如 `509010001`),存在背包里,可以通过 Tavern 宝箱、活动、商店、邮件等多渠道获得。**没有任何"直接给一个英雄实体"的入口**——所有英雄都必须经过 `Hero_SummonHero` 这一步。
|
||
|
||
`recruitLimit`:**ROK 源码无此字段**。判定逻辑仅有 `sHero.getLimit`(开服天数解锁)。
|
||
|
||
#### 4.1.2 重复招募处理(碎片返还)
|
||
|
||
如果调 `Hero_SummonHero` 时该英雄已存在(`getHero` 非空且非空表):
|
||
- **服务端 RPC 层会先验失败**:`Hero.lua` `SummonHero` 行 42-46 返回 `HERO_ALREADY_EXIST (3002)`。
|
||
- **但 `HeroLogic:addHero` 内部**还有一层兜底(行 41-52):如果已存在,把传入的本应该消耗的道具**转换成同 ID 道具加回背包**(实际是没扣到也没退到,因为 proxy 层先拒了)。这层兜底用于其他调用方(如直接发奖、活动给英雄)时的兼容。
|
||
|
||
#### 4.1.3 完整流程(伪代码)
|
||
|
||
```text
|
||
function SummonHero(rid, heroId):
|
||
// 步骤 1:参数校验
|
||
if not heroId: return ERR(HERO_ARG_ERROR)
|
||
sHero = CFG.s_Hero[heroId]
|
||
if not sHero: return ERR(CFG_ERROR)
|
||
|
||
// 步骤 2:开服天数校验
|
||
if getSelfNodeOpenDays() < sHero.getLimit: return ERR(HERO_OPEN_DAYS_NOT_ENOUGH)
|
||
|
||
// 步骤 3:是否已拥有
|
||
if HeroLogic:getHero(rid, heroId) is not empty: return ERR(HERO_ALREADY_EXIST)
|
||
|
||
// 步骤 4:碎片是否足够
|
||
if BagComponent.itemCount(sHero.getItem) < sHero.getItemNum:
|
||
return ERR(HERO_SUMMON_ITEM_NOT_ENOUGH)
|
||
|
||
// 步骤 5:扣碎片
|
||
ItemLogic:delItemById(rid, sHero.getItem, sHero.getItemNum, eventType=SUMMON_HERO_COST_ITEM)
|
||
|
||
// 步骤 6:创建英雄实体
|
||
HeroLogic:addHero(rid, heroId):
|
||
defHero = HeroDef:getDefaultHeroAttr()
|
||
defHero.heroId = heroId
|
||
defHero.star = sHero.initStar
|
||
defHero.level = 1
|
||
defHero.summonTime = os.time()
|
||
HeroLogic:unlockSkill(rid, heroId, initStar, defHero) // 按初始星级解锁初始技能
|
||
MSM.d_hero[rid].req.Add(rid, heroId, defHero) // 写库
|
||
HeroLogic:syncHero(rid, heroId, defHero, true) // 推 Hero_HeroInfo
|
||
RoleLogic:cacleSyncHistoryPower(rid, ..., recalc=true)
|
||
BuildingLogic:changeDefendHero(rid) // 自动调整城防主将
|
||
RechargeLogic:triggerLimitPackage(rid, NEW_HERO, sHero.rare)
|
||
ActivityRoleMgr.setActivitySchedule(rid, RECRUIT_HERO_COUNT, 1)
|
||
```
|
||
|
||
`unlockSkill` 步骤逻辑(`HeroLogic.lua` 行 391-406):
|
||
|
||
```text
|
||
function unlockSkill(rid, heroId, star, heroInfo):
|
||
heroConfig = s_ZeroEmpty[HERO_SKILL_OPEN]
|
||
if heroConfig[heroId][star] exists and star != 5:
|
||
skillId = heroConfig[heroId][star]
|
||
if not already in heroInfo.skills:
|
||
heroInfo.skills.append({skillId, skillLevel=1})
|
||
```
|
||
|
||
> 即配置表 `HERO_SKILL_OPEN` 定义"达到某星级解锁某技能"的映射。**第 5 星不通过此处解锁**(觉醒走另一通道)。
|
||
|
||
#### 4.1.4 涉及 RPC
|
||
|
||
| 协议 | ID | 用途 |
|
||
|---|---|---|
|
||
| `Hero_SummonHero` | 601 | 客户端主动招募 |
|
||
| `Hero_HeroInfo` | 30201 | 服务端推送新增/变化的英雄(含 `noShow` 标志屏蔽 UI 弹窗) |
|
||
|
||
#### 4.1.5 涉及错误码
|
||
|
||
`HERO_ARG_ERROR` (3000)、`CFG_ERROR`、`HERO_OPEN_DAYS_NOT_ENOUGH` (3000)、`HERO_ALREADY_EXIST` (3002)、`HERO_SUMMON_ITEM_NOT_ENOUGH` (3001)。
|
||
|
||
---
|
||
|
||
### 4.2 升级(经验)
|
||
|
||
#### 4.2.1 经验来源
|
||
|
||
- **唯一入口** `Hero_AddHeroExp` (602):使用 `s_Item.subType` 为"经验书"类的道具,按 `desData1` × 数量加经验(`Hero.lua` 行 160)。
|
||
|
||
#### 4.2.2 等级计算
|
||
|
||
- `s_HeroLevel` 主键 = `rareGroup * 10000 + level - 1`(注意 -1 偏移,见 `HeroProxy.cs` 行 184)
|
||
- **服务端按 `rare * 10000 + level` 查 `s_HeroLevel:Get` 的 `exp` 字段,即"升到下一级所需经验"**
|
||
- 当 `s_HeroLevel.exp == 0` 时为满级(`HeroProxy.IsMaxLevel`)
|
||
|
||
#### 4.2.3 升级时属性变化
|
||
|
||
ROK **不在升级时单独"刷属性"**——属性按需查表:
|
||
- 部队带兵量 = `s_HeroLevel.soldiers`
|
||
- 战力 = `s_HeroLevel.score` 直接读(非累加)
|
||
- 技能/天赋/装备的属性独立由 `getHeroAttr` 即时合并(见 §4.8)
|
||
|
||
升级会触发:
|
||
- `RechargeLogic:triggerLimitPackage(HERO_LEVEL_UP, rare, level)`
|
||
- `updateMapArmyHero` 同步到地图部队
|
||
- `ArmyLogic:updateArmyInfoOnHeroInfoChange` 更新军队信息
|
||
- `ActivityRoleMgr.setActivitySchedule(HERO_LEVEL_COUNT)`
|
||
- `cacleSyncHistoryPower` 重算战力
|
||
|
||
#### 4.2.4 等级与星级关系
|
||
|
||
**升级硬封顶 = 当前星级的 `s_HeroStar.starLimit`**(如 1 星英雄最多升到 10 级)。`addHeroExp` 行 412-414:
|
||
|
||
```text
|
||
if heroInfo.level == sHeroStar.starLimit: return false
|
||
```
|
||
|
||
到达上限后继续加经验:经验**清零**(行 434-436)而非保留。
|
||
|
||
#### 4.2.5 完整流程(伪代码)
|
||
|
||
```text
|
||
function AddHeroExp(rid, heroId, itemId, itemNum):
|
||
heroInfo = HeroLogic:getHero(rid, heroId)
|
||
sHeroStar = CFG.s_HeroStar[heroInfo.star]
|
||
if heroInfo.level == sHeroStar.starLimit: return ERR(HERO_LEVEL_LIMIT)
|
||
if BagComponent.itemCount(itemId) < itemNum: return ERR(ITEM_NOT_ENOUGH)
|
||
|
||
sItem = CFG.s_Item[itemId]
|
||
addExp = sItem.desData1 * itemNum
|
||
BagComponent.delItem(itemId, itemNum)
|
||
|
||
HeroLogic:addHeroExp(rid, heroId, addExp):
|
||
sHeroStar = CFG.s_HeroStar[heroInfo.star]
|
||
if heroInfo.level == sHeroStar.starLimit: return false
|
||
sHero = CFG.s_Hero[heroId]
|
||
heroLevelId = sHero.rare * 10000 + heroInfo.level
|
||
exp = CFG.s_HeroLevel[heroLevelId].exp
|
||
|
||
heroInfo.exp += addExp
|
||
while heroInfo.exp >= exp:
|
||
heroInfo.level += 1
|
||
RechargeLogic:triggerLimitPackage(HERO_LEVEL_UP, sHero.rare, level)
|
||
if heroInfo.level == sHeroStar.starLimit:
|
||
heroInfo.exp = 0
|
||
break
|
||
else:
|
||
heroInfo.exp -= exp
|
||
exp = CFG.s_HeroLevel[sHero.rare * 10000 + heroInfo.level].exp
|
||
|
||
setHero & syncHero
|
||
if uplevel:
|
||
cacleSyncHistoryPower / updateMapArmyHero / updateArmyInfo / setActivitySchedule
|
||
```
|
||
|
||
#### 4.2.6 RPC + 错误码
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_AddHeroExp` | 602 | `HERO_LEVEL_LIMIT` (3011), `ITEM_NOT_ENOUGH` |
|
||
|
||
---
|
||
|
||
### 4.3 升星
|
||
|
||
#### 4.3.1 升星材料
|
||
|
||
- 每次升星可一次性提交**最多 6 个**升星材料(`Hero.lua` 行 214)。
|
||
- 每个材料对应 `s_HeroStarExp` 表一条:
|
||
- `rareGroup`:必须与英雄稀有度 (`sHero.rare`) 一致
|
||
- `exp`:每个材料贡献的升星经验
|
||
- `lucky`:每个材料贡献的幸运值
|
||
- 计算公式(行 219-229):
|
||
|
||
```text
|
||
total_exp = Σ(material.exp × material.itemNum)
|
||
total_lucky = Σ(material.lucky × material.itemNum)
|
||
```
|
||
|
||
#### 4.3.2 幸运系数
|
||
|
||
`Hero.lua` 行 231-233:
|
||
|
||
```text
|
||
if total_lucky >= Random.Get(1, 100): // 即概率 = min(total_lucky, 100) %
|
||
total_exp *= 2
|
||
```
|
||
|
||
> 也就是说:lucky 直接作为"双倍触发概率(%)"。理论上 lucky ≥ 100 时必触发双倍,但 `Random.Get(1,100)` 的下限是 1,因此**lucky=100 时必触发**。
|
||
|
||
#### 4.3.3 升星后属性变化
|
||
|
||
属性按需查表,参考 §4.2.3。升星仅会:
|
||
- 解锁可能的新技能(`unlockSkill(rid, heroId, newStar)`,见 §4.1.3)
|
||
- 增加可用天赋点(通过 `s_ZeroEmpty.HERO_STAR_TALENT` 累加)
|
||
|
||
#### 4.3.4 升星上限
|
||
|
||
- `s_HeroStar` 总条数 = 6(从配置看 6 星即满)
|
||
- 服务端:`if not CFG.s_HeroStar:Get(heroInfo.star + 1): return ERR(HERO_STAR_MAX)`
|
||
- 6 星时 `rare1-5` 均为 0,即不需要经验也不能再升
|
||
|
||
#### 4.3.5 升星前置:**等级必须达到本星级上限**
|
||
|
||
`Hero.lua` 行 185-188:
|
||
|
||
```text
|
||
if heroInfo.level < sHeroStar.starLimit: return ERR(HERO_LEVEL_NO_ENOUGH)
|
||
```
|
||
|
||
#### 4.3.6 完整流程(伪代码)
|
||
|
||
```text
|
||
function HeroStarUp(rid, heroId, items: [{itemId, itemNum}]):
|
||
heroInfo = HeroLogic:getHero(rid, heroId)
|
||
sHeroStar = CFG.s_HeroStar[heroInfo.star]
|
||
sHero = CFG.s_Hero[heroId]
|
||
|
||
// 校验链
|
||
if not CFG.s_HeroStar[heroInfo.star + 1]: return ERR(HERO_STAR_MAX) // 3012
|
||
if heroInfo.level < sHeroStar.starLimit: return ERR(HERO_LEVEL_NO_ENOUGH) // 3013
|
||
if items is empty: return ERR(HERO_NO_ITEM) // 3014
|
||
for item in items:
|
||
if CFG.s_HeroStarExp[item.itemId].rareGroup != sHero.rare:
|
||
return ERR(HERO_ITEM_ERROR) // 3015
|
||
if BagComponent.itemCount(item.itemId) < item.itemNum:
|
||
return ERR(ITEM_NOT_ENOUGH)
|
||
if Σ(item.itemNum) > 6: return ERR(HERO_ITEM_TO_MUCH) // 3016
|
||
|
||
lucky = 0; addExp = 0
|
||
for item in items:
|
||
sHeroStarExp = CFG.s_HeroStarExp[item.itemId]
|
||
lucky += sHeroStarExp.lucky * item.itemNum
|
||
addExp += sHeroStarExp.exp * item.itemNum
|
||
BagComponent.delItem(item.itemId, item.itemNum)
|
||
|
||
if lucky >= Random(1,100): addExp *= 2 // 双倍
|
||
|
||
HeroLogic:addHeroStarExp(rid, heroId, addExp):
|
||
if not CFG.s_HeroStar[heroInfo.star + 1]: return false
|
||
heroInfo.starExp += addExp
|
||
while heroInfo.starExp >= curStarReqExp:
|
||
heroInfo.star += 1
|
||
HeroLogic:unlockSkill(rid, heroId, heroInfo.star, heroInfo)
|
||
if not next star: heroInfo.starExp = 0; break
|
||
else: heroInfo.starExp -= curStarReqExp; refresh exp threshold
|
||
cacleSyncHistoryPower / updateMapArmyHero
|
||
setActivitySchedule(HERO_STAR_LEVEL_COUNT, 1)
|
||
```
|
||
|
||
#### 4.3.7 RPC + 错误码
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_HeroStarUp` | 606 | `HERO_STAR_MAX` (3012), `HERO_LEVEL_NO_ENOUGH` (3013), `HERO_NO_ITEM` (3014), `HERO_ITEM_ERROR` (3015), `HERO_ITEM_TO_MUCH` (3016), `ITEM_NOT_ENOUGH` |
|
||
|
||
---
|
||
|
||
### 4.4 技能升级
|
||
|
||
#### 4.4.1 技能槽位含义
|
||
|
||
- **槽位 1-4:普通技能**(被动/主动/触发/持续,看 `s_HeroSkill.type`)。这些技能通过 `unlockSkill` 按"达到某星级"解锁,技能 ID 通常为 `heroId * 100 + n`(n=1..4)。
|
||
- **槽位 5:觉醒技能**。技能 ID 为 `heroId * 100 + 5`。仅当 1-4 技能全部满级(5 级)时自动解锁(见 §4.5)。
|
||
|
||
#### 4.4.2 升级消耗
|
||
|
||
- **道具**:`sHero.getItem`(即招募碎片)。**升技能与招募共享同一道具池**。
|
||
- **数量**:查 `s_HeroSkillLevel`,主键计算:
|
||
|
||
```text
|
||
upLevelIndex = (Σ skillLevel) - skillCount + 1 // 第几次升级
|
||
costItemId = sHero.getItem
|
||
cost = sHeroSkillLevel[upLevelIndex].costItemN // N = rare (1-5)
|
||
```
|
||
- 客户端等价 (`HeroProxy.cs` 行 405):`id = 100 + GetSkillAllLevel() - GetSkillCount()`(注意客户端用 100 偏移作为 ID)。
|
||
- **金币**:**ROK 无金币消耗**(仅消耗碎片)。
|
||
|
||
#### 4.4.3 升级上限
|
||
|
||
- 单技能最大等级 = 5(`HeroProxy.IsAllSkillMax`:`GetSkillAllLevel() >= 20`,即 4 个技能 × 5 级)
|
||
- 整体上限通过 `s_HeroSkillEffect` 表是否存在 `(skillId * 1000 + nextLevel)` 来判定(`HeroLogic.lua` 行 158)
|
||
|
||
#### 4.4.4 升级目标的随机性 ⚠️
|
||
|
||
**这是 ROK 一个非常重要且非直观的设计**——服务端**不允许玩家选择升哪个技能**!
|
||
|
||
```text
|
||
function heroSkillLevelUp(rid, heroId):
|
||
skillIds = []
|
||
for skillInfo in heroInfo.skills:
|
||
if CFG.s_HeroSkillEffect[skillInfo.skillId * 1000 + skillInfo.skillLevel + 1] exists:
|
||
skillIds.append({id: skillInfo.skillId, rate: 1000})
|
||
// 扣道具
|
||
BagComponent.delItem(sHero.getItem, count)
|
||
// 随机选一个可升级的技能
|
||
skillId = Random.GetId(skillIds) // 等权重随机(rate 都是 1000)
|
||
skillLevel = ++ for that skillId
|
||
syncHero(...)
|
||
if checkHeroSkillFull(rid, heroId) AND has 5th skill:
|
||
heroAwake(rid, heroId) // 自动触发觉醒
|
||
```
|
||
|
||
返回 `{skillId, skillLevel}`,客户端通过 `Hero_HeroSkillLevelUp.response` 收到这个结果,弹窗展示"恭喜你升级了 XX 技能"。
|
||
|
||
#### 4.4.5 RPC + 错误码
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_HeroSkillLevelUp` | 603 | `HERO_NOT_EXIST` (3003), `HERO_SKILL_MAX` (3004), `HERO_LEVEL_ITEM_NOT_ENOUGH` (3005) |
|
||
|
||
---
|
||
|
||
### 4.5 觉醒
|
||
|
||
#### 4.5.1 觉醒条件
|
||
|
||
1. 该英雄存在第 5 个技能配置:`CFG.s_HeroSkill[heroId * 100 + 5]` 不为空
|
||
2. 前 4 个技能**全部满级**:`HeroLogic:checkHeroSkillFull(rid, heroId)` 返回 true(即没有任何技能能继续升级)
|
||
3. `Hero.lua` 行 101 校验:`if not table.size(heroInfo.skills) < 4 or not checkHeroSkillFull: return ERR(HERO_SKILL_NOT_MAX)`
|
||
- **注意源码这里有运算符优先级问题**(`not table.size < 4` 实际为 `(not table.size) < 4` → `false < 4` 总成立)。实际生效条件为第二部分 `checkHeroSkillFull`。重写时按"技能数 >= 4 且全满"标准实现即可。
|
||
|
||
#### 4.5.2 觉醒消耗
|
||
|
||
**无任何消耗**——`heroAwake` 直接把第 5 技能加入 `heroInfo.skills`,level=1。
|
||
|
||
#### 4.5.3 觉醒效果
|
||
|
||
- 解锁第 5 个技能(觉醒技),等级固定 1。
|
||
- 技能 ID 公式:`heroId * 100 + 5`
|
||
- 战力重算 + 同步部队
|
||
|
||
#### 4.5.4 自动 vs 手动
|
||
|
||
- **自动**:在 `heroSkillLevelUp` 升级技能后会判断,如果满足条件自动调 `heroAwake`,无需玩家主动操作。
|
||
- **手动**:`Hero_HeroAwake` RPC 是冗余入口(兼容性),实际由前 4 技能升满后自动触发。
|
||
|
||
#### 4.5.5 完整流程
|
||
|
||
```text
|
||
function heroAwake(rid, heroId):
|
||
skillId = heroId * 100 + 5
|
||
if skillId already in heroInfo.skills: return existing
|
||
heroInfo.skills.append({skillId, skillLevel=1})
|
||
setHero & syncHero(true)
|
||
cacleSyncHistoryPower
|
||
return {skillId, skillLevel=1}
|
||
```
|
||
|
||
#### 4.5.6 RPC + 错误码
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_HeroAwake` | 604 | `HERO_NOT_EXIST` (3003), `HERO_NOT_AWAKE` (3006), `HERO_SKILL_NOT_MAX` (3007) |
|
||
|
||
---
|
||
|
||
### 4.6 天赋树
|
||
|
||
#### 4.6.1 三套天赋页(TalentIndex 1/2/3)
|
||
|
||
- 每个英雄有 **3 套独立的天赋页**,对应 `talentTrees[1]`、`talentTrees[2]`、`talentTrees[3]`。
|
||
- 玩家可任意切换"当前激活的天赋页" (`talentIndex`),**仅激活页**的天赋影响战力和战斗属性。
|
||
- 切换时需要消耗道具或钻石(详见 §4.6.5)。
|
||
|
||
> 服务端校验 `if index > 3: return ERR(HERO_TALENT_INDEX_ERROR)`。索引范围 1-3。
|
||
|
||
#### 4.6.2 天赋点来源
|
||
|
||
**`heroInfo.talentPoint` 字段已废弃**(`HeroDef.lua` 仍保留,但 `HeroLogic.lua` 中所有"消耗 talentPoint"的代码都被注释掉了,见行 62-69、428-432、488-491、618)。
|
||
|
||
实际"当前可用天赋点"通过**实时计算**:
|
||
|
||
```text
|
||
function checkTalentPoint(rid, heroId, index):
|
||
heroInfo = getHero(rid, heroId)
|
||
rare = sHero.rare
|
||
|
||
heroLevelMap = s_ZeroEmpty[HERO_LEVEL_TALENT][rare] // {[level] = point}
|
||
heroStarMap = s_ZeroEmpty[HERO_STAR_TALENT] // {[star] = point}
|
||
|
||
talentPoint = 0
|
||
for (level, point) in heroLevelMap:
|
||
if heroInfo.level >= level: talentPoint += point // 累加所有已达成的等级阶段
|
||
for (star, point) in heroStarMap:
|
||
if heroInfo.star >= star: talentPoint += point // 累加所有已达成的星级阶段
|
||
|
||
// 减去当前天赋页已用点数
|
||
if heroInfo.talentTrees[index]:
|
||
for _ in heroInfo.talentTrees[index].talentTree:
|
||
talentPoint -= 1
|
||
|
||
return talentPoint > 0
|
||
```
|
||
|
||
> **关键约束**:所有 3 套天赋页**共用同一池天赋点**(每页独立计算用了多少,但池子相同)。
|
||
|
||
客户端等价 `HeroProxy.GetTotalTalentPoint`(行 249-271):遍历 `s_HeroLevel` 与 `s_HeroStar` 表,累加所有满足条件的 `starEffectData`。
|
||
|
||
#### 4.6.3 节点前置依赖
|
||
|
||
节点学习需满足三个条件(依次校验,见 `Hero.lua` 行 252-271):
|
||
|
||
1. **天赋点充足** (`checkTalentPoint`),否则 `HERO_TALENT_POINT_NOT_ENOUGH` (3017)。
|
||
2. **该英雄能学这个天赋** (`checkHeroStudyTalent`):
|
||
- 取 `sHero.talent` 对应的根天赋(3 个)→ 它们的 `gainTree`
|
||
- 看 `s_ZeroEmpty.TALENT[gainTree][id]` 是否为真
|
||
- 否则 `HERO_CAN_NOT_STUDY_TALENT` (3018)
|
||
3. **前置节点已学** (`checkHeroStudyTalentPre`):
|
||
- 取目标节点 `level`
|
||
- 若 `level == 1`:允许(无前置)
|
||
- 否则:检查同一页 `talentTree` 中**是否存在 level = `level - 1`** 的节点
|
||
- 否则 `HERO_TALENT_PRE_NOT_STUDY` (3019)
|
||
4. **同级互斥**(同一 level 同一页只能选一个,见 `checkHeroStudyTalentSame`):
|
||
- 若同一页已有同 `level` 的节点 → `HERO_STUDY_SAME_LEVEL_TALENT` (3020)
|
||
|
||
#### 4.6.4 学习消耗
|
||
|
||
**学习单个节点无任何道具/资源消耗**——仅消耗"天赋点"(隐式记账)。
|
||
|
||
#### 4.6.5 切换页 vs 重置页
|
||
|
||
两者都用 `Hero_ChangeTalentIndex` (608) 和 `Hero_ResetTalent` (609) 两个 RPC。**关键区别**:
|
||
|
||
- **`Hero_ChangeTalentIndex`**:切换激活页(`talentIndex`)
|
||
- 若 `index == heroInfo.talentIndex`:返回 `HERO_TALENT_INDEX_SAME` (3022)
|
||
- 消耗:`useDenar=false` 时扣 1 个 `s_Config.talentResetItemID` 道具;`useDenar=true` 时扣 `s_Item[id].shortcutPrice` 钻石
|
||
- 失败:`ITEM_NOT_ENOUGH` / `ROLE_DENAR_NOT_ENOUGH`
|
||
|
||
- **`Hero_ResetTalent`**:重置某页的所有已学天赋
|
||
- **若 `index == heroInfo.talentIndex`**(重置当前页):才需要消耗道具/钻石(同上)
|
||
- **若重置非当前页**:免费
|
||
- 操作:`heroInfo.talentTrees[index].talentTree = {}` (清空)
|
||
|
||
#### 4.6.6 天赋页改名
|
||
|
||
`Hero_ModifyTalentName` (610):
|
||
- 名称长度校验:`s_Config.heroNameLimit[1] <= utf8.len(name) <= heroNameLimit[2]`
|
||
- 失败:`HERO_TALENT_NAME_LENGTH_ERROR` (3023)
|
||
- 写入:`heroInfo.talentTrees[index].name = name`
|
||
|
||
#### 4.6.7 天赋专精(Mastery)
|
||
|
||
每个 `gainTree` 有多个 `s_HeroTalentMastery` 阶段。属性/技能加成在 `getHeroTalentAttr` 中**根据已学节点数 (`count`)** 计算:
|
||
|
||
```text
|
||
function getHeroTalentAttr(rid, heroId):
|
||
talentTree = heroInfo.talentTrees[heroInfo.talentIndex].talentTree
|
||
|
||
battleSkill = {}
|
||
battleAttr = {}
|
||
talentCount = {} // {gainTree -> count}
|
||
|
||
// 节点本身的加成
|
||
for id in talentTree:
|
||
node = s_HeroTalentGainTree[id]
|
||
if node.battleSkillID > 0: battleSkill[node.battleSkillID] = node.battleSkillID
|
||
for i, attrName in node.attrType: battleAttr[attrName] += node.attrNumber[i]
|
||
talentCount[node.gainTree] += 1
|
||
|
||
// 专精的加成
|
||
sHeroTalentMastery = CFG.s_HeroTalentMastery:Get() // 整张表
|
||
for (gainTree, count) in talentCount:
|
||
for i from count downto 1:
|
||
mastery = sHeroTalentMastery[gainTree][i]
|
||
if mastery exists:
|
||
// 触发该层专精
|
||
if mastery.battleSkillID > 0: battleSkill[battleSkillID] = battleSkillID
|
||
for j, attrName in mastery.attrType: battleAttr[attrName] += mastery.attrNumber[j]
|
||
|
||
return {battleSkill, battleAttr}
|
||
```
|
||
|
||
> ⚠ ROK 此处 mastery 是按 `[gainTree][index]` 二级索引取,**index 含义是"按已学点数顺序排"**,并非客户端 `HeroProxy.talentScore` 用的 `needTalentPoint` 阈值——两者实现略有差异,重写时应以**服务端 Lua 为准**。
|
||
|
||
#### 4.6.8 完整流程(伪代码)
|
||
|
||
```text
|
||
function TalentUp(rid, heroId, id, index):
|
||
heroInfo = HeroLogic:getHero(rid, heroId)
|
||
if not heroInfo: return ERR(HERO_NOT_EXIST)
|
||
if not checkTalentPoint(rid, heroId, index): return ERR(HERO_TALENT_POINT_NOT_ENOUGH)
|
||
if not checkHeroStudyTalent(heroId, id): return ERR(HERO_CAN_NOT_STUDY_TALENT)
|
||
if not checkHeroStudyTalentPre(rid, heroId, index, id): return ERR(HERO_TALENT_PRE_NOT_STUDY)
|
||
if not checkHeroStudyTalentSame(rid, heroId, index, id): return ERR(HERO_STUDY_SAME_LEVEL_TALENT)
|
||
|
||
// 学习
|
||
if not heroInfo.talentTrees[index]:
|
||
heroInfo.talentTrees[index] = {index, talentTree=[], name=""}
|
||
heroInfo.talentTrees[index].talentTree.append(id)
|
||
|
||
setHero & syncHero(true)
|
||
TaskLogic:addTaskStatisticsSum(HERO_TALENT_NUM, 1)
|
||
cacleSyncHistoryPower
|
||
updateMapArmyHero(rid, heroId)
|
||
return {result: true}
|
||
```
|
||
|
||
#### 4.6.9 RPC + 错误码汇总
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_TalentUp` | 607 | `HERO_NOT_EXIST`, `HERO_TALENT_POINT_NOT_ENOUGH` (3017), `HERO_CAN_NOT_STUDY_TALENT` (3018), `HERO_TALENT_PRE_NOT_STUDY` (3019), `HERO_STUDY_SAME_LEVEL_TALENT` (3020) |
|
||
| `Hero_ChangeTalentIndex` | 608 | `HERO_NOT_EXIST`, `HERO_TALENT_INDEX_ERROR` (3021), `HERO_TALENT_INDEX_SAME` (3022), `ITEM_NOT_ENOUGH`, `ROLE_DENAR_NOT_ENOUGH` |
|
||
| `Hero_ResetTalent` | 609 | `HERO_NOT_EXIST`, `HERO_TALENT_INDEX_ERROR`, `ITEM_NOT_ENOUGH`, `ROLE_DENAR_NOT_ENOUGH` |
|
||
| `Hero_ModifyTalentName` | 610 | `HERO_TALENT_NAME_LENGTH_ERROR` (3023), `HERO_NOT_EXIST`, `HERO_TALENT_INDEX_ERROR` |
|
||
|
||
---
|
||
|
||
### 4.7 装备穿戴
|
||
|
||
#### 4.7.1 装备槽位(8 个)
|
||
|
||
来源:`Hero.lua` 行 430-437 + `BagProxy.cs` `EquipSubType` 枚举。
|
||
|
||
| equipIndex | 字段名 | EquipSubType | s_Item.subType |
|
||
|---|---|---|---|
|
||
| 1 | `head` | Head | `HELMET` |
|
||
| 2 | `breastPlate` | BreastPlate | `BREASTPLATE` |
|
||
| 3 | `weapon` | Weapon | `ARMS` |
|
||
| 4 | `gloves` | Gloves | `GLOVES` |
|
||
| 5 | `pants` | Pants | `PANTS` |
|
||
| 6 | `accessories1` | accessories1 | `ACCESSORIES` |
|
||
| 7 | `accessories2` | accessories2 | `ACCESSORIES` |
|
||
| 8 | `shoes` | shoes | `SHOES` |
|
||
|
||
字段值是**背包道具的 `itemIndex`**(不是 `itemId`),0 = 未装备。
|
||
|
||
#### 4.7.2 穿戴规则
|
||
|
||
1. 道具必须存在 (`getItem(rid, itemIndex)` 非空)
|
||
2. `s_Item.subType` 必须与目标槽位的 `subType` 一致;否则 `HERO_EQUIP_SUBTYPE_ERROR` (3025)
|
||
3. 英雄等级必须 ≥ `s_Equip.useLevel`;否则 `HERO_EQUIP_LV_NO_ENOUGH` (3024)
|
||
4. **英雄必须处于待命状态**(不在出征/守城军队中);否则 `HERO_EQUIP_NOT_IN_CITY` (3026)
|
||
- 通过 `checkHeroIdle` 遍历 `ArmyLogic:getArmy(rid)`,检查 `mainHeroId` / `deputyHeroId`
|
||
|
||
#### 4.7.3 同一装备切换英雄(重要)
|
||
|
||
`Hero_HeroWearEquip` 中(`Hero.lua` 行 459-467):
|
||
|
||
1. 若该 itemInfo 已被其他英雄装备 (`itemInfo.heroId > 0`):从旧英雄上**清空对应槽位** (`beforeHeroInfo[attr] = 0`)
|
||
2. 若目标英雄的目标槽位**已有装备**:把旧装备的 `heroId` 清 0
|
||
3. 写入:`heroInfo[attr] = itemIndex`,`itemInfo.heroId = heroId`
|
||
|
||
#### 4.7.4 卸下装备
|
||
|
||
`Hero_TakeOffEquip` (612):
|
||
- 校验英雄待命
|
||
- `itemInfo.heroId = 0`;`heroInfo[attr] = 0`
|
||
- 同步 Hero + Item
|
||
|
||
#### 4.7.5 与背包的联动(**字段需要在 Item Entity 加 `heroId` 反向索引**)
|
||
|
||
- `s_Item` 是道具基础,`s_Equip` 是装备扩展(`compose` 套装组 ID、`useLevel`、`attAddEx` 等)
|
||
- `s_EquipAtt` 定义属性枚举
|
||
- `s_EquipCompose` 定义套装:`compose2/4/6/8` 件数效果,分别提供 `compose2/4/6/8AddEx` 属性加成
|
||
- **专属装备加成**:`itemInfo.exclusive > 0` 且 `checkHeroTalent(heroId, exclusive)` 时,属性乘 `s_Config.equipTalentPromote × 2` 并向上 0.5 取整(`HeroLogic.lua` 行 822-825)
|
||
|
||
#### 4.7.6 RPC + 错误码
|
||
|
||
| RPC | ID | 错误码 |
|
||
|---|---|---|
|
||
| `Hero_HeroWearEquip` | 611 | `ITEM_NOT_EXIST`, `HERO_EQUIP_SUBTYPE_ERROR` (3025), `HERO_EQUIP_LV_NO_ENOUGH` (3024), `HERO_EQUIP_NOT_IN_CITY` (3026), `HERO_EQUIP_ALREADY_WEAR` (3027) |
|
||
| `Hero_TakeOffEquip` | 612 | `HERO_EQUIP_NOT_IN_CITY` (3026) |
|
||
|
||
---
|
||
|
||
### 4.8 战力计算
|
||
|
||
#### 4.8.1 战力公式
|
||
|
||
来源:`HeroCacle.lua` 全文 + `HeroProxy.cs.skillScore/talentScore`。
|
||
|
||
```text
|
||
power(hero) =
|
||
sHero.score // ① 基础战力
|
||
+ s_HeroLevel[rare*10000 + level].score // ② 等级战力(直接读,非累加)
|
||
+ Σ s_HeroSkillEffect[skillId*1000 + lv].score // ③ 技能战力(所有已学技能)
|
||
+ Σ s_HeroTalentGainTree[id].score // ④ 天赋节点战力(仅当前激活页)
|
||
+ Σ s_HeroTalentMastery[group][i].score // ⑤ 天赋专精战力(按已学节点数触发的所有阶段)
|
||
+ [装备分由 getHeroEquipAttr 间接生效,但 HeroCacle 中并未独立计算"装备战力分"]
|
||
```
|
||
|
||
> ⚠ ROK 服务端 `HeroCacle:caclePower` **没有显式"装备分"参与战力**——装备只影响战斗时的属性,不直接进总战力。客户端 `HeroProxy.UpdatePower` 同样只算 `base + level + skill + talent`。这一点重写时需要决策:要不要把装备纳入战力。**ROK 原版不纳入。**
|
||
|
||
#### 4.8.2 属性查询:`getHeroAttr`
|
||
|
||
战斗时按属性名查询英雄某项属性:
|
||
|
||
```text
|
||
function getHeroAttr(rid, heroId, attrName, notCancleTalent=false):
|
||
num = 0
|
||
if not notCancleTalent:
|
||
num += getHeroTalentAttr(rid, heroId).battleAttr[attrName] // 天赋
|
||
num += getHeroEquipAttr(rid, heroId).battleAttr[attrName] // 装备
|
||
num += cancleskillAttr(rid, heroId)[attrName] // 技能
|
||
return num
|
||
```
|
||
|
||
> 副将(`deputyHero`)的天赋属性**不参与计算**(`notCancleTalent=true`,见行 917)。仅主将享受天赋加成。
|
||
|
||
#### 4.8.3 战力变化触发的事件
|
||
|
||
任意会影响战力的操作完成后会调:
|
||
|
||
```text
|
||
RoleLogic:cacleSyncHistoryPower(rid, nil, nil, recalc=true) // 重算并同步
|
||
```
|
||
|
||
并对涉及到的部队同步:
|
||
|
||
```text
|
||
updateMapArmyHero(rid, heroId, newLevel?):
|
||
遍历角色所有部队
|
||
如果该 heroId 是 main/deputy
|
||
重新算 skills + talentAttr + levels
|
||
通过 BattleAttrLogic:syncObjectHeroChange 通知战斗服务器
|
||
通过 ResourceLogic:roleArmyCollectSpeedChange 更新采集速度
|
||
```
|
||
|
||
---
|
||
|
||
### 4.9 其他次要功能
|
||
|
||
#### 4.9.1 英雄列表排序
|
||
|
||
**4 种排序**(`HeroProxy.SortType`):`Rare` / `Star` / `Level` / `Power`(还有 `Recomend` 未实现)。
|
||
|
||
所有比较器(`CompareHeroByXxx`,共 4 个)都遵循通用规则:
|
||
|
||
```text
|
||
xScore = (x.data != null) ? 1 : (x.itemCount < x.config.getItemNum ? 0 : 2)
|
||
yScore = (同上)
|
||
// 排序优先级:未集齐(0) < 已拥有(1) < 可召唤(2)
|
||
if xScore != yScore: 按 xScore 排
|
||
// 同分组内:按主排序字段(rare/star/level/power),同分用 (rare → star → level → power → summonTime) 依次比
|
||
```
|
||
|
||
最后 `heros.Reverse()`(默认降序)。
|
||
|
||
#### 4.9.2 三段分组
|
||
|
||
`GetHerosBySort` 返回三个 List:
|
||
|
||
- `own`:`hero.data != null`(已拥有)
|
||
- `summon`:`hero.itemCount >= getItemNum`(碎片够,可召唤)
|
||
- `nosummon`:`itemCount < getItemNum`(碎片不够)
|
||
|
||
⚠ `hero.config.listDisplay == 1` 的英雄**不进任何分组**(怪物英雄,列表不显示)。
|
||
|
||
#### 4.9.3 列表筛选
|
||
|
||
ROK 客户端**没有显式的"按文明/职业筛选"功能**。仅有上述排序 + 分组。
|
||
|
||
#### 4.9.4 雕像兑换
|
||
|
||
`Hero_ExchangeHeroItem` (605):用 `sHero.exchange` 道具(通用雕像)兑换为 `sHero.getItem` 道具(具体英雄碎片)。
|
||
|
||
校验链:
|
||
- `sHero.exchange == Enum.Exchange.NO (0)` → `HERO_NOT_EXCHANGE` (3008)
|
||
- 该英雄已存在且**技能数 ≥4 且全满级** → `HERO_EXCHANGE_SKILL_MAX` (3009)(技能满了说明不需要再兑换碎片)
|
||
- 雕像道具不足 → `HERO_EXCHANGE_ITEM_NOT_ENOUGH` (3010)
|
||
|
||
操作:扣 `sHero.exchange` × `itemNum` → 加 `sHero.getItem` × `itemNum`(**1:1 转换**)。
|
||
|
||
#### 4.9.5 红点统计
|
||
|
||
`HeroProxy.GetCanSummonerHeroCount` 每个英雄遍历,满足任一条件 +1:
|
||
- 已拥有 + `IsCanUpSkill`(可升技能:未全满 + 碎片够)
|
||
- 已拥有 + `level == starLimit` 且 `!IsStarMaxLevel`(可升星)
|
||
- 已拥有 + `GetCurPageRemainPoint(talentIndex) > 0`(有天赋点未用)
|
||
- 未拥有 + 碎片够(可召唤)
|
||
|
||
---
|
||
|
||
## 5. 协议清单(C2G / G2C / 推送)
|
||
|
||
> 模块号:Hero Module 601-700(请求),Hero Module 30201-30250(推送)。
|
||
|
||
| 协议名 | 协议号 | 方向 | request | response | 用途 |
|
||
|---|---|---|---|---|---|
|
||
| `Hero_SummonHero` | 601 | C2G | `heroId` | 无 | 招募英雄 |
|
||
| `Hero_AddHeroExp` | 602 | C2G | `heroId, itemId, itemNum` | `result, itemId, itemNum` | 使用经验书升级 |
|
||
| `Hero_HeroSkillLevelUp` | 603 | C2G | `heroId` | `skillId, skillLevel` | 升技能(**服务端随机选目标**) |
|
||
| `Hero_HeroAwake` | 604 | C2G | `heroId` | `skillId, skillLevel` | 手动觉醒(一般由技能升级自动触发) |
|
||
| `Hero_ExchangeHeroItem` | 605 | C2G | `heroId, itemNum` | `result` | 雕像兑换碎片 |
|
||
| `Hero_HeroStarUp` | 606 | C2G | `heroId, items: [{itemId, itemNum}]` | `result` | 升星(最多 6 个材料) |
|
||
| `Hero_TalentUp` | 607 | C2G | `heroId, id, index` | `result` | 学习单个天赋节点 |
|
||
| `Hero_ChangeTalentIndex` | 608 | C2G | `heroId, index, useDenar` | `result` | 切换激活的天赋页 |
|
||
| `Hero_ResetTalent` | 609 | C2G | `heroId, index, useDenar` | `result` | 重置某天赋页 |
|
||
| `Hero_ModifyTalentName` | 610 | C2G | `heroId, index, name` | `result` | 改天赋页名 |
|
||
| `Hero_HeroWearEquip` | 611 | C2G | `heroId, itemIndex, equipIndex` | `result` | 穿戴装备 |
|
||
| `Hero_TakeOffEquip` | 612 | C2G | `heroId, equipIndex` | `result` | 卸下装备 |
|
||
| `Hero_HeroInfo` | 30201 | G2C 推送 | `heroInfo: *HeroInfo(heroId), noShow: bool` | 无 | 推送英雄完整数据(新增/变化);`noShow=true` 表示酒馆场景不要弹"获得英雄"展示 |
|
||
|
||
**关联协议(其他模块发布的 hero 相关):**
|
||
|
||
| 协议名 | 协议号 | 方向 | 用途 |
|
||
|---|---|---|---|
|
||
| `Build_Tavern` | 407 | C2G | 酒馆开宝箱(间接获取英雄碎片) |
|
||
| `Build_DefendHero` | 410 | C2G | 设置驻防主/副将 |
|
||
| `Shop_GetLimitHeroInfo` | 907 | C2G | 限时英雄商店(碎片售卖) |
|
||
|
||
**子结构定义**(`Common.sproto`):
|
||
|
||
- `.SkillInfo { skillId, skillLevel }`
|
||
- `.TalentTrees { index, talentTree: *integer, name }`
|
||
- `.HeroInfo { heroId, star, starExp, level, exp, summonTime, soldierKillNum, savageKillNum, skills, talentTrees, talentIndex, head, breastPlate, weapon, gloves, pants, accessories1, accessories2, shoes }`
|
||
- `.Heros { heroId, num, isNew }`(Tavern 宝箱用,给的是碎片数 num)
|
||
|
||
---
|
||
|
||
## 6. 错误码清单
|
||
|
||
来源:`ErrorCode.cs` 行 24–26(Role)+ 149–176(Hero)+ 224(Building)+ 473(Expedition)。
|
||
|
||
| 错误码 | 数值 | 含义 |
|
||
|---|---|---|
|
||
| `ROLE_HERO_NOT_EXIST` | 1004 | 角色场景下英雄不存在 |
|
||
| `ROLE_HERO_NOT_WAIT_STATUS` | 1005 | 英雄未处于待命状态 |
|
||
| `ROLE_HERO_STAR_NOT_ENOUGH` | 1006 | 英雄星级不足 |
|
||
| `HERO_OPEN_DAYS_NOT_ENOUGH` | 3000 | 招募时所在王国未到该英雄解锁天数 |
|
||
| `HERO_SUMMON_ITEM_NOT_ENOUGH` | 3001 | 招募碎片不足 |
|
||
| `HERO_ALREADY_EXIST` | 3002 | 英雄已存在 |
|
||
| `HERO_NOT_EXIST` | 3003 | 英雄不存在 |
|
||
| `HERO_SKILL_MAX` | 3004 | 技能已满级,无法升级 |
|
||
| `HERO_LEVEL_ITEM_NOT_ENOUGH` | 3005 | 升技能道具不足 |
|
||
| `HERO_NOT_AWAKE` | 3006 | 该英雄不存在觉醒技能 |
|
||
| `HERO_SKILL_NOT_MAX` | 3007 | 技能未满,无法觉醒 |
|
||
| `HERO_NOT_EXCHANGE` | 3008 | 该英雄无法使用通用雕像兑换 |
|
||
| `HERO_EXCHANGE_SKILL_MAX` | 3009 | 技能已满,无需兑换碎片 |
|
||
| `HERO_EXCHANGE_ITEM_NOT_ENOUGH` | 3010 | 通用雕像不足 |
|
||
| `HERO_LEVEL_LIMIT` | 3011 | 等级达到当前星级上限,无法升级 |
|
||
| `HERO_STAR_MAX` | 3012 | 星级已满 |
|
||
| `HERO_LEVEL_NO_ENOUGH` | 3013 | 等级未达到 starLimit,无法升星 |
|
||
| `HERO_NO_ITEM` | 3014 | 升星材料为空 |
|
||
| `HERO_ITEM_ERROR` | 3015 | 升星材料的 rareGroup 与英雄稀有度不符 |
|
||
| `HERO_ITEM_TO_MUCH` | 3016 | 单次升星材料数 > 6 |
|
||
| `HERO_TALENT_POINT_NOT_ENOUGH` | 3017 | 天赋点不足 |
|
||
| `HERO_CAN_NOT_STUDY_TALENT` | 3018 | 该英雄无法学习此天赋节点 |
|
||
| `HERO_TALENT_PRE_NOT_STUDY` | 3019 | 前置天赋未学 |
|
||
| `HERO_STUDY_SAME_LEVEL_TALENT` | 3020 | 同 level 天赋已学过 |
|
||
| `HERO_TALENT_INDEX_ERROR` | 3021 | 天赋页索引非法(>3) |
|
||
| `HERO_TALENT_INDEX_SAME` | 3022 | 切换的天赋页与当前页相同 |
|
||
| `HERO_TALENT_NAME_LENGTH_ERROR` | 3023 | 天赋页名称长度违规 |
|
||
| `HERO_EQUIP_LV_NO_ENOUGH` | 3024 | 英雄等级低于装备 useLevel |
|
||
| `HERO_EQUIP_SUBTYPE_ERROR` | 3025 | 装备 subType 与槽位不匹配 |
|
||
| `HERO_EQUIP_NOT_IN_CITY` | 3026 | 英雄不在城内(出征中) |
|
||
| `HERO_EQUIP_ALREADY_WEAR` | 3027 | 该装备已被装备(实际代码中走"自动卸旧再穿新",此码可能未触发) |
|
||
| `BUILDING_WALL_HERO_SAME` | 6019 | 驻防主副将相同 |
|
||
| `EXPEDITION_HERO_NOT_FREE` | 17004 | 远征英雄非空闲 |
|
||
|
||
通用错误码(非 Hero 模块但 Hero 流程会复用):
|
||
|
||
| 错误码 | 含义 |
|
||
|---|---|
|
||
| `HERO_ARG_ERROR` | 参数错误(在 SummonHero 中作为 ErrorCode 引用,源码未列出具体数值,按通用 5001 范围) |
|
||
| `CFG_ERROR` | 配置缺失 |
|
||
| `ITEM_NOT_EXIST` | 道具不存在 |
|
||
| `ITEM_NOT_ENOUGH` | 道具数量不足 |
|
||
| `ROLE_DENAR_NOT_ENOUGH` | 钻石不足 |
|
||
|
||
---
|
||
|
||
## 7. UI 流程
|
||
|
||
> ROK 客户端 PureMVC 架构,每个 Mediator 对应一个 Panel;按业务流程列出。
|
||
|
||
### 7.1 入口:CaptainMediator(统帅总览)
|
||
|
||
**Panel**:`UI.s_captain`(`CaptainMediator` + `CaptainView`)
|
||
|
||
**注册 Notification**:
|
||
- `CmdConstant.GetNewHero` - 拿到新英雄
|
||
- `Hero_HeroStarUp.TagName` - 升星响应
|
||
- `CmdConstant.UpdateHero` - 英雄数据更新
|
||
- `Hero_HeroSkillLevelUp.TagName` - 技能升级响应
|
||
- `CmdConstant.ItemInfoChange` - 道具变化(用于刷新升技能道具显示)
|
||
- `CmdConstant.HeroListVisible` - 显示/隐藏列表
|
||
- `CmdConstant.RefreshEquipRedPoint` - 装备红点
|
||
- `CmdConstant.HeroSceneVisible` - 3D 场景显示
|
||
|
||
**关键流程**:
|
||
1. 进入面板 → `InitData` 加载 6 个 prefab:`UI_LC_Captain`、`UI_Item_CaptainHead`、`UI_Item_CaptainSummon`、`UI_Item_CaptainPartline`、`UI_Item_CaptainSkill`、`UI_LC_CaptainUnSum`
|
||
2. 从 `PlayerPrefs("Hero_SortType_{rid}", default Rare)` 读上次排序方式
|
||
3. `HeroProxy.GetHerosBySort` → 三段分组(已拥有 / 可召唤 / 未集齐)
|
||
4. 列表用 `ListView` 渲染:
|
||
- `UI_LC_Captain` 行:每行 2 个已拥有/可召唤英雄
|
||
- `UI_Item_CaptainPartline` 分隔行
|
||
- `UI_LC_CaptainUnSum` 行:每行 2 个未集齐英雄
|
||
5. 选中某英雄后:
|
||
- 加载 `m_hero.config.heroModel` Spine 动画
|
||
- 加载 `m_hero.config.heroScene` 场景 prefab
|
||
- 播放 `voiceSelect` 语音
|
||
|
||
**右侧详情面板切换**(`view.m_UI_Item_CaptainData` 总览态):
|
||
- 点击 `m_btn_starUp_GameButton` → `ShowStarUp` → 检查 `level >= starLimit` → 打开升星面板
|
||
- 点击 `StandardButton_skillup` → `ShowSkillUp` → 打开技能升级面板
|
||
- 点击 `StandardButton_gift` → `ShowTalent` → 打开天赋树面板
|
||
- 点击 `m_btn_changeEquip_GameButton` → 切换到装备界面(条件:英雄已拥有 + 城堡等级 ≥ `s_SystemOpen[11000].openLv`)
|
||
|
||
### 7.2 招募流程
|
||
|
||
**入口**:
|
||
1. 玩家在主界面/任务引导/酒馆 → 跳转 `UI.s_captain`
|
||
2. 选中"未集齐"英雄 → 显示碎片进度,碎片不够时灰显
|
||
3. 选中"可召唤"英雄 → 显示"招募"按钮
|
||
4. 点击招募 → `HeroProxy.SummonHero(id)` → 发 `Hero_SummonHero` 协议
|
||
5. 服务端推 `Hero_HeroInfo`(首个英雄会推 `noShow=true` 屏蔽展示)
|
||
6. `HeroProxy.UpdateHeroInfo` 收到后:若是新英雄(bNewHero=true && !firstGetData && !noShow)→ 发 `CmdConstant.GetNewHero` 通知
|
||
7. `HeroCmd.Execute` 处理 `GetNewHero`:
|
||
- 关闭 `UI.s_gameTool`
|
||
- 若该英雄是文明初始英雄(`heroId == civilization.initialHero`)→ 触发首次获得引导
|
||
- 否则打开 `UI.s_captainSummon` 展示新英雄(3D 镜头 + 名字/称号/品质/3 个天赋图标 + 文明标识)
|
||
8. 关闭召唤展示后回到主流程
|
||
|
||
### 7.3 经验升级流程
|
||
|
||
**Panel**:`UI_Win_CaptainLevelUp` (`UI_Win_CaptainLevelUpMediator`)
|
||
|
||
1. 在详情面板 → 经验条上点 "+" → 打开经验升级窗
|
||
2. 列出背包中所有 `subType == 经验书` 的道具
|
||
3. 玩家选数量 + 确认 → 发 `Hero_AddHeroExp(heroId, itemId, itemNum)`
|
||
4. 服务端响应后 `HeroProxy.UpdateHeroInfo` 推送,UI 自动刷新经验/等级
|
||
|
||
### 7.4 升星流程
|
||
|
||
**Panel**:`UI_Item_CaptainStarUp`(嵌入 CaptainView 内)
|
||
|
||
**入口**:`m_btn_starUp_GameButton`
|
||
1. 校验 `level >= sHeroStar.starLimit`(不够提示 `Tip` 文本 166056,说明"先升到 X 级才能升星")
|
||
2. 显示当前 star/starExp 进度条 + 可选材料列表(来自 `s_HeroStarExp` 按 `rareGroup == hero.rare` 过滤)
|
||
3. 玩家选若干个(≤6) → "升星"按钮 → 发 `Hero_HeroStarUp(heroId, items)`
|
||
4. 收到 `Hero_HeroStarUp.response.result == true` 后调 `m_UI_Item_CaptainStarUp.OnAddStarExpSuccess()` 播放表现
|
||
5. 若产生升星:会单独弹 `UI_IF_CaptainStarUpSuccess` 升星成功界面
|
||
|
||
### 7.5 技能升级流程
|
||
|
||
**Panel**:`UI_Item_CaptainSkillUp`(嵌入 CaptainView 内)
|
||
|
||
**入口**:`m_UI_Item_CaptainData.AddSkillUpButtonClickedEvent` → `ShowSkillUp`
|
||
1. 显示该英雄的 5 个技能 + 当前等级
|
||
2. 显示升级所需碎片数 (`HeroProxy.Hero.GetSkillLevelUpCostItemNum`) + 背包余量
|
||
3. 点击"升级技能"按钮 → 发 `Hero_HeroSkillLevelUp(heroId)`
|
||
4. 服务端响应 `{skillId, skillLevel}` → `HeroSkillLevelUpSuceess` 高亮"被升级"的那个技能(**玩家不知道会升哪个**!)
|
||
5. 若升级后达到觉醒条件:服务端自动触发 `heroAwake`,再推一次 `Hero_HeroInfo`
|
||
6. 升级成功也会弹 `UI_IF_CaptainSkillUpSuccess` 显示效果
|
||
|
||
### 7.6 天赋树流程
|
||
|
||
**Panel**:`UI_Item_CaptainTalent`(嵌入 CaptainView 内)+ 子 SubView `UI_Item_CaptainTalent_SubView`
|
||
|
||
**入口**:`m_UI_Item_CaptainData.AddTalentButtonClickedEvent` → `ShowTalent`
|
||
1. 显示 3 个 tab(对应 talentIndex 1/2/3,可改名 `UI_Win_TalentChangeName`)
|
||
2. 当前 tab 显示一棵天赋树(3 棵 gainTree 横排,每棵有 N 层节点)
|
||
3. 显示剩余天赋点 (`HeroProxy.Hero.GetCurPageRemainPoint`)
|
||
4. 点击节点 → 校验 → 发 `Hero_TalentUp(heroId, id, index)`
|
||
5. 切换 tab → 弹 `UI_Win_TalentChangeAlert` 询问 → 确认后发 `Hero_ChangeTalentIndex`(默认用道具,可选钻石)
|
||
6. 重置当前 tab → 同样的 Alert 流程 → 发 `Hero_ResetTalent`
|
||
|
||
### 7.7 装备穿戴流程
|
||
|
||
**Panel**:`UI_Item_CaptainEquip`(嵌入 CaptainView 内)+ `UI_Item_CaptainEquipUse` + `UI_Item_CaptainEquipList`
|
||
|
||
**入口**:`m_btn_changeEquip_GameButton`(条件:英雄已拥有 + 城堡等级 ≥ 11000.openLv)
|
||
1. 8 个槽位(按 §4.7.1 顺序排列),已穿戴的显示道具图标,空位显示槽位占位图
|
||
2. 点击某槽位 → 弹"选择装备"列表,从背包筛 `subType == 槽位 subType` 的道具
|
||
3. 选中道具 → 发 `Hero_HeroWearEquip(heroId, itemIndex, equipIndex)`
|
||
4. 长按已穿戴的 → 弹"卸下" → 发 `Hero_TakeOffEquip(heroId, equipIndex)`
|
||
|
||
### 7.8 雕像兑换流程
|
||
|
||
**Panel**:`UI_Win_CaptainItemSource`(兑换 + 道具来源面板)
|
||
|
||
1. 玩家进入英雄详情,看到该英雄的招募碎片 → 点击 "+" → 显示获取来源
|
||
2. 若 `sHero.exchange > 0`(可雕像兑换)→ 显示"用通用雕像兑换"入口
|
||
3. 玩家选数量 → 发 `Hero_ExchangeHeroItem(heroId, itemNum)`
|
||
4. 服务端 1:1 转换 → 同步背包道具
|
||
|
||
### 7.9 酒馆开宝箱(碎片来源)
|
||
|
||
**Panel**:`UI.s_tavernSummon`(`TavernSummonMediator` + `TavernSummonView`)
|
||
|
||
**两种宝箱**:
|
||
- 白银宝箱(type=1):CD 时间到给免费次数,或扣 `silverBoxOpenItem` × `silverBoxOpenItemNum` 数量;不足时可用钻石(`shopPrice`)
|
||
- 黄金宝箱(type=2):同理,CD 长(`s_BuildingTavern.goldBoxCD`)
|
||
|
||
**操作**:
|
||
- 免费开 / 单开 / 全开(≥10 个时显示)
|
||
- 协议:`Build_Tavern(type, free, count, useDenar)`
|
||
- 响应:`RewardInfo { items, heros, ... }` + `count` + `type`
|
||
|
||
**结果展示**:
|
||
- 单开(count < 10):4 格抽奖动画,热英雄碎片插入随机位置
|
||
- 多开(count ≥ 10):跳转 `UI.s_tavernReward` 滚动展示完整列表
|
||
|
||
> **酒馆只产出"碎片+道具",不直接给英雄实体!** 英雄实体一律通过 `Hero_SummonHero` 流程。
|
||
|
||
---
|
||
|
||
## 8. 与 Survivors 实现的对照点
|
||
|
||
### 8.1 架构对照
|
||
|
||
| 维度 | ROK | Survivors(迁移后) |
|
||
|---|---|---|
|
||
| 客户端框架 | PureMVC + ILRuntime/IFix | ET + YIUI + HybridCLR |
|
||
| 协议 | sproto | protobuf3 |
|
||
| 服务端 | C++ + Lua(snax proxy) | ET Server(Scene-Entity 模型) |
|
||
| 配置 | Lua `tabtoy` → `Configs.data` | Luban → JSON + 生成 C# |
|
||
| 持久化 | 自研 `MSM.d_hero[rid]` 内存 + 落库 | MongoDB(`cn.etetet.db` 封装) |
|
||
|
||
### 8.2 PureMVC Proxy → ET Component 映射
|
||
|
||
| ROK | Survivors |
|
||
|---|---|
|
||
| `HeroProxy` (PureMVC) | `HeroComponent` 挂在 Player Entity 上 |
|
||
| `HeroProxy.Hero` 包装类 | `Hero` 子 Entity(继承 ET Entity) |
|
||
| `HeroProxy.UpdateHeroInfo(notification)` | `Hero_HeroInfoHandler` 服务端推送消息处理器 |
|
||
| `HeroProxy.AnalysisHero` 初始化 | `HeroComponent.Awake` 时根据所有 `HeroConfig` 创建占位 |
|
||
| `HeroProxy.GetHerosBySort` | `HeroComponent.GetHerosBySort`(保留 4 种排序) |
|
||
| `HeroCmd` Notification 分发 | ET 事件系统 `EventSystem.Publish<HeroLevelUpEvent>` |
|
||
|
||
### 8.3 sproto → protobuf 字段映射
|
||
|
||
| sproto type | protobuf 等价 |
|
||
|---|---|
|
||
| `integer` | `int64`(默认)或 `int32`(明确小字段) |
|
||
| `boolean` | `bool` |
|
||
| `string` | `string` |
|
||
| `*Type` (list) | `repeated Type` |
|
||
| `*Type(key)` (map by key) | `map<int64, Type>`,**注意**:sproto 用对象字段当 key,protobuf map 是显式 KV |
|
||
|
||
> **重要差异**:
|
||
> - sproto `*HeroInfo(heroId)` 是"以 heroId 字段聚类的数组" → protobuf 应映射为 `map<int64, HeroInfo>`
|
||
> - sproto `*TalentTrees(index)` → `map<int32, TalentTrees>`
|
||
> - sproto 的 tag 不需要在 protobuf 中保留,按字段顺序新分配即可,但**建议保留语义化分组**
|
||
|
||
### 8.4 配置表对照
|
||
|
||
| ROK Define | Survivors Luban Bean | 备注 |
|
||
|---|---|---|
|
||
| `HeroDefine` | `HeroConfig` | 直接对齐,`getItem/getItemNum` 保留 |
|
||
| `HeroLevelDefine` | `HeroLevelConfig` | 主键 `(rareGroup, lv)` 双键 |
|
||
| `HeroStarDefine` | `HeroStarConfig` | `rare1-5` 字段保留,或重构成 `map<rareGroup, int> reqExp` |
|
||
| `HeroStarExpDefine` | `HeroStarExpConfig` | 1:1 |
|
||
| `HeroSkillDefine` | `HeroSkillConfig` | 1:1 |
|
||
| `HeroSkillEffectDefine` | `HeroSkillEffectConfig` | 主键 `skillId * 1000 + level` 公式保留 |
|
||
| `HeroSkillLevelDefine` | `HeroSkillLevelConfig` | 1:1 |
|
||
| `HeroTalentDefine` | `HeroTalentConfig` | `gainTree` 由 string 改 int(Luban 类型严格) |
|
||
| `HeroTalentGainTreeDefine` | `HeroTalentNodeConfig` | **建议改名**避免单词冗长 |
|
||
| `HeroTalentMasteryDefine` | `HeroTalentMasteryConfig` | 1:1 |
|
||
|
||
### 8.5 RPC 命名映射
|
||
|
||
| ROK sproto | Survivors protobuf | ResponseType |
|
||
|---|---|---|
|
||
| `Hero_SummonHero` | `C2G_SummonHero` | `G2C_SummonHero` |
|
||
| `Hero_AddHeroExp` | `C2G_HeroAddExp` | `G2C_HeroAddExp` |
|
||
| `Hero_HeroSkillLevelUp` | `C2G_HeroSkillUp` | `G2C_HeroSkillUp` |
|
||
| `Hero_HeroAwake` | `C2G_HeroAwake` | `G2C_HeroAwake` |
|
||
| `Hero_ExchangeHeroItem` | `C2G_ExchangeHeroItem` | `G2C_ExchangeHeroItem` |
|
||
| `Hero_HeroStarUp` | `C2G_HeroStarUp` | `G2C_HeroStarUp` |
|
||
| `Hero_TalentUp` | `C2G_TalentUp` | `G2C_TalentUp` |
|
||
| `Hero_ChangeTalentIndex` | `C2G_ChangeTalentIndex` | `G2C_ChangeTalentIndex` |
|
||
| `Hero_ResetTalent` | `C2G_ResetTalent` | `G2C_ResetTalent` |
|
||
| `Hero_ModifyTalentName` | `C2G_ModifyTalentName` | `G2C_ModifyTalentName` |
|
||
| `Hero_HeroWearEquip` | `C2G_HeroWearEquip` | `G2C_HeroWearEquip` |
|
||
| `Hero_TakeOffEquip` | `C2G_TakeOffEquip` | `G2C_TakeOffEquip` |
|
||
| `Hero_HeroInfo`(推送) | `G2C_HeroInfo`(推送) | - |
|
||
|
||
### 8.6 数据存储映射
|
||
|
||
| ROK | Survivors |
|
||
|---|---|
|
||
| `MSM.d_hero[rid]` | MongoDB collection `Hero`(按 `playerId + heroId` 复合主键) |
|
||
| `req.Get / req.Set / req.Add` | `DBComponent.Query<Hero> / .Save<Hero>` |
|
||
| `syncHero` 推送 | `MessageHelper.SendToClient(playerId, new G2C_HeroInfo {...})` |
|
||
| `cacleSyncHistoryPower` | `PlayerComponent.RecalcPower()` + 推送 |
|
||
|
||
---
|
||
|
||
## 9. 实现优先级建议
|
||
|
||
按 ROK 业务复杂度 + Survivors 现有能力评估,建议拆 4 期实现。
|
||
|
||
### P1 - 必做基础(招募 + 升级)⭐⭐⭐
|
||
|
||
> **目标**:跑通"获得碎片 → 招募 → 升级到等级上限"完整链路。
|
||
|
||
- [ ] `HeroConfig` + `HeroLevelConfig` + `HeroStarConfig` 三张表 Luban 化
|
||
- [ ] `Hero` Entity 设计(仅持久化 `heroId/star/starExp/level/exp/summonTime/skills`)
|
||
- [ ] `HeroComponent` 挂在 Player Entity 上
|
||
- [ ] RPC:`C2G_SummonHero` + `C2G_HeroAddExp`
|
||
- [ ] 服务端业务:`SummonHero` 校验链 + `addHeroExp` 循环升级 + 等级封顶
|
||
- [ ] 推送:`G2C_HeroInfo`
|
||
- [ ] GM 命令:`gm.hero.add(heroId)` / `gm.hero.addExp(heroId, amount)` / `gm.hero.list`
|
||
- [ ] 战力计算:`base + level`(先简化,跳过技能/天赋/装备)
|
||
|
||
### P2 - 战斗相关(升星 + 技能 + 装备)⭐⭐
|
||
|
||
> **目标**:英雄养成的核心数值闭环。
|
||
|
||
- [ ] `HeroStarExpConfig` + `HeroSkillConfig` + `HeroSkillEffectConfig` + `HeroSkillLevelConfig`
|
||
- [ ] RPC:`C2G_HeroStarUp` + `C2G_HeroSkillUp` + `C2G_HeroAwake`
|
||
- [ ] 升星:6 个材料 + 幸运双倍机制(`lucky >= Random(1,100)`)
|
||
- [ ] 升星:解锁高阶技能(`unlockSkill`)
|
||
- [ ] 技能升级:**服务端随机选目标技能** + 5 级满 + 触发觉醒
|
||
- [ ] 觉醒:第 5 技能 ID = `heroId * 100 + 5`
|
||
- [ ] 8 槽位装备:`EquipConfig` + `EquipAttConfig` + `EquipComposeConfig`
|
||
- [ ] RPC:`C2G_HeroWearEquip` + `C2G_TakeOffEquip`
|
||
- [ ] 装备切换英雄时自动从旧英雄卸下
|
||
- [ ] 套装属性(2/4/6/8 件)
|
||
- [ ] 战力计算:加 `skill` 分
|
||
|
||
### P3 - 天赋系统 ⭐⭐
|
||
|
||
> **目标**:高阶差异化养成。
|
||
|
||
- [ ] `HeroTalentConfig` + `HeroTalentGainTreeConfig` + `HeroTalentMasteryConfig`
|
||
- [ ] `s_ZeroEmpty.TALENT / HERO_LEVEL_TALENT / HERO_STAR_TALENT` 三张全局表(Survivors 可改为 `HeroTalentGlobalConfig` 单表)
|
||
- [ ] RPC:`C2G_TalentUp` + `C2G_ChangeTalentIndex` + `C2G_ResetTalent`
|
||
- [ ] 学习单节点:5 个校验链(点足 / 能学 / 前置 / 同级互斥)
|
||
- [ ] 切换页:当前页切换才收费
|
||
- [ ] 重置页:仅当前页收费
|
||
- [ ] 战力计算:加 `talent` 节点分 + mastery 分
|
||
|
||
### P4 - UI 一次性补齐 + 次要功能 ⭐
|
||
|
||
> **目标**:把 P1-P3 服务端业务用 YIUI 包成可玩界面,集中做不返工。
|
||
|
||
- [ ] `HeroListPanel`:排序(4 种)+ 三段分组 + 选中详情
|
||
- [ ] `HeroDetailPanel`:信息展示 + 升级/升星/技能/天赋/装备入口
|
||
- [ ] `HeroSummonResultPanel`:获得新英雄 3D 展示
|
||
- [ ] `HeroStarUpPanel`:材料选择 + 幸运进度
|
||
- [ ] `HeroSkillUpPanel`:5 个技能 + 升级按钮(随机机制要 tooltip 说明)
|
||
- [ ] `HeroTalentPanel`:3 个 tab + 天赋树 + 切换/重置/改名
|
||
- [ ] `HeroEquipPanel`:8 个槽位 + 选装备子面板
|
||
- [ ] **可推迟**:雕像兑换 (`C2G_ExchangeHeroItem`) / 天赋页改名 (`C2G_ModifyTalentName`) / 红点统计
|
||
|
||
### 可推迟(不阻塞)
|
||
|
||
- 副将系统:ROK 中副将影响军队属性(不享受天赋加成),Survivors 单机肉鸽不需要"军队",可砍掉
|
||
- 行动力减免:与 Survivors 战斗模式不兼容
|
||
- 限时礼包触发:与付费系统耦合
|
||
- 怪物英雄(`s_MonsterTroops`):仅用于 SLG 野怪,Survivors 不需要
|
||
- 部队属性同步到战斗服务器:Survivors 是单机肉鸽,没有战斗服务器
|
||
- 城防主将自动调整(`BuildingLogic:changeDefendHero`):与城建系统耦合
|
||
|
||
---
|
||
|
||
## 附录 A:实际数据样本
|
||
|
||
**HeroDefine 样本**(`hero.json` ID=1000):
|
||
|
||
```json
|
||
{
|
||
"ID": 1000, "civilization": 101, "initStar": 1, "star": 6, "rare": 5,
|
||
"score": 1100, "getItem": 509010001, "getItemNum": 10,
|
||
"exchange": 503050004, "getLimit": 0,
|
||
"skill": [100001, 100002, 100003, 100004, 100005],
|
||
"talent": [100001, 100002, 100003],
|
||
"heroIcon": "hero_head[hero_head_1000]", "heroModel": "Hero_1000_SD"
|
||
}
|
||
```
|
||
|
||
**HeroStarDefine 样本**(`hero_star.json` 5★):
|
||
|
||
```json
|
||
{
|
||
"ID": 5, "starLimit": 50,
|
||
"rare5": 65000, "rare4": 48000, "rare3": 30000, "rare2": 20000, "rare1": 10000,
|
||
"starEffect": 2, "starEffectData": 1
|
||
}
|
||
```
|
||
|
||
**HeroSkillLevelDefine 样本**(`hero_skill_level.json` 第 6 次升级):
|
||
|
||
```json
|
||
{
|
||
"ID": 105, "level": 6,
|
||
"costItem5": 30, "costItem4": 20, "costItem3": 20, "costItem2": 10, "costItem1": 10
|
||
}
|
||
```
|
||
|
||
**HeroStarExpDefine 样本**(`hero_star_exp.json` 2 星材料):
|
||
|
||
```json
|
||
{
|
||
"ID": 1001, "rareGroup": 2, "itemID": 503010002,
|
||
"exp": 400, "lucky": 20, "sure": 0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 B:调研中发现的关键设计/坑
|
||
|
||
1. **`talentPoint` 字段已废弃**——`HeroDef.lua` 仍保留但 `HeroLogic.lua` 中所有相关代码已注释掉;实际天赋点通过 `checkTalentPoint` 实时计算。重写时**不要持久化这个字段**,避免逻辑混乱。
|
||
2. **技能升级随机性**——服务端不允许玩家选升哪个技能(`Random.GetId(skillIds)` 等权重)。前端 UI 需要清楚地告知玩家"随机升级一个未满技能"。
|
||
3. **同稀有度共享 cost**——`HeroSkillLevelDefine` 用 `costItem1-5` 区分稀有度的消耗量,5 星比 1 星贵 4-8 倍。
|
||
4. **升星与等级互锁**——必须先升满当前星级的等级上限才能升星,升星后等级上限提升。
|
||
5. **碎片是通用货币**——招募 + 升技能 + 雕像兑换都用同一个 `sHero.getItem` 道具,玩家不会有"碎片够多但不知道干嘛"的窘境。
|
||
6. **战力不含装备**——`HeroCacle:caclePower` 不算装备分;装备只影响战斗时实际属性(`getHeroAttr`)。
|
||
7. **副将不享受天赋加成**——`getHeroAttr` 对副将传 `notCancleTalent=true`。
|
||
8. **觉醒技能 ID 公式固定**——`heroId * 100 + 5`;前 4 技能 ID 通常为 `heroId * 100 + 1..4`,但实际由 `s_Hero.skill` 数组定义,不要假设。
|
||
9. **`skillEffect` 主键公式**——`skillId * 1000 + level`;查询不到表示已满级。
|
||
10. **`hero_level` 主键有 -1 偏移**——客户端 `HeroProxy.cs:184` 用 `(int)level + config.rare * 10000 - 1`,服务端 Lua 直接用 `rare * 10000 + level`;**两者实际相差 1**,怀疑客户端有 bug 或者表 ID 设计就是从 0 开始。重写时应统一使用一种公式,建议沿用服务端逻辑 (`rare * 10000 + level`)。
|
||
11. **3 套天赋页共享同一池天赋点**——而非"每页一个独立池"。重置或切换页只是清空已学列表,可用点数总数不变。
|
||
12. **第 5 技能不通过 `unlockSkill`**——`HeroLogic.lua:394` 明确判断 `_star ~= 5`,第 5 技能(觉醒)由 `heroAwake` 单独负责。
|
||
13. **`Build_Tavern` 是英雄碎片主入口**——但 ROK 工程同时还有 `Shop_GetLimitHeroInfo`(限时商店)和活动奖励等其他来源;碎片本质就是普通道具。
|
||
|
||
---
|
||
|
||
## 附录 C:未在 ROK 中找到 / 不存在的内容
|
||
|
||
1. **抽卡协议**:ROK 完全没有"抽英雄实体"的协议。`Build_Tavern` 是抽宝箱(产出碎片+道具)。
|
||
2. **保底机制**:宝箱奖励是配置表 `s_TavernRank` 按权重随机,无显式保底。
|
||
3. **英雄拥有数量上限**:源码无任何限制。
|
||
4. **`HeroTalentGainTreeConfig` / `HeroTalentConfig` / `HeroTalentMasteryConfig` 的 JSON 数据**:`gmd\Unity\Assets\Bundles\Config\` 下不存在这 3 张表的 json(只有 hero/hero_level/hero_skill 等基础表)。**ROK 配置只在 `Configs.data` 加密 SQLite + 服务端 Lua 中**,需要从那里导出。
|
||
5. **升星动画/特效配置**:未在 hero 表中找到,可能在通用 `s_Effect` 表里。
|