# 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-572` `SummonHero(int id)` - **服务端核心逻辑**(`server/game_server/logic/service/proxy/Hero.lua:18-58`): ```lua 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` 或钻石 - **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`): ```lua -- 简化伪码 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`: ```sproto .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`): ```lua 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`): ```lua 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-486` `PMLogic:addHero(rid, heroId)` GM 后台命令 - `PMLogic.lua:1480-1495` `PMLogic:allLevelUp` 一键解锁所有英雄 - 玩家正式渠道不走 ### 2.8 ⟨途径 8⟩ 公会复活 / 增援回调(功能恢复用,非常规获取) - `PMLogic.lua:1197/1200/1271/1274`:在公会援军 / 阵亡复活相关回调里,针对**已死亡数据被清的成员**重新补回 mainHero / deputyHero 实例 - 实质是数据修复,不是玩家主动获取 --- ## 3. 碎片机制详解 ### 3.1 碎片 itemId 与英雄 ID 的关联 ROK 通过 `HeroDefine.getItem`(招募所需道具 itemId)字段在配置层绑定。**没有**"英雄 ID 自动派生碎片 ID"的规则,纯查表,多对一也允许(多个英雄可以共享同一种碎片,由配表决定)。 ```csharp // 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 skill; // 技能 ID 列表 public List 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:204` `HERO_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 "传统抽卡"(卡池+权重+保底+十连)—— **不存在** 排查依据: 1. `HeroLogic.lua` grep `summon|recruit|exchange|draw|gacha|lottery|pool` 全部 3 处命中均为字段名/计数器,无任何 Random 调用 2. `Hero_SummonHero` RPC 服务端实现(`Hero.lua:18-58`)从头到尾**没有任何 `Random.*` 函数调用**,完全是确定性查表 + 扣道具 + addHero 3. ROK 客户端 `View_Mediator/` 没有 `Summon/Gacha/Lottery/Draw` 目录;唯一相关的是 `Captain/CaptainSummon*` 但那是"获得英雄后的展示动画",不是抽卡入口 4. 没有 `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 移植** 1. 第一阶段先实现 `cn.etetet.summon`(碎片兑换召唤)—— 与 ROK 完全一致:每个英雄 `getItem`+`getItemNum`,玩家凑齐手动召唤 2. 第二阶段实现 `cn.etetet.tavern`(酒馆双宝箱)—— 产出端的"准抽卡" 3. **不做** `cn.etetet.gacha`(传统抽卡),因为 ROK 没有 4. 优点:与原版美术 / 数值 / 设计完全对齐; 5. 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突 **方案 B:1:1 + 独立扩展抽卡**(**推荐**) 1. P0-P1 实现 `cn.etetet.summon` + `cn.etetet.tavern`:严格 1:1 ROK 的招募 + 酒馆开箱 2. P3+ 新增 `cn.etetet.gacha`:作为 Survivors 独立扩展功能,与 ROK 解耦 - 可设计成"特殊活动卡池":限时上线,权重+保底+十连 - 抽到的可以是碎片(继续走 `cn.etetet.summon` 流程兑换)或直接英雄 3. 优点:保留 ROK 设计的同时满足"测试抽卡逻辑"诉求;新功能不污染原 1:1 路径 4. 缺点:工作量略大;需要清晰区分"碎片兑换召唤"与"抽卡"在 UI/UX 上的不同入口 **方案 C:用抽卡替代碎片兑换**(不推荐) 1. 直接做 `cn.etetet.gacha`,跳过 ROK 的碎片兑换 2. 优点:开发简单 3. 缺点:偏离 ROK 设计哲学;ROK 的"碎片累积+主动召唤"是其养成长尾的核心环路,丢掉这一层会让英雄获取节奏完全不一样 ### 7.3 命名重构建议 如果用户接受 `cn.etetet.summon` + `cn.etetet.tavern` + 独立的 `cn.etetet.gacha` 拆分: ```text 原 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:1 还原 ROK 的「碎片兑换 + 酒馆开箱」是 Survivors 这个工程立项的核心承诺(前序对话明确"严格 1:1");这两个机制是 ROK 中英雄获取的真实主流程 2. 抽卡用户也明确要求保留("还需要抽卡的逻辑用来验证产出"),但应明确**抽卡是 Survivors 在 ROK 基础上的扩展**,不污染 1:1 路径 3. 三个包职责清晰、可独立开发、便于后期裁剪: - `cn.etetet.summon` —— 必做(P0) - `cn.etetet.tavern` —— 必做(P1,因为没酒馆就没碎片来源) - `cn.etetet.gacha` —— 可延后(P3+,作为运营活动手段) ### 落地步骤建议 1. **立刻:** 更新 openspec proposal `port-rok-hero-bag-system`,把 "cn.etetet.gacha 1:1" 改为 "cn.etetet.summon + cn.etetet.tavern 1:1",新增 `cn.etetet.gacha` 作为 P3 扩展项 2. **HeroDefine 配置:** 字段命名跟 ROK 对齐(`getItem` / `getItemNum` / `getLimit` / `exchange`),不要造 `fragmentItemId` / `recruitLimit` 等新字段 3. **P0:** 实现 `Hero_SummonHero` 等价 RPC(C2G_SummonHero),扣 `getItem` × `getItemNum` → 加英雄实例,错误码对齐 `HERO_*` 段 4. **P1:** 实现 Tavern 双宝箱(C2G_TavernOpen),银 / 金 + 免费次数 + CD + 保底(金箱首抽)+ 通用 ItemPackage 引用 5. **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_*) |