XtGameKit/Doc/ROK-Hero-Spec.md

68 KiB
Raw Permalink Blame History

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.lua923 行)
  • 服务端 RPC HandlerServer/server/game_server/logic/service/proxy/Hero.lua521 行)
  • 服务端 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
  • 客户端 ProxyClient/Assets/Scripts/Hotfix/MVC/Proxy/HeroProxy.cs983 行)
  • 客户端 NotificationClient/Assets/Scripts/Hotfix/MVC/CMD/HeroCmd.cs
  • 客户端列表+详情:Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Captain/CaptainMediator.cs1350 行)
  • 客户端配置类:Client/Assets/Scripts/Hotfix/Config/Hero*Define.cs10 个)
  • 客户端错误码: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 子结构

SkillInfoCommon.sproto 行 318

字段 类型 含义
skillId int 技能配置 ID对应 s_HeroSkill
skillLevel int 当前技能等级1 起)

运行时 getRoleAllHeroSkills 会临时附加 talent: true(天赋衍生技能)、deputySkill: true(副将技能)这两个不持久化的标记位。

TalentTreesCommon.sproto 行 334

字段 类型 含义
index int 分页索引 (1/2/3)
talentTree []int 已学习的天赋节点 ID 列表(s_HeroTalentGainTree 表 ID
name string 天赋页自定义名称(玩家可改)

2.3 同一玩家可拥有英雄数量上限

无显式上限HeroLogic:addHero 仅判断"是否已拥有该 heroId"——已拥有则把招募道具转为对应数量的同 ID 道具退回(碎片返还,见 §4.1)。可拥有英雄数 = 配置表 s_HerolistDisplay ≠ 1 的条目数。

实际 hero.json46 条(其中部分为 listDisplay=1 的怪物英雄不参与列表),玩家最多拥有的英雄数 ≈ 配置表中 listDisplay=0 的条数。


3. 配置表清单

来源:Client/Assets/Scripts/Hotfix/Config/Hero*Define.csC# 反序列化 DTO与 ROK 服务端 CFG.s_Hero* 一一对应)+ 实际数据 gmd\Unity\Assets\Bundles\Config\hero*.json46 / 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=509010001getItemNum=1010 个碎片可招募),star=6skill=[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 → IsMaxLevelexp==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 所属树组 IDHeroTalent.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 handlerL* = HeroLogic.lua 函数。

4.1 招募 / 兑换英雄

4.1.1 英雄获取来源(真实业务流

ROK 中英雄获取链路如下,绝非"抽卡"

酒馆开宝箱 (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 这一步。

recruitLimitROK 源码无此字段。判定逻辑仅有 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 完整流程(伪代码)

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

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_ERRORHERO_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 + levels_HeroLevel:Getexp 字段,即"升到下一级所需经验"
  • 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

if heroInfo.level == sHeroStar.starLimit: return false

到达上限后继续加经验:经验清零(行 434-436而非保留。

4.2.5 完整流程(伪代码)

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

    total_exp = Σ(material.exp × material.itemNum)
    total_lucky = Σ(material.lucky × material.itemNum)
    

4.3.2 幸运系数

Hero.lua 行 231-233

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

if heroInfo.level < sHeroStar.starLimit: return ERR(HERO_LEVEL_NO_ENOUGH)

4.3.6 完整流程(伪代码)

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 + nn=1..4)。
  • 槽位 5觉醒技能。技能 ID 为 heroId * 100 + 5。仅当 1-4 技能全部满级5 级)时自动解锁(见 §4.5)。

4.4.2 升级消耗

  • 道具sHero.getItem(即招募碎片)。升技能与招募共享同一道具池

  • 数量:查 s_HeroSkillLevel,主键计算:

    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 升级上限

  • 单技能最大等级 = 5HeroProxy.IsAllSkillMaxGetSkillAllLevel() >= 20,即 4 个技能 × 5 级)
  • 整体上限通过 s_HeroSkillEffect 表是否存在 (skillId * 1000 + nextLevel) 来判定(HeroLogic.lua 行 158

4.4.4 升级目标的随机性 ⚠️

这是 ROK 一个非常重要且非直观的设计——服务端不允许玩家选择升哪个技能

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) < 4false < 4 总成立)。实际生效条件为第二部分 checkHeroSkillFull。重写时按"技能数 >= 4 且全满"标准实现即可。

4.5.2 觉醒消耗

无任何消耗——heroAwake 直接把第 5 技能加入 heroInfo.skillslevel=1。

4.5.3 觉醒效果

  • 解锁第 5 个技能(觉醒技),等级固定 1。
  • 技能 ID 公式:heroId * 100 + 5
  • 战力重算 + 同步部队

4.5.4 自动 vs 手动

  • 自动:在 heroSkillLevelUp 升级技能后会判断,如果满足条件自动调 heroAwake,无需玩家主动操作。
  • 手动Hero_HeroAwake RPC 是冗余入口(兼容性),实际由前 4 技能升满后自动触发。

4.5.5 完整流程

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

实际"当前可用天赋点"通过实时计算

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_HeroLevels_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) 计算:

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 完整流程(伪代码)

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(不是 itemId0 = 未装备。

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] = itemIndexitemInfo.heroId = heroId

4.7.4 卸下装备

Hero_TakeOffEquip (612)

  • 校验英雄待命
  • itemInfo.heroId = 0heroInfo[attr] = 0
  • 同步 Hero + Item

4.7.5 与背包的联动(字段需要在 Item Entity 加 heroId 反向索引

  • s_Item 是道具基础,s_Equip 是装备扩展(compose 套装组 ID、useLevelattAddEx 等)
  • s_EquipAtt 定义属性枚举
  • s_EquipCompose 定义套装:compose2/4/6/8 件数效果,分别提供 compose2/4/6/8AddEx 属性加成
  • 专属装备加成itemInfo.exclusive > 0checkHeroTalent(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

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

战斗时按属性名查询英雄某项属性:

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 战力变化触发的事件

任意会影响战力的操作完成后会调:

RoleLogic:cacleSyncHistoryPower(rid, nil, nil, recalc=true)  // 重算并同步

并对涉及到的部队同步:

updateMapArmyHero(rid, heroId, newLevel?):
    遍历角色所有部队
    如果该 heroId 是 main/deputy
        重新算 skills + talentAttr + levels
        通过 BattleAttrLogic:syncObjectHeroChange 通知战斗服务器
        通过 ResourceLogic:roleArmyCollectSpeedChange 更新采集速度

4.9 其他次要功能

4.9.1 英雄列表排序

4 种排序HeroProxy.SortTypeRare / Star / Level / Power(还有 Recomend 未实现)。

所有比较器(CompareHeroByXxx,共 4 个)都遵循通用规则:

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

  • ownhero.data != null(已拥有)
  • summonhero.itemCount >= getItemNum(碎片够,可召唤)
  • nosummonitemCount < 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 × itemNum1: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统帅总览

PanelUI.s_captainCaptainMediator + CaptainView

注册 Notification

  • CmdConstant.GetNewHero - 拿到新英雄
  • Hero_HeroStarUp.TagName - 升星响应
  • CmdConstant.UpdateHero - 英雄数据更新
  • Hero_HeroSkillLevelUp.TagName - 技能升级响应
  • CmdConstant.ItemInfoChange - 道具变化(用于刷新升技能道具显示)
  • CmdConstant.HeroListVisible - 显示/隐藏列表
  • CmdConstant.RefreshEquipRedPoint - 装备红点
  • CmdConstant.HeroSceneVisible - 3D 场景显示

关键流程

  1. 进入面板 → InitData 加载 6 个 prefabUI_LC_CaptainUI_Item_CaptainHeadUI_Item_CaptainSummonUI_Item_CaptainPartlineUI_Item_CaptainSkillUI_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_GameButtonShowStarUp → 检查 level >= starLimit → 打开升星面板
  • 点击 StandardButton_skillupShowSkillUp → 打开技能升级面板
  • 点击 StandardButton_giftShowTalent → 打开天赋树面板
  • 点击 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 经验升级流程

PanelUI_Win_CaptainLevelUp (UI_Win_CaptainLevelUpMediator)

  1. 在详情面板 → 经验条上点 "+" → 打开经验升级窗
  2. 列出背包中所有 subType == 经验书 的道具
  3. 玩家选数量 + 确认 → 发 Hero_AddHeroExp(heroId, itemId, itemNum)
  4. 服务端响应后 HeroProxy.UpdateHeroInfo 推送UI 自动刷新经验/等级

7.4 升星流程

PanelUI_Item_CaptainStarUp(嵌入 CaptainView 内)

入口m_btn_starUp_GameButton

  1. 校验 level >= sHeroStar.starLimit(不够提示 Tip 文本 166056说明"先升到 X 级才能升星"
  2. 显示当前 star/starExp 进度条 + 可选材料列表(来自 s_HeroStarExprareGroup == 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 技能升级流程

PanelUI_Item_CaptainSkillUp(嵌入 CaptainView 内)

入口m_UI_Item_CaptainData.AddSkillUpButtonClickedEventShowSkillUp

  1. 显示该英雄的 5 个技能 + 当前等级
  2. 显示升级所需碎片数 (HeroProxy.Hero.GetSkillLevelUpCostItemNum) + 背包余量
  3. 点击"升级技能"按钮 → 发 Hero_HeroSkillLevelUp(heroId)
  4. 服务端响应 {skillId, skillLevel}HeroSkillLevelUpSuceess 高亮"被升级"的那个技能(玩家不知道会升哪个
  5. 若升级后达到觉醒条件:服务端自动触发 heroAwake,再推一次 Hero_HeroInfo
  6. 升级成功也会弹 UI_IF_CaptainSkillUpSuccess 显示效果

7.6 天赋树流程

PanelUI_Item_CaptainTalent(嵌入 CaptainView 内)+ 子 SubView UI_Item_CaptainTalent_SubView

入口m_UI_Item_CaptainData.AddTalentButtonClickedEventShowTalent

  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 装备穿戴流程

PanelUI_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 雕像兑换流程

PanelUI_Win_CaptainItemSource(兑换 + 道具来源面板)

  1. 玩家进入英雄详情,看到该英雄的招募碎片 → 点击 "+" → 显示获取来源
  2. sHero.exchange > 0(可雕像兑换)→ 显示"用通用雕像兑换"入口
  3. 玩家选数量 → 发 Hero_ExchangeHeroItem(heroId, itemNum)
  4. 服务端 1:1 转换 → 同步背包道具

7.9 酒馆开宝箱(碎片来源)

PanelUI.s_tavernSummonTavernSummonMediator + 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 tabtoyConfigs.data Luban → JSON + 生成 C#
持久化 自研 MSM.d_hero[rid] 内存 + 落库 MongoDBcn.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 上
  • RPCC2G_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
  • RPCC2G_HeroStarUp + C2G_HeroSkillUp + C2G_HeroAwake
  • 升星6 个材料 + 幸运双倍机制(lucky >= Random(1,100)
  • 升星:解锁高阶技能(unlockSkill
  • 技能升级:服务端随机选目标技能 + 5 级满 + 触发觉醒
  • 觉醒:第 5 技能 ID = heroId * 100 + 5
  • 8 槽位装备:EquipConfig + EquipAttConfig + EquipComposeConfig
  • RPCC2G_HeroWearEquip + C2G_TakeOffEquip
  • 装备切换英雄时自动从旧英雄卸下
  • 套装属性2/4/6/8 件)
  • 战力计算:加 skill

P3 - 天赋系统

目标:高阶差异化养成。

  • HeroTalentConfig + HeroTalentGainTreeConfig + HeroTalentMasteryConfig
  • s_ZeroEmpty.TALENT / HERO_LEVEL_TALENT / HERO_STAR_TALENT 三张全局表Survivors 可改为 HeroTalentGlobalConfig 单表)
  • RPCC2G_TalentUp + C2G_ChangeTalentIndex + C2G_ResetTalent
  • 学习单节点5 个校验链(点足 / 能学 / 前置 / 同级互斥)
  • 切换页:当前页切换才收费
  • 重置页:仅当前页收费
  • 战力计算:加 talent 节点分 + mastery 分

P4 - UI 一次性补齐 + 次要功能

目标:把 P1-P3 服务端业务用 YIUI 包成可玩界面,集中做不返工。

  • HeroListPanel排序4 种)+ 三段分组 + 选中详情
  • HeroDetailPanel:信息展示 + 升级/升星/技能/天赋/装备入口
  • HeroSummonResultPanel:获得新英雄 3D 展示
  • HeroStarUpPanel:材料选择 + 幸运进度
  • HeroSkillUpPanel5 个技能 + 升级按钮(随机机制要 tooltip 说明)
  • HeroTalentPanel3 个 tab + 天赋树 + 切换/重置/改名
  • HeroEquipPanel8 个槽位 + 选装备子面板
  • 可推迟:雕像兑换 (C2G_ExchangeHeroItem) / 天赋页改名 (C2G_ModifyTalentName) / 红点统计

可推迟(不阻塞)

  • 副将系统ROK 中副将影响军队属性不享受天赋加成Survivors 单机肉鸽不需要"军队",可砍掉
  • 行动力减免:与 Survivors 战斗模式不兼容
  • 限时礼包触发:与付费系统耦合
  • 怪物英雄(s_MonsterTroops):仅用于 SLG 野怪Survivors 不需要
  • 部队属性同步到战斗服务器Survivors 是单机肉鸽,没有战斗服务器
  • 城防主将自动调整(BuildingLogic:changeDefendHero):与城建系统耦合

附录 A实际数据样本

HeroDefine 样本hero.json ID=1000

{
  "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★

{
  "ID": 5, "starLimit": 50,
  "rare5": 65000, "rare4": 48000, "rare3": 30000, "rare2": 20000, "rare1": 10000,
  "starEffect": 2, "starEffectData": 1
}

HeroSkillLevelDefine 样本hero_skill_level.json 第 6 次升级):

{
  "ID": 105, "level": 6,
  "costItem5": 30, "costItem4": 20, "costItem3": 20, "costItem2": 10, "costItem1": 10
}

HeroStarExpDefine 样本hero_star_exp.json 2 星材料):

{
  "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——HeroSkillLevelDefinecostItem1-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 表里。