33 KiB
ROK 英雄获取机制业务规则(1:1 迁移依据)
来源:
E:\Game\gmd\ROK服务端 Lua + 客户端 C# Proxy/View + sproto 协议 用途:澄清 ROK 是否有抽卡 / 玩家究竟如何获得英雄,作为 Survivors 工程移植 / 设计英雄包的依据 调研日期:2026-05-27
0. 核心结论(一句话)
ROK 不是传统抽卡游戏。 它走的是 「随机宝箱产出碎片 → 玩家主动用碎片兑换指定英雄」 的两段式获取链,而不是「英雄卡池 + 权重 + 单抽十连保底」。
完整链路(一张图):
┌─────────────────────────────────────────────────────────────────────┐
│ ROK 英雄获取完整链路 │
└─────────────────────────────────────────────────────────────────────┘
【产出端 - 随机】 【中间产物】 【兑换端 - 确定性】
酒馆白银宝箱 ──┐ ┌── 英雄招募碎片 A
(Build_Tavern │ │ (HeroDefine.getItem)
silverBox) │ │
│ │ ┌──────────────┐
酒馆黄金宝箱 ──┤───→ 服务端 ItemPackage 随机 ────→├──→│ Captain UI │
(Build_Tavern │ (silverBoxItemPackage) │ │ 点击"召唤" │
goldBox) │ (goldBoxItemPackage) │ └──────┬───────┘
│ │ │
任务/活动/邮件─┤ 产出可以是: │ ▼
/充值/商店 ─┤ • ITEM (碎片,最常见) │ Hero_SummonHero RPC
奖励 ─┤ • HERO (直接给英雄,罕见) │ (heroId, 扣 getItemNum 个
│ • 资源/士兵/... │ getItem 碎片,确定性获得)
特殊:新手引导─┘ │
完成"攻击野蛮人" │
→ 直接给 guideHero │
│
──────────────────────────────────────────────── │
关键:碎片产出端"似抽卡",但英雄本体的"获取"环节是 │
确定性兑换,没有随机、没有保底、没有十连。 │
──────────────────────────────────────────────── │
│
雕像兑换 (Hero_ExchangeHeroItem 605) │
把"通用英雄雕像"道具转换为某英雄的招募碎片,──────┘
不是直接兑换英雄。
对 Survivors cn.etetet.gacha 包的建议(详见第 7 节):
- ROK 没有"抽卡",因此 1:1 迁移角度建议改名为
cn.etetet.summon(英雄碎片兑换召唤)+cn.etetet.tavern(酒馆开箱产出碎片); - 用户在前序对话已明确表态"还需要抽卡的逻辑用来验证产出",因此
cn.etetet.gacha作为 Survivors 独立扩展功能保留,但应明确定位它是「在 ROK 基础之上新增」而非「移植 ROK 已有」。
1. 调研方法
1.1 走读文件
| 路径 | 关键发现 |
|---|---|
E:\Game\gmd\ROK\Server\common\protocol\Protocol.sproto |
Hero 模块协议号段 601-700;Tavern 在 Build 模块 407 |
E:\Game\gmd\ROK\Server\common\protocol\Common.sproto |
.Heros / .RewardInfo 结构定义 |
E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\HeroLogic.lua |
addHero 唯一入口;无 summon/recruit/exchange/draw/gacha/lottery/pool 关键字(除 summonTime 字段名) |
E:\Game\gmd\ROK\Server\server\game_server\logic\service\proxy\Hero.lua |
response.SummonHero / response.ExchangeHeroItem 等英雄相关 RPC handler |
E:\Game\gmd\ROK\Server\server\game_server\logic\service\proxy\Build.lua |
response.Tavern 酒馆开箱 RPC handler |
E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\BuildingLogic.lua |
openSilver / openGold 宝箱开启逻辑 |
E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\ItemLogic.lua |
getItemPackage 通用奖励包(含 HERO 类型节点) |
E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\Proxy\HeroProxy.cs |
客户端 SummonHero(id) 入口 |
E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\View_Mediator\Captain\* |
英雄列表 UI / 召唤按钮 |
E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\View_Mediator\Tavern\* |
酒馆双宝箱 UI |
E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\Config\HeroConfig.cs |
HeroDefine schema |
E:\Game\gmd\ROK\Server\common\errorcode\ErrorCode.lua |
HERO 相关错误码 3000-3027 |
1.2 关键字 grep 结果
在 HeroLogic.lua 里 grep summon|recruit|exchange|draw|gacha|lottery|pool|fragment(不区分大小写):
HeroLogic.lua:42: MSM.ActivityRoleMgr[_rid].req.setActivitySchedule( _rid, Enum.ActivityActionType.RECRUIT_HERO_COUNT, 1 )
HeroLogic.lua:60: defHeroAttr.summonTime = os.time()
HeroLogic.lua:97: summonTime = heroInfo.summonTime,
只有三处命中,且全部都是「记录英雄被获取的时间戳」和「活动事件统计计数」,不是抽卡/卡池/概率逻辑。
在客户端 MVC/View_Mediator/ 列目录:
Activity/ Alliance/ Bag/ Captain/ Chat/ Equip/ Hero/(仅 RareView) ...
Tavern/ ← 唯一长得像"抽卡"的目录
Hero/ 目录只有 UI_Hero_RareMediator.cs / UI_Hero_RareView.cs(稀有度展示组件);真正的英雄列表与召唤入口在 Captain/ 目录(统帅=Captain=Hero)。
在 ROK 全工程 grep fragmentItemId|recruitLimit:0 命中。这两个字段名是 Survivors openspec proposal 自己拟的,不来自 ROK。
1.3 阳性 / 阴性结论
| 关键问题 | 结论 | 证据 |
|---|---|---|
| ROK 有传统抽卡卡池(heroId 池 + 权重 + 保底)吗? | 没有 | 服务端无任何 HeroPool / gacha / lottery / weight 配置或逻辑;Hero_SummonHero RPC 内部是 1:1 道具兑换,无 Random 调用 |
| ROK 有"招募"概念吗? | 有,但名字叫"召唤 SummonHero" | Hero_SummonHero 601 协议;客户端按钮 OnSummonClick;语言包文案使用"招募/Recruit" |
| ROK 有"碎片兑换"概念吗? | 有 | HeroDefine.getItem + getItemNum 就是"招募所需碎片 itemId + 数量";玩家凑齐后调用 Hero_SummonHero 扣碎片得英雄 |
| ROK 有其他英雄获取途径吗? | 有 | 新手引导直给 guideHero、文明初始英雄 civilization.initialHero、各种奖励包里直接含 HERO 类型节点(罕见)、酒馆宝箱开出英雄(罕见,主要开出的是碎片) |
fragmentItemId / recruitLimit 在 ROK 存在吗? |
不存在 | grep 全工程 0 命中。等价字段是 getItem;不存在 recruitLimit(同一英雄一辈子只能拥有一次,已拥有再 summon 会报 HERO_ALREADY_EXIST 错误码) |
| ROK 的「酒馆开箱」算不算抽卡? | 算"准抽卡" | Build_Tavern 407 协议,白银/黄金两种宝箱,免费次数+CD+付费三种开法,奖励池为 silverBoxItemPackage / goldBoxItemPackage(通用 ItemPackage),可输出碎片/英雄/资源/士兵 |
2. 英雄获取的所有途径
按"玩家获得英雄实例所占比例"排序(推测):
2.1 ⟨途径 1⟩ 碎片兑换召唤(主路径)
- 名称:英雄召唤 / Summon Hero / 招募
- 玩家入口:Captain UI(统帅大厅)→ 选中一个未拥有的英雄卡 → 点击「召唤」按钮(
UI_Item_CaptainData_SubView.OnSummonClick,文件View_Mediator/Common/Logic/UI_Item_CaptainData_SubView.cs:61-65) - RPC:
Hero_SummonHero (601),参数{ heroId: integer }- 客户端发起:
HeroProxy.cs:567-572SummonHero(int id)
- 客户端发起:
- 服务端核心逻辑(
server/game_server/logic/service/proxy/Hero.lua:18-58):
function response.SummonHero( msg )
local rid, heroId = msg.rid, msg.heroId
-- 1. 参数检查 + 配置检查
if not heroId then return nil, ErrorCode.HERO_ARG_ERROR end
local sHero = CFG.s_Hero:Get( heroId )
if not sHero or table.empty( sHero ) then return nil, ErrorCode.CFG_ERROR end
-- 2. 王国开服天数 >= sHero.getLimit
local openDays = Common.getSelfNodeOpenDays()
if openDays < ( sHero.getLimit or 0 ) then
return nil, ErrorCode.HERO_OPEN_DAYS_NOT_ENOUGH -- 3000
end
-- 3. 必须没拥有过该英雄(同英雄只能召唤一次)
local heroInfo = HeroLogic:getHero( rid, heroId )
if heroInfo and not table.empty( heroInfo ) then
return nil, ErrorCode.HERO_ALREADY_EXIST -- 3002
end
-- 4. 碎片道具数量足够
if not ItemLogic:checkItemEnough( rid, sHero.getItem, sHero.getItemNum ) then
return nil, ErrorCode.HERO_SUMMON_ITEM_NOT_ENOUGH -- 3001
end
-- 5. 扣道具 → 加英雄(确定性,无随机)
ItemLogic:delItemById( rid, sHero.getItem, sHero.getItemNum, nil,
Enum.LogType.SUMMON_HERO_COST_ITEM )
HeroLogic:addHero( rid, heroId )
end
- 消耗 & 产出:
- 消耗:
HeroDefine.getItem物品 ×HeroDefine.getItemNum个 - 产出:1 个完整英雄实例(初始
star = sHero.initStar,level = 1,summonTime = os.time()),并解锁该星级开放的技能(unlockSkill)
- 消耗:
- 是否有概率 / 保底:无。完全确定性。
- 频率限制:每个英雄一辈子只能召唤 1 次(数据库以
heroId为唯一键存储,再次召唤同 ID 报HERO_ALREADY_EXIST)。无每日/每周限制。 - 副作用:
- 统计
Enum.ActivityActionType.RECRUIT_HERO_COUNT活动事件 +1(HeroLogic.lua:42) - 触发限时礼包:
LimitTimeType.NEW_HERO(HeroLogic.lua:83) - 自动更换驻防英雄
BuildingLogic:changeDefendHero - 重算战力
RoleLogic:cacleSyncHistoryPower
- 统计
2.2 ⟨途径 2⟩ 酒馆开箱产出(最主要的"碎片来源")
- 名称:酒馆召唤 / Tavern Summon / 开宝箱
- 玩家入口:城市建筑「酒馆 Tavern」点击进入,出现两种宝箱:
- 白银宝箱 (woodbox):每日免费次数(按酒馆等级
s_BuildingTavern.silverBoxCnt)+ CD(silverBoxCD),免费用完后用silverBoxOpenItem道具或钻石购买 - 黄金宝箱 (goldbox):免费次数 1 次 + CD(
s_BuildingTavern.goldBoxCD),其余消耗goldBoxOpenItem或钻石
- 白银宝箱 (woodbox):每日免费次数(按酒馆等级
- RPC:
Build_Tavern (407),参数{ type, free, count, useDenar }type:1=白银 /2=黄金(Enum.BoxType.SILVER/GOLD)count:批量开启数量(一般 1 或 10)
- 服务端核心逻辑(
server/game_server/logic/service/proxy/Build.lua:297-361+BuildingLogic.lua:openSilver/openGold):
-- 简化伪码
function response.Tavern( msg )
local rid, type, free, count, useDenar = msg.rid, msg.type, msg.free, msg.count, msg.useDenar
if type == Enum.BoxType.SILVER then
if free then
-- 验免费次数 + 验 CD
if roleInfo.silverFreeCount <= 0 then return ErrorCode.BUILD_SILVER_FREE_NOT_ENOHGH end
if roleInfo.openNextSilverTime > os.time() then return ErrorCode.BUILD_SILVER_FREE_TIME_ERROR end
else
-- 验道具或钻石
if not useDenar then check item silverBoxOpenItem×silverBoxOpenItemNum*count
else check denar shopPrice*count
end
return BuildingLogic:openSilver( rid, free, count, useDenar )
elseif type == Enum.BoxType.GOLD then
...同上,配置项换成 goldBox*
return BuildingLogic:openGold( rid, free, count, useDenar )
end
end
function BuildingLogic:openSilver( _rid, _free, _count, _useDenar )
-- 扣免费次数 / 扣道具 / 扣钻石
...
-- 调用通用包随机
local groupId = CFG.s_Config:Get("silverBoxItemPackage")
local rewardInfo = ItemLogic:getItemPackage( _rid, groupId, ..., _count, true )
-- 任务进度
TaskLogic:addTaskStatisticsSum( _rid, TaskType.TAVERN_BOX, TaskTavernBoxType.SILVER, _count )
return { rewardInfo = rewardInfo, count = _count, type = Enum.BoxType.SILVER }
end
function BuildingLogic:openGold( _rid, _free, _count, __useDenar )
...同上
local groupId = CFG.s_Config:Get("goldBoxItemPackage")
-- 黄金箱首抽保底:第一次开走 goldBoxFirstReward 包
local firstOpenGold = RoleLogic:getRole( _rid, Enum.Role.firstOpenGold )
if not firstOpenGold then
groupId = CFG.s_Config:Get("goldBoxFirstReward")
RoleLogic:setRole( _rid, Enum.Role.firstOpenGold, true )
end
local rewardInfo = ItemLogic:getItemPackage( _rid, groupId, ..., _count, true )
return { rewardInfo = rewardInfo, count = _count, type = Enum.BoxType.GOLD }
end
- 消耗 & 产出:
- 消耗:免费次数 OR 银箱钥匙/金箱钥匙道具 OR 钻石(
s_Item.shopPrice) - 产出:
RewardInfo(资源/道具/英雄实例/士兵的混合包),结构见Common.sproto:381-398:
- 消耗:免费次数 OR 银箱钥匙/金箱钥匙道具 OR 钻石(
.RewardInfo {
food/wood/stone/gold/denar 0-4 : integer
items 5 : *RewardItem # 道具数组(碎片落这里)
soldiers 6 : *SoldierInfo
groupId 7 : integer
actionForce 8 : integer
guildGifts 9 : *GuildGift
heros 10 : *Heros # 英雄数组(罕见情况直接给英雄)
...
}
.Heros { heroId 0:integer; num 1:integer; isNew 2:integer }
- 是否有概率 / 保底:有
- 概率:
silverBoxItemPackage/goldBoxItemPackage配置组,每个节点有type/typeData/number三元组,由ItemLogic:getItemPackage走通用包逻辑(ItemLogic.lua:531-)随机产出 - 保底:黄金箱首次必出固定礼包
goldBoxFirstReward(BuildingLogic.lua:1084-1088,记录在Enum.Role.firstOpenGold玩家标志位);银箱无保底
- 概率:
- 频率限制:
- 银箱:每日免费次数受酒馆等级限制(配置
s_BuildingTavern.silverBoxCnt),有冷却silverBoxCD;付费无限 - 金箱:免费次数固定 1,冷却
s_BuildingTavern.goldBoxCD;付费无限
- 银箱:每日免费次数受酒馆等级限制(配置
- 特殊行为:
- 服务端返回的奖励里
heros[].isNew标志位区分"新英雄(1)"和"已拥有英雄重复给(0)",客户端用于展示动画 - 当 ItemPackage 抽到 HERO 类型节点且玩家已拥有该英雄时,
HeroLogic:addHero自动把该英雄转化为对应的sHero.getItem碎片道具(HeroLogic.lua:39-52)—— 即"重复英雄自动转碎片" - 任务追踪:
Enum.TaskType.TAVERN_BOX(按银/金分类统计)
- 服务端返回的奖励里
2.3 ⟨途径 3⟩ 雕像兑换(碎片之间的转换)
- 名称:雕像兑换 / Exchange Hero Item
- 本质:不是"获得英雄",而是"道具A换成道具B"——把通用「英雄雕像」道具换成某英雄专属的招募碎片,用于绕过抽不到某英雄碎片的尴尬。
- RPC:
Hero_ExchangeHeroItem (605),参数{ heroId, itemNum } - 服务端逻辑(
Hero.lua:108-135):
function response.ExchangeHeroItem( msg )
local rid, heroId, itemNum = msg.rid, msg.heroId, msg.itemNum
local sHero = CFG.s_Hero:Get( heroId )
-- sHero.exchange = 通用雕像 itemId;0 = 该英雄不支持兑换
if Enum.Exchange.NO == sHero.exchange then return nil, ErrorCode.HERO_NOT_EXCHANGE end
-- 注:此处源代码逻辑略奇怪("heroInfo 不存在但 not empty"),实际有效约束是下面这一条:
-- 已拥有该英雄 + 技能已满,禁止再兑换更多碎片(防浪费)
if table.size(heroInfo.skills) >= 4 and HeroLogic:checkHeroSkillFull( rid, heroId ) then
return nil, ErrorCode.HERO_EXCHANGE_SKILL_MAX
end
-- 验 itemNum 个通用雕像存在
if not ItemLogic:checkItemEnough( rid, sHero.exchange, itemNum ) then
return nil, ErrorCode.HERO_EXCHANGE_ITEM_NOT_ENOUGH
end
-- 1:1 兑换:扣 itemNum 个通用雕像,加 itemNum 个该英雄招募碎片
ItemLogic:delItemById( rid, sHero.exchange, itemNum, nil, Enum.LogType.HERO_EXCHANGE_COST_ITEM )
ItemLogic:addItem( { rid = rid, itemId = sHero.getItem, itemNum = itemNum,
eventType = Enum.LogType.HERO_EXCHANGE_GAIN_ITEM } )
return { result = true }
end
- 消耗 & 产出:
sHero.exchange通用雕像 ×N →sHero.getItem该英雄碎片 ×N(1:1) - 是否有概率 / 保底:无,确定性 1:1
- 频率限制:玩家手动触发,无限制
- 使用场景:玩家长期开箱攒了一堆「通用英雄雕像」但缺某英雄的特定碎片时,定向兑换补差
2.4 ⟨途径 4⟩ 新手引导直给(一次性)
- 名称:完成新手"攻击野蛮人"引导奖励英雄
- 服务端逻辑(
Role.lua:1056-1062):
local guideHeroStage = CFG.s_Config:Get( "guideHeroStage" ) or 7
if not RoleLogic:checkGuideFinish( oldNoviceGuideStep, guideHeroStage )
and RoleLogic:checkGuideFinish( noviceGuideStep, guideHeroStage ) then
local guideHero = CFG.s_Config:Get( "guideHero" ) or 0
if guideHero > 0 then
HeroLogic:addHero( rid, guideHero )
end
end
- 频率:玩家全生命周期内 1 次
- 特殊表现:客户端
HeroCmd.cs:33-36会检测heroId == civilization.initialHero,触发首获英雄剧情引导
2.5 ⟨途径 5⟩ 文明初始英雄(角色创建时)
- 玩家选定文明(civilization)后,
CivilizationDefine.initialHero字段定义该文明的开局赠送英雄 - 客户端
HeroCmd.cs:30-37据此识别"首次得到的初始英雄",跳过普通的召唤动画走剧情
2.6 ⟨途径 6⟩ 通用奖励包内嵌(任务/活动/邮件/充值/商店)
- ROK 服务端不存在"任务直接给英雄"的专属代码(
TaskLogic不调HeroLogic:addHero、EmailLogic不调、GuildShopLogic不调、ActivityLogic仅"统计英雄等级数"不发放) - 但所有这些系统的奖励都走通用
ItemLogic:getItemPackage→ItemLogic:addItem,而通用包支持Enum.ItemPackageType.HERO节点(ItemLogic.lua:389-410)。所以理论上:- 任务奖励可以配置
HERO节点 → 通关后直给某英雄 - 邮件附件可以挂
HERO节点 → 客服补偿/活动派发可直给某英雄 - 充值礼包、商店物品同理
- 任务奖励可以配置
- 当配置成
HERO类型时,重复获得已拥有英雄会自动转化为sHero.getItem碎片(见 §2.2 末尾)
2.7 ⟨途径 7⟩ PMLogic / GM 命令(仅运营 / 测试)
PMLogic.lua:481-486PMLogic:addHero(rid, heroId)GM 后台命令PMLogic.lua:1480-1495PMLogic:allLevelUp一键解锁所有英雄- 玩家正式渠道不走
2.8 ⟨途径 8⟩ 公会复活 / 增援回调(功能恢复用,非常规获取)
PMLogic.lua:1197/1200/1271/1274:在公会援军 / 阵亡复活相关回调里,针对已死亡数据被清的成员重新补回 mainHero / deputyHero 实例- 实质是数据修复,不是玩家主动获取
3. 碎片机制详解
3.1 碎片 itemId 与英雄 ID 的关联
ROK 通过 HeroDefine.getItem(招募所需道具 itemId)字段在配置层绑定。没有"英雄 ID 自动派生碎片 ID"的规则,纯查表,多对一也允许(多个英雄可以共享同一种碎片,由配表决定)。
// HeroConfig.cs (客户端 C# 镜像) ── 字段名权威源
public class HeroDefine {
public int ID; // 英雄ID
public int initStar; // 初始星级
public int star; // 星级上限
public int rare; // 稀有度 1=白 2=绿 3=蓝 4=紫 5=橙
public int score; // 基础战力
public int getItem; // ★ 招募所需道具(碎片) itemId
public int getItemNum; // ★ 招募所需道具数量
public int exchange; // 通用雕像兑换 itemId,0=不可
public int getLimit; // 王国开服天数解锁门槛
public List<int> skill; // 技能 ID 列表
public List<int> talent; // 天赋 ID 列表
public int listDisplay; // 1=不显示在英雄列表
// (...其他模型/语音字段省略)
}
3.2 兑换公式
凑齐 sHero.getItemNum 个 sHero.getItem
→ 调用 Hero_SummonHero(heroId)
→ 扣道具
→ 永久拥有 1 个英雄(star = sHero.initStar, level = 1)
无升星批量兑换、无折扣、无随机加成。升星走单独的 Hero_HeroStarUp (606) 协议,消耗的是 s_HeroStarExp 配置的星级道具,跟招募碎片是两套独立资源。
3.3 关于 recruitLimit(Survivors proposal 里的字段)
ROK 不存在该字段。等价机制:
- 同一英雄一辈子只能
addHero进表 1 次(数据库以heroId为主键) - 第二次起
Hero_SummonHero返回HERO_ALREADY_EXIST错误码 - 如需让玩家持续获得"该英雄相关收益",ROK 的做法是 重复获得的碎片可以继续累积在背包:
- 多余碎片可用于升级技能(
HeroLogic.lua:204HERO_LEVEL_UP_COST_ITEM) - 多余碎片可参与雕像兑换流通
- 多余碎片可用于升级技能(
如果 Survivors 想保留 recruitLimit 概念,等价含义只能是「多少个碎片兑一次」(即 getItemNum),或者扩展为「同英雄可重复招募 N 次用于升星」(ROK 不是这么做的)。
3.4 多余碎片的处理
保留在背包,不自动转化。具体路径:
- 已拥有英雄 → 路径 2/6 又掉到该英雄(HERO 类型节点)→
HeroLogic:addHero检测到已存在 → 自动转sHero.getItem道具(HeroLogic.lua:43-52) - 已拥有英雄 → 路径 2/6 又掉到该英雄碎片(ITEM 类型节点)→ 正常进背包累加
- 玩家技能升级或雕像兑换时主动消耗多余碎片
4. 抽卡 / 概率机制
4.1 "传统抽卡"(卡池+权重+保底+十连)—— 不存在
排查依据:
HeroLogic.luagrepsummon|recruit|exchange|draw|gacha|lottery|pool全部 3 处命中均为字段名/计数器,无任何 Random 调用Hero_SummonHeroRPC 服务端实现(Hero.lua:18-58)从头到尾没有任何Random.*函数调用,完全是确定性查表 + 扣道具 + addHero- ROK 客户端
View_Mediator/没有Summon/Gacha/Lottery/Draw目录;唯一相关的是Captain/CaptainSummon*但那是"获得英雄后的展示动画",不是抽卡入口 - 没有
s_HeroPool/s_GachaPool/s_Lottery/s_DrawWeight之类的配置表(在Server/common/service/data/config/下全表扫过 Hero 相关只有 7 张表,全都是属性/技能/天赋/星级 表)
4.2 "酒馆开箱"(已确认存在)
如 §2.2 所述:
| 项 | 银箱 | 金箱 |
|---|---|---|
| 奖励池配置 | silverBoxItemPackage |
goldBoxItemPackage(首抽 goldBoxFirstReward) |
| 池构成 | ItemPackage 通用 group,节点类型包含 ITEM / CURRENCY / HERO / SOLDIER / SUB_ITEM_TYPE 等 | 同 |
| 权重 | 由 ItemPackage 节点的 number 字段及 group 内 weight 推断(实现在 ItemLogic:getItemPackage) |
同 |
| 保底 | 无 | 有 —— 玩家首次开金箱必走 goldBoxFirstReward 固定包,标志位 Enum.Role.firstOpenGold |
| 开法 | 免费次数 + 钥匙道具 + 钻石 | 同 |
| 每日免费次数 | s_BuildingTavern[酒馆等级].silverBoxCnt |
s_BuildingTavern[酒馆等级].goldBoxCnt(实际固定为 1) |
| CD | s_Config.silverBoxCD |
s_BuildingTavern[酒馆等级].goldBoxCD |
| 批量开 | 支持,count 字段传次数 | 同 |
| 客户端预览 | TavernRankDefine group=1 列表展示"可能开出的稀有奖励" |
group=2 同 |
这就是 ROK 最接近"抽卡"的机制。但产出端主要是英雄碎片道具(玩家攒齐再去 Hero_SummonHero),偶尔直接产出英雄实例。
4.3 抽卡相关错误码
ErrorCode.lua:
BUILD_SILVER_FREE_NOT_ENOHGH 银箱免费次数不足
BUILD_SILVER_FREE_TIME_ERROR 银箱免费 CD 未到
BUILD_GOLD_FREE_NOT_ENOHGH 金箱免费次数不足
ITEM_NOT_ENOUGH 钥匙不足
ROLE_DENAR_NOT_ENOUGH 钻石不足
5. 协议清单(与英雄获取相关)
| 协议号 | 名称 | 方向 | 用途 |
|---|---|---|---|
Build_Tavern 407 |
酒馆召唤 / 开宝箱 | C2S | 开白银/黄金宝箱,产出碎片或英雄的主路径 |
Hero_SummonHero 601 |
召唤统帅 | C2S | 用碎片兑换具体英雄,主获取路径 |
Hero_HeroInfo 30201 |
统帅信息推送 | S2C | 服务端在新英雄获得 / 数值变更后下发,客户端 HeroProxy.UpdateHeroInfo 处理 |
Hero_ExchangeHeroItem 605 |
雕像兑换 | C2S | 通用雕像 → 该英雄碎片(不是直接兑英雄) |
Hero_AddHeroExp 602 |
加经验 | C2S | 不是获取,但同包 |
Hero_HeroSkillLevelUp 603 |
技能升级 | C2S | 同上 |
Hero_HeroAwake 604 |
觉醒 | C2S | 同上 |
Hero_HeroStarUp 606 |
升星 | C2S | 同上 |
Hero_TalentUp/ChangeTalentIndex/ResetTalent/ModifyTalentName 607-610 |
天赋系列 | C2S | 同上 |
Hero_HeroWearEquip / TakeOffEquip 611-612 |
装备 | C2S | 同上 |
6. 错误码清单(与英雄获取相关)
HERO_OPEN_DAYS_NOT_ENOUGH 3000 召唤统帅,所在王国不满足该统帅解锁天数条件
HERO_SUMMON_ITEM_NOT_ENOUGH 3001 召唤统帅所需道具不足
HERO_ALREADY_EXIST 3002 统帅已存在
HERO_NOT_EXIST 3003 统帅不存在
HERO_NOT_EXCHANGE 3008 该统帅无法使用通用雕像兑换
HERO_EXCHANGE_SKILL_MAX 3009 统帅技能已满,无法兑换碎片
HERO_EXCHANGE_ITEM_NOT_ENOUGH 3010 通用雕像不足
# 酒馆宝箱
BUILD_SILVER_FREE_NOT_ENOHGH ... 银箱免费次数不足
BUILD_SILVER_FREE_TIME_ERROR ... 银箱免费 CD 未到
BUILD_GOLD_FREE_NOT_ENOHGH ... 金箱免费次数不足
ITEM_NOT_ENOUGH ... 钥匙道具不足
ROLE_DENAR_NOT_ENOUGH ... 钻石不足
7. 与 Survivors 实现的对照点
7.1 之前 openspec proposal 中的 cn.etetet.gacha 包
结论:名字与实际职责不匹配。ROK 里"抽卡"实际是「酒馆开箱」+「碎片兑换」两段式,不是传统单一抽卡。建议拆分调整:
| Survivors 包 | 对应 ROK 模块 | 职责 |
|---|---|---|
cn.etetet.hero |
HeroLogic.lua + Hero.lua proxy |
英雄实例存储、属性、技能、星级、天赋、装备等已规划 |
cn.etetet.summon(建议新名) |
Hero.lua: SummonHero/ExchangeHeroItem |
碎片兑换召唤 + 雕像兑换,对应 RPC Hero_SummonHero Hero_ExchangeHeroItem |
cn.etetet.tavern(建议新名,或合并到 cn.etetet.building) |
BuildingLogic.lua: openSilver/openGold + Build.lua: Tavern |
酒馆宝箱开启(碎片产出端),双宝箱+免费次数+CD+保底 |
cn.etetet.gacha(用户要求保留) |
ROK 没有对应 | Survivors 独立扩展:传统卡池抽卡(用于"验证产出") |
7.2 三种方案对比
方案 A:严格 1:1 移植
- 第一阶段先实现
cn.etetet.summon(碎片兑换召唤)—— 与 ROK 完全一致:每个英雄getItem+getItemNum,玩家凑齐手动召唤 - 第二阶段实现
cn.etetet.tavern(酒馆双宝箱)—— 产出端的"准抽卡" - 不做
cn.etetet.gacha(传统抽卡),因为 ROK 没有 - 优点:与原版美术 / 数值 / 设计完全对齐;
- 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突
方案 B:1:1 + 独立扩展抽卡(推荐)
- P0-P1 实现
cn.etetet.summon+cn.etetet.tavern:严格 1:1 ROK 的招募 + 酒馆开箱 - P3+ 新增
cn.etetet.gacha:作为 Survivors 独立扩展功能,与 ROK 解耦- 可设计成"特殊活动卡池":限时上线,权重+保底+十连
- 抽到的可以是碎片(继续走
cn.etetet.summon流程兑换)或直接英雄
- 优点:保留 ROK 设计的同时满足"测试抽卡逻辑"诉求;新功能不污染原 1:1 路径
- 缺点:工作量略大;需要清晰区分"碎片兑换召唤"与"抽卡"在 UI/UX 上的不同入口
方案 C:用抽卡替代碎片兑换(不推荐)
- 直接做
cn.etetet.gacha,跳过 ROK 的碎片兑换 - 优点:开发简单
- 缺点:偏离 ROK 设计哲学;ROK 的"碎片累积+主动召唤"是其养成长尾的核心环路,丢掉这一层会让英雄获取节奏完全不一样
7.3 命名重构建议
如果用户接受 cn.etetet.summon + cn.etetet.tavern + 独立的 cn.etetet.gacha 拆分:
原 openspec proposal 涉及调整:
- 把"cn.etetet.gacha 实现 ROK 的抽卡"修改为"cn.etetet.gacha 是 Survivors 独立扩展"
- 新增 cn.etetet.summon 包(实现 Hero_SummonHero + Hero_ExchangeHeroItem 对应 RPC)
- 在 cn.etetet.building(如已规划)下新增 Tavern 子模块,或独立成 cn.etetet.tavern
- HeroDefine 字段:
fragmentItemId → 改名为 getItem (与 ROK 一致)
fragmentNum → 改名为 getItemNum (与 ROK 一致)
recruitLimit → 删除 (ROK 不存在);或解释为"同英雄重复获得碎片继续累积"
openDaysLimit → 改名为 getLimit (与 ROK 一致)
exchangeItemId → 新增 exchange (通用雕像 itemId, 0=不可)
8. 给用户的建议
推荐方案:B (1:1 + 独立扩展抽卡)
理由:
- 严格 1:1 还原 ROK 的「碎片兑换 + 酒馆开箱」是 Survivors 这个工程立项的核心承诺(前序对话明确"严格 1:1");这两个机制是 ROK 中英雄获取的真实主流程
- 抽卡用户也明确要求保留("还需要抽卡的逻辑用来验证产出"),但应明确抽卡是 Survivors 在 ROK 基础上的扩展,不污染 1:1 路径
- 三个包职责清晰、可独立开发、便于后期裁剪:
cn.etetet.summon—— 必做(P0)cn.etetet.tavern—— 必做(P1,因为没酒馆就没碎片来源)cn.etetet.gacha—— 可延后(P3+,作为运营活动手段)
落地步骤建议
- 立刻: 更新 openspec proposal
port-rok-hero-bag-system,把 "cn.etetet.gacha 1:1" 改为 "cn.etetet.summon + cn.etetet.tavern 1:1",新增cn.etetet.gacha作为 P3 扩展项 - HeroDefine 配置: 字段命名跟 ROK 对齐(
getItem/getItemNum/getLimit/exchange),不要造fragmentItemId/recruitLimit等新字段 - P0: 实现
Hero_SummonHero等价 RPC(C2G_SummonHero),扣getItem×getItemNum→ 加英雄实例,错误码对齐HERO_*段 - P1: 实现 Tavern 双宝箱(C2G_TavernOpen),银 / 金 + 免费次数 + CD + 保底(金箱首抽)+ 通用 ItemPackage 引用
- P3+: 独立设计
cn.etetet.gacha卡池系统(权重表、保底、十连、抽卡券),可与酒馆解耦
风险提示
- ROK 的"碎片 → 召唤"链严重依赖通用 ItemPackage 系统(
getItemPackage)。如果 Survivors 还没规划"通用奖励包"基础设施,需要同步规划,否则酒馆 / 任务奖励 / 商店奖励都没法落地 - ROK 的
civilization.initialHero/guideHero等"开局直给英雄"路径与新手引导耦合,移植时需保留以保证新玩家体验 - 雕像兑换(
Hero_ExchangeHeroItem)这条"碎片之间互换"路径在 ROK 中是中后期玩法,可放 P2 实现
附录:关键源码位置索引(便于后续 1:1 比对)
| 主题 | 文件 | 行号 |
|---|---|---|
| 英雄数据新增 | HeroLogic.lua |
38-84 (addHero) |
| 召唤 RPC | service/proxy/Hero.lua |
17-58 (SummonHero) |
| 雕像兑换 RPC | service/proxy/Hero.lua |
108-135 (ExchangeHeroItem) |
| 酒馆 RPC | service/proxy/Build.lua |
296-361 (response.Tavern) |
| 银箱开启 | BuildingLogic.lua |
981-1031 (openSilver) |
| 金箱开启 | BuildingLogic.lua |
1049-1106 (openGold)(含首抽保底) |
| 通用奖励包随机产出 | ItemLogic.lua |
261-436 (getItemPackage 内 HERO/ITEM/SUB_ITEM_TYPE 分支) |
| 奖励包 → 加英雄 | ItemLogic.lua |
493-505 (heros 字段处理) |
| 协议 sproto | protocol/Protocol.sproto |
862-874 (Build_Tavern), 970-974 (Hero_SummonHero), 1013-1021 (Hero_ExchangeHeroItem) |
| 公共数据结构 | protocol/Common.sproto |
373-398 (.Heros .RewardInfo) |
| HeroDefine 字段 | Client/.../Config/HeroConfig.cs |
全文件 |
| 客户端发起召唤 | Client/.../Proxy/HeroProxy.cs |
567-572 (SummonHero) |
| 召唤按钮事件 | Client/.../View_Mediator/Common/Logic/UI_Item_CaptainData_SubView.cs |
61-65 (OnSummonClick) |
| 酒馆 UI | Client/.../View_Mediator/Tavern/TavernSummonMediator.cs |
全文件 |
| 错误码 | Server/common/errorcode/ErrorCode.lua |
149-176 (HERO_*) |