XtGameKit/Doc/ROK-HeroAcquire-Spec.md

576 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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|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 = 通用雕像 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`
```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; // 通用雕像兑换 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 关于 `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. 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突
**方案 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` 拆分:
```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` 等价 RPCC2G_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_*) |