576 lines
33 KiB
Markdown
576 lines
33 KiB
Markdown
# 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<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. 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突
|
||
|
||
**方案 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_*) |
|