XtGameKit/Doc/ROK-Hero-Spec.md

1442 lines
68 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.

# ROK 英雄系统业务规则1:1 迁移依据)
> **来源**`E:\Game\gmd\ROK` 服务端 Luasnax `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`(行 9681107、26192627+ `Common.sproto`(行 318349 的 `.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` 行 14917630003027
---
## 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 | 兑换通用雕像的道具 ID0 = 不可兑换 |
| `l_heroLanguage` | `[]int` | 英雄台词语言包列表(点角色随机说话) |
| `getLimit` | int | 招募解锁的王国开服天数 |
| `skill` | `[]int` | 该英雄的 5 个技能 ID索引 1-4 为普通技能,索引 5 为觉醒技能;可能少于 5 |
| `talent` | `[]int` | 该英雄的天赋节点 ID3 个,对应 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文明 1015 星传说):`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→102→203→304→405→506→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 星 65003/2/1 星 5000
> - 4★→5★5 星 250004 星 160003 星 125002 星 100001 星 8000
> - 5★→6★5 星 650004 星 480003 星 300002 星 200001 星 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 | 技能 ID46 位,如 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` 行 2426Role+ 149176Hero+ 224Building+ 473Expedition
| 错误码 | 数值 | 含义 |
|---|---|---|
| `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=1CD 时间到给免费次数,或扣 `silverBoxOpenItem` × `silverBoxOpenItemNum` 数量;不足时可用钻石(`shopPrice`
- 黄金宝箱type=2同理CD 长(`s_BuildingTavern.goldBoxCD`
**操作**
- 免费开 / 单开 / 全开≥10 个时显示)
- 协议:`Build_Tavern(type, free, count, useDenar)`
- 响应:`RewardInfo { items, heros, ... }` + `count` + `type`
**结果展示**
- 单开count < 104 格抽奖动画,热英雄碎片插入随机位置
- 多开count ≥ 10跳转 `UI.s_tavernReward` 滚动展示完整列表
> **酒馆只产出"碎片+道具",不直接给英雄实体!** 英雄实体一律通过 `Hero_SummonHero` 流程。
---
## 8. 与 Survivors 实现的对照点
### 8.1 架构对照
| 维度 | ROK | Survivors迁移后 |
|---|---|---|
| 客户端框架 | PureMVC + ILRuntime/IFix | ET + YIUI + HybridCLR |
| 协议 | sproto | protobuf3 |
| 服务端 | C++ + Luasnax proxy | ET ServerScene-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 用对象字段当 keyprotobuf 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 改 intLuban 类型严格) |
| `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` 表里。