# 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` | 三套天赋页数据,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` | ### 8.3 sproto → protobuf 字段映射 | sproto type | protobuf 等价 | |---|---| | `integer` | `int64`(默认)或 `int32`(明确小字段) | | `boolean` | `bool` | | `string` | `string` | | `*Type` (list) | `repeated Type` | | `*Type(key)` (map by key) | `map`,**注意**:sproto 用对象字段当 key,protobuf map 是显式 KV | > **重要差异**: > - sproto `*HeroInfo(heroId)` 是"以 heroId 字段聚类的数组" → protobuf 应映射为 `map` > - sproto `*TalentTrees(index)` → `map` > - sproto 的 tag 不需要在 protobuf 中保留,按字段顺序新分配即可,但**建议保留语义化分组** ### 8.4 配置表对照 | ROK Define | Survivors Luban Bean | 备注 | |---|---|---| | `HeroDefine` | `HeroConfig` | 直接对齐,`getItem/getItemNum` 保留 | | `HeroLevelDefine` | `HeroLevelConfig` | 主键 `(rareGroup, lv)` 双键 | | `HeroStarDefine` | `HeroStarConfig` | `rare1-5` 字段保留,或重构成 `map 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 / .Save` | | `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` 表里。