XtGameKit/Doc/ROK-HeroAcquire-Spec.md

33 KiB
Raw Permalink Blame History

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-700Tavern 在 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|recruitLimit0 命中。这两个字段名是 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
  • RPCHero_SummonHero (601),参数 { heroId: integer }
    • 客户端发起:HeroProxy.cs:567-572 SummonHero(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 活动事件 +1HeroLogic.lua:42
    • 触发限时礼包:LimitTimeType.NEW_HEROHeroLogic.lua:83
    • 自动更换驻防英雄 BuildingLogic:changeDefendHero
    • 重算战力 RoleLogic:cacleSyncHistoryPower

2.2 ⟨途径 2⟩ 酒馆开箱产出(最主要的"碎片来源"

  • 名称:酒馆召唤 / Tavern Summon / 开宝箱
  • 玩家入口:城市建筑「酒馆 Tavern」点击进入出现两种宝箱
    • 白银宝箱 (woodbox):每日免费次数(按酒馆等级 s_BuildingTavern.silverBoxCnt+ CDsilverBoxCD),免费用完后用 silverBoxOpenItem 道具或钻石购买
    • 黄金宝箱 (goldbox):免费次数 1 次 + CDs_BuildingTavern.goldBoxCD),其余消耗 goldBoxOpenItem 或钻石
  • RPCBuild_Tavern (407),参数 { type, free, count, useDenar }
    • type1=白银 / 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
.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-)随机产出
    • 保底:黄金箱首次必出固定礼包 goldBoxFirstRewardBuildingLogic.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"——把通用「英雄雕像」道具换成某英雄专属的招募碎片,用于绕过抽不到某英雄碎片的尴尬。
  • RPCHero_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 = 通用雕像 itemId0 = 该英雄不支持兑换
    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 该英雄碎片 ×N1: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⟩ 文明初始英雄(角色创建时)

  • 玩家选定文明civilizationCivilizationDefine.initialHero 字段定义该文明的开局赠送英雄
  • 客户端 HeroCmd.cs:30-37 据此识别"首次得到的初始英雄",跳过普通的召唤动画走剧情

2.6 ⟨途径 6⟩ 通用奖励包内嵌(任务/活动/邮件/充值/商店)

  • ROK 服务端不存在"任务直接给英雄"的专属代码(TaskLogic 不调 HeroLogic:addHeroEmailLogic 不调、GuildShopLogic 不调、ActivityLogic 仅"统计英雄等级数"不发放)
  • 所有这些系统的奖励都走通用 ItemLogic:getItemPackageItemLogic: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"的规则,纯查表,多对一也允许(多个英雄可以共享同一种碎片,由配表决定)。

// 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;            // 通用雕像兑换 itemId0=不可
    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 关于 recruitLimitSurvivors 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. 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突

方案 B1: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 拆分:

原 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 等价 RPCC2G_SummonHerogetItem × 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_*)