21 KiB
ROK 装备系统业务规则(1:1 迁移依据)
来源:
E:\Game\gmd\ROK服务端 Lua(HeroLogic.lua/BuildingLogic.lua/ItemLogic.lua/ proxyHero.lua/ proxyBuild.lua)+ 客户端 Mediator/View + sproto 协议 + Config DTO 用途:作为 Survivors 工程装备子系统实现的业务依据 调研日期:2026-05-27
0. 核心结论(先看这里)
- 装备就是 Item 的一种(带
heroId字段、不堆叠、每件独立itemIndex);不需要建独立 EquipEntity,靠 ItemUnit 的heroId>0区分 - Hero 持有装备的方式:Hero Entity 上有 8 个整型字段(
head/breastPlate/weapon/gloves/pants/accessories1/accessories2/shoes),值是itemIndex引用回背包 - 8 个槽位 ↔ 6 种 subType 的多对一映射:accessories1 与 accessories2 都接
ACCESSORIES子类型(共享一种装备池) - 锻造分两段 RPC:先
CheckMakeEquip跑 50% 概率给"专属"标记 → 再MakeEquipment真正消耗材料 + 金币产出装备;专属属性写在装备 Item 的exclusive字段 - 分解、合成(材料 mix/split)都是确定性配置查询,不带随机
- 错误码段:装备穿戴用 Hero 段(3024-3027),锻造用 Building 段(6029-6031)
- 建议:在 Survivors 不建独立
cn.etetet.equip包,作为cn.etetet.bag的子系统实现(与 Item 共享数据底座),锻造逻辑放cn.etetet.bag/Equip/Forge.cs子目录
1. 整体业务范围
| 功能模块 | 实现位置(ROK 原版) | 是否 P1 必需 |
|---|---|---|
| 装备穿戴(WearEquip) | proxy/Hero.lua:414 + HeroLogic.lua |
P2(与英雄养成同期) |
| 装备卸下(TakeOffEquip) | proxy/Hero.lua:486 |
P2 |
| 装备战力计算(套装加成) | HeroLogic.lua:792 getHeroEquipAttr |
P2 |
| 装备锻造(MakeEquipment) | proxy/Build.lua:742 + BuildingLogic |
P3 |
| 锻造预检(CheckMakeEquip) | proxy/Build.lua:713 |
P3 |
| 装备分解(DecompositionEquipment) | proxy/Build.lua:792 |
P3 |
| 材料合成(mix) | 配置 + ItemScript 模式 | P3+ |
| 材料分解(split) | 配置 + ItemScript 模式 | P3+ |
| 套装效果(2/4/6/8 件套) | HeroLogic.lua:832 + EquipComposeDefine |
P2 |
| 专属属性(exclusive) | RoleLogic.exclusive 角色标记 + 装备 Item exclusive 字段 |
P3+(可推迟) |
2. 数据模型
2.1 Hero Entity 上的装备字段
Hero 实体直接挂 8 个整型字段(值为 itemIndex,0 表示空槽):
| 字段名 | 槽位 | 接受的 ItemSubType |
|---|---|---|
head |
1 | HELMET |
breastPlate |
2 | BREASTPLATE |
weapon |
3 | ARMS |
gloves |
4 | GLOVES |
pants |
5 | PANTS |
accessories1 |
6 | ACCESSORIES |
accessories2 |
7 | ACCESSORIES |
shoes |
8 | SHOES |
注意两个细节:
- 客户端"装备索引"
equipIndex用 1-8(不是 0-7),代码里到处写equips[1]...equips[8]- accessories 有两槽且接同一种 subType(一个英雄能同时戴两件饰品,但要分别选 6 号槽还是 7 号槽)
2.2 装备 Item 字段(在 BagItem 之上扩展)
装备 = BagItem 但多了几个语义字段:
| 字段 | 类型 | 含义 |
|---|---|---|
itemIndex |
int | 背包唯一索引(与普通 Item 一致) |
itemId |
int | 装备配置 ID(指向 EquipDefine) |
count |
int | 永远是 1(装备不堆叠) |
heroId |
int | 当前穿戴该装备的英雄 ID,0 = 未穿 |
exclusive |
int / bool | 是否带"专属"加成(锻造时 50% 触发) |
设计推论:装备就是
count=1+heroId≥0的特殊 Item,不需要独立 Entity 树。
3. 配置表清单
3.1 EquipDefine(装备基础表,主键 itemID)
| 字段 | 类型 | 含义 |
|---|---|---|
itemID |
int (PK) | 装备道具 ID(也是 ItemDefine 的 itemId) |
group |
int | 装备组(同一组可能是不同品质或不同套装的同部位) |
useLevel |
int | 英雄达到此等级才能穿戴 |
makeMaterial |
List<int> | 锻造所需材料 itemId 列表 |
makeMaterialNum |
List<int> | 对应的数量(与 makeMaterial 等长) |
costGold |
int | 锻造消耗金币 |
decomposeMaterial |
List<int> | 分解返还材料 itemId 列表 |
decomposeMaterialNum |
List<int> | 对应数量 |
att |
List<int> | 装备属性 ID 列表(指向 EquipAttDefine.ID) |
attAddEx |
List<int> | 服务端属性数值(千分比整数) |
attAdd |
List<int> | 客户端展示用属性数值 |
compose |
int | 套装 ID(0 = 非套装),指向 EquipComposeDefine.ID |
order |
int | UI 排序 |
3.2 EquipAttDefine(装备属性映射表)
| 字段 | 类型 | 含义 |
|---|---|---|
ID |
int (PK) | 属性 ID(被 EquipDefine.att 引用) |
attNew |
enum attrType | 客户端使用的属性枚举 |
att |
string | 服务端属性 key 字符串(用于数值表内查名) |
l_nameID |
int | 语言包 ID |
icon |
string | 图标资源名 |
color |
string | 文字颜色 |
3.3 EquipComposeDefine(套装效果表,主键 ID = EquipDefine.compose)
四段套装加成(满 2 件 / 4 件 / 6 件 / 8 件分别叠加):
| 字段 | 类型 | 含义 |
|---|---|---|
ID |
int (PK) | 套装 ID |
l_nameID |
int | 套装名语言包 ID |
compose2 |
List<int> | 满 2 件触发的属性 ID 列表 |
compose2Add |
List<int> | 客户端展示数值 |
compose2AddEx |
List<int> | 服务端千分比数值 |
compose4 / compose4Add / compose4AddEx |
同上 | 满 4 件 |
compose6 / compose6Add / compose6AddEx |
同上 | 满 6 件 |
compose8 / compose8Add / compose8AddEx |
同上 | 满 8 件(全套) |
3.4 EquipMaterialDefine(材料表,主键 itemID)
| 字段 | 类型 | 含义 |
|---|---|---|
itemID |
int (PK) | 材料道具 ID |
order |
int | UI 排序 |
group |
int | 材料组(同组不同品质用于显示分类) |
rare |
int | 品质 |
add |
int | 各品质对应的数值参数 |
mixCostNum |
int | 合成上一级时所需本级数量(如 3 个低 → 1 个高) |
mix |
int | 合成后的材料 itemId(下一级) |
split |
int | 分解后的材料 itemId(下一级反向?或者材料碎片) |
splitCostCur |
int | 分解时消耗的货币 itemId |
splitCostCurNum |
int | 分解消耗货币数量 |
splitGetNum |
int | 分解一次产出 split 道具的数量 |
3.5 EquipMaterialGroupDefine(材料组表)
| 字段 | 类型 | 含义 |
|---|---|---|
ID |
int (PK) | 组 ID |
group |
int | 与 EquipMaterialDefine.group 关联 |
l_nameID |
int | 组名语言包 ID |
icon |
string | 组图标 |
4. 业务规则详细
4.1 装备穿戴 Hero_HeroWearEquip 611
请求:{ heroId: int, itemIndex: int, equipIndex: int }(equipIndex = 1..8)
服务端流程(proxy/Hero.lua:414):
1. 取 ItemLogic.getItem(rid, itemIndex) → itemInfo
- 不存在 → ERR ITEM_NOT_EXIST
2. 取 EquipDefine[itemInfo.itemId] → sEquip
3. 校验 ItemSubType 与 equipIndex 槽位匹配(按 §2.1 映射表)
- 不匹配 → ERR HERO_EQUIP_SUBTYPE_ERROR (3025)
4. 校验 hero.level >= sEquip.useLevel
- 不够 → ERR HERO_EQUIP_LV_NO_ENOUGH (3024)
5. 校验目标英雄 idle(不在出征中)
- HeroLogic.checkHeroIdle(rid, heroId) → false → ERR HERO_EQUIP_NOT_IN_CITY (3026)
6. 处理"装备已被其他英雄穿戴":
if itemInfo.heroId > 0 then
beforeHero = HeroLogic.getHero(rid, itemInfo.heroId)
attr = equips[equipIndex].attr # 该槽位对应的 hero 字段
if beforeHero[attr] > 0 then
beforeHero[attr] = 0 # 旧英雄槽位清空
HeroLogic.setHero + syncHero(推送给客户端)
end
end
7. 处理"目标槽位已有装备":
attr = equips[equipIndex].attr
if hero[attr] > 0 then
oldEquip = ItemLogic.getItem(rid, hero[attr])
oldEquip.heroId = 0 # 旧装备解绑
ItemLogic.setItem + syncItem
end
8. 建立新关系:
hero[attr] = itemIndex
itemInfo.heroId = heroId
HeroLogic.setHero + syncHero
(Item 端 syncItem 已在 §6/§7 中被触发)
关键设计点:
- 槽位映射在 Lua 里硬编码(不在配置表):
equips[1]={HELMET, head}...equips[8]={SHOES, shoes} - ROK 这里没用客户端先发卸下、再发穿戴的两步流程,服务端一个 RPC 完成"卸旧+穿新"
- 跨英雄抢装备时,原英雄的同槽位会自动腾出,但原英雄是否还有别的装备保持不变
4.2 装备卸下 Hero_TakeOffEquip 612
请求:{ heroId: int, equipIndex: int }
服务端流程(proxy/Hero.lua:486):
1. 校验英雄 idle
- false → ERR HERO_EQUIP_NOT_IN_CITY
2. attr = equips[equipIndex].attr
if hero[attr] > 0 then
itemInfo = ItemLogic.getItem(rid, hero[attr])
itemInfo.heroId = 0
ItemLogic.setItem + syncItem
hero[attr] = 0
HeroLogic.setHero + syncHero
end
3. response { result = true }
关键点:卸下后装备回到背包未绑定状态,不会消失。
4.3 装备战力计算 HeroLogic.getHeroEquipAttr
位置:HeroLogic.lua:792
输入:(rid, heroId)
输出:{ attrName1: value1, attrName2: value2, ... }(属性叠加表)
算法:
equips = [head, breastPlate, weapon, gloves, pants, accessories1, accessories2, shoes]
equipCompose = {} # 套装计数 { composeId: pieceCount }
# 第一步:累加每件装备的基础属性
for pos in equips:
if hero[pos] > 0:
item = ItemLogic.getItem(hero[pos])
sEquip = EquipDefine[item.itemId]
if sEquip.compose > 0:
equipCompose[sEquip.compose] = (equipCompose[sEquip.compose] or 0) + 1
for i = 1..len(sEquip.att):
attrName = EquipAttDefine[sEquip.att[i]].att
num = sEquip.attAddEx[i] # 千分比
# 如果天赋有"装备属性提升"加成
if heroHas TalentBonus(equipTalentPromote):
num = ceil(equipTalentPromote * num * 2) / 2
result[attrName] += num
# 第二步:套装加成(compose2 / compose4 / compose6 / compose8)
for composeId, count in equipCompose:
sCompose = EquipComposeDefine[composeId]
if count >= 2: 累加 sCompose.compose2 / compose2AddEx
if count >= 4: 累加 sCompose.compose4 / compose4AddEx
if count >= 6: 累加 sCompose.compose6 / compose6AddEx
if count >= 8: 累加 sCompose.compose8 / compose8AddEx
return result
注意:所有数值都是千分比整数,在客户端展示时除以 10 显示为百分比。
4.4 锻造预检 Build_CheckMakeEquip 417
请求:{ itemId: int }(要锻造的装备 ID)
响应:{ exclusive: bool }
作用:客户端按下"锻造"按钮时先调这个 RPC,决定本次是否能锻造出"专属"装备。
服务端流程(proxy/Build.lua:713):
1. sEquip = EquipDefine[itemId]
2. 校验金币 >= sEquip.costGold
- 否 → ERR ROLE_GOLD_NOT_ENOUGH
3. 校验材料:调用 BuildingLogic.cancleMaterial(rid, itemId) 做存量检查(不真扣)
- 否 → ERR BUILDING_EQUIP_ITEM_NO_ENOUGH (6030)
4. 随机:num = Random(1, 100)
if num > 50:
exclusive = true
RoleLogic.setRole(rid, { exclusive = true }) # 把 50% 概率结果存到玩家身上
5. return { exclusive }
关键点:专属概率 50% 写在硬编码里(num > 50),不在配置表。
4.5 装备锻造 Build_MakeEquipment 418
请求:{ itemId: int, exclusive: int }(exclusive 由 CheckMakeEquip 返回,客户端透传回来)
服务端流程(proxy/Build.lua:742):
1. 二次校验 exclusive: 客户端说要带专属 → 但 RoleLogic.exclusive 必须为 true(否则作弊)
- 不一致 → ERR BUILDING_EQUIP_EXCLUSIVE_ERROR (6031)
2. 二次校验金币 / 材料(同 4.4 第 2-3 步)
3. flag, itemInfo = BuildingLogic.cancleMaterial(rid, itemId)
itemInfo 是 dict { itemId: needNum }
4. 逐项 ItemLogic.checkItemEnough → 任一不足 → ERR
5. 逐项 ItemLogic.delItemById(LogType=MAKE_EQUIP_COST_ITEM)
6. RoleLogic.addGold(-price, LogType=MAKE_EQUIP_COST_CURRENCY)
7. ItemLogic.addItem({ itemId, itemNum=1, exclusive=exclusive, LogType=MAKE_EQUIP_GAIN_ITEM })
8. 任务统计:TaskLogic.addTaskStatisticsSum(EQUIP_QUALITY, ItemDefine.quality, +1)
9. response { itemIndex, itemInfo }(实际响应字段以 sproto 为准)
关键设计点:
- 没有锻造概率,材料够 + 金币够 = 必定产出(专属属性是另一回事)
- 专属 50% 概率在预检 RPC 里就决定了,玩家"看脸"决定要不要消耗材料锻造
- 装备本体是
count=1的 BagItem,带exclusive字段
4.6 装备分解 Build_DecompositionEquipment 419
请求:{ itemIndex: int }
服务端流程(proxy/Build.lua:792):
1. itemInfo = ItemLogic.getItem(rid, itemIndex)
- 不存在 → ERR BUILDING_EQUIP_NO_EXIST (6029)
2. 如已穿戴:校验英雄 idle
- 否 → ERR HERO_EQUIP_NOT_IN_CITY
3. sEquip = EquipDefine[itemInfo.itemId]
4. ItemLogic.delItem(rid, itemIndex, 1, LogType=DECOMPOSITION_EQUIP_COST_ITEM)
5. 按 sEquip.decomposeMaterial[] / decomposeMaterialNum[] 数组返还材料:
for i = 1..N:
ItemLogic.addItem({ itemId=sEquip.decomposeMaterial[i], itemNum=sEquip.decomposeMaterialNum[i], LogType=DECOMPOSITION_EQUIP_GAIN_ITEM })
6. 如已穿戴,同步对应英雄槽位清零(伪代码省略,与穿戴的反向)
关键点:
- 已穿戴装备允许直接分解(前提英雄在城内),不要求先卸下
- 返还固定(按配置表),无随机
4.7 材料合成 / 分解 mix / split
没有专属 RPC——这两个操作通过 道具使用 Use_Item 协议 实现:
- 客户端 UI(
UI_Win_MaterialView)让玩家选材料 + 数量 - 服务端按
EquipMaterialDefine.mix/mixCostNum配置查表执行 - 合成:消耗 N 个低级材料 → 产出 1 个高级材料
- 分解:消耗 1 个高级材料 +
splitCostCur货币 → 产出splitGetNum个低级
关键设计点:复用现有 UseItem 链路,无需新协议。
5. 客户端 UI 流程
5.1 装备主面板 UI_Win_EquipView(武将装备视图)
- 显示英雄的 8 个装备槽(每槽点击 → 选择装备列表)
- 显示当前套装件数 + 已激活的套装效果
- 显示装备战力小计
- "卸下"按钮 / "替换"流程
5.2 装备快速操作 UI_Win_EquipQuickView
- 快速一键穿戴该英雄部位的"最强可用装备"
5.3 锻造面板(在 Build 体系下,不在 Equip View_Mediator)
- 选装备 ID(按 group 分类)→ 显示所需材料 + 金币
- 按下"锻造" → 调 CheckMakeEquip → 若返回 exclusive=true,UI 突出显示
- 玩家确认 → 调 MakeEquipment
- 成功 →
UI_IF_EquipSuccessView展示飘屏
5.4 材料合成 UI_Win_MaterialView
- 选材料组 → 按 rare 排序展示 → 选合成 / 分解 → 走 UseItem
5.5 天赋装备选择 UI_IF_EquipTalentChooseView
- 在英雄升级到关键节点时弹出,让玩家选装备方向(特殊功能,可推迟)
6. 协议清单
| 协议号 | 名称 | 方向 | 用途 |
|---|---|---|---|
| 417 | Build_CheckMakeEquip |
C2G + Resp | 锻造前 50% 概率预检(决定本次能否带专属) |
| 418 | Build_MakeEquipment |
C2G + Resp | 真正锻造 |
| 419 | Build_DecompositionEquipment |
C2G + Resp | 装备分解 |
| 611 | Hero_HeroWearEquip |
C2G + Resp | 英雄穿戴装备(含自动卸旧) |
| 612 | Hero_TakeOffEquip |
C2G + Resp | 英雄卸下指定槽位 |
| (复用) | Use_Item |
C2G + Resp | 材料合成 / 分解(无专属协议) |
| (推送) | Hero_HeroInfo |
G2C | 英雄数据变更(含 8 槽位 itemIndex) |
| (推送) | Item_ItemInfo |
G2C | 装备 Item 的 heroId 变更 |
7. 错误码清单
| 错误码 | 名称 | 含义 |
|---|---|---|
| 3024 | HERO_EQUIP_LV_NO_ENOUGH |
英雄等级不足 useLevel |
| 3025 | HERO_EQUIP_SUBTYPE_ERROR |
装备类型与槽位不匹配 |
| 3026 | HERO_EQUIP_NOT_IN_CITY |
英雄不在城内(出征中无法换装) |
| 3027 | HERO_EQUIP_ALREADY_WEAR |
该装备已被穿戴(预留,实际流程会自动处理) |
| 6029 | BUILDING_EQUIP_NO_EXIST |
装备 Item 不存在(分解时) |
| 6030 | BUILDING_EQUIP_ITEM_NO_ENOUGH |
锻造材料 / 道具不足 |
| 6031 | BUILDING_EQUIP_EXCLUSIVE_ERROR |
锻造专属校验失败(CheckMakeEquip 与 MakeEquipment 不一致) |
| (复用) | ROLE_GOLD_NOT_ENOUGH |
金币不足 |
| (复用) | ITEM_NOT_EXIST |
道具不存在 |
8. 与 Survivors 实现的对照点
8.1 包归属
建议不建独立 cn.etetet.equip 包:
| 理由 | 说明 |
|---|---|
| 装备就是 Item 的子集 | count=1 + heroId≥0,没必要建独立 Entity 树 |
| 穿戴逻辑天然依赖 Hero + Bag 两个包 | 放独立包要双向引用,反而割裂 |
| ROK 原版的"穿戴"在 HeroLogic,"锻造"在 BuildingLogic | 没有独立 EquipLogic.lua |
实现安排:
cn.etetet.bag/ # 装备数据底座
├── Scripts/Model/Server/
│ ├── BagComponent.cs # ItemUnit 上加 heroId/exclusive 字段
│ └── ItemUnit.cs # heroId>0 = 装备已穿戴
└── Scripts/Hotfix/Server/
├── BagComponentSystem.cs # AddItem 时识别装备类型
└── Forge/ # 锻造子目录
├── ForgeHelper.cs # CheckMakeEquip + MakeEquipment + Decompose
└── MaterialMixHelper.cs
cn.etetet.hero/ # 穿戴关系挂在 Hero 上
├── Scripts/Model/Server/
│ └── HeroEquipSlots.cs # Hero 上加 head/.../shoes 8 字段
└── Scripts/Hotfix/Server/
└── HeroWearEquipHandler.cs # 转发到 BagComponentSystem + HeroComponentSystem
8.2 8 槽位与 Survivors 协议
message HeroEquipSlots {
int64 head = 1;
int64 breast_plate = 2;
int64 weapon = 3;
int64 gloves = 4;
int64 pants = 5;
int64 accessories1 = 6;
int64 accessories2 = 7;
int64 shoes = 8;
}
enum EquipSubType {
EQUIP_HELMET = 0;
EQUIP_BREASTPLATE = 1;
EQUIP_ARMS = 2;
EQUIP_GLOVES = 3;
EQUIP_PANTS = 4;
EQUIP_ACCESSORIES = 5; // accessories1 + accessories2 共用
EQUIP_SHOES = 6;
}
// 协议号建议 3100-3199 段(与本项目 Bag 段相邻)
message C2G_HeroWearEquip { int64 hero_id = 1; int64 item_index = 2; int32 equip_index = 3; }
message R2C_HeroWearEquip { int32 error = 1; }
message C2G_HeroTakeOffEquip { int64 hero_id = 1; int32 equip_index = 1; }
message C2G_CheckMakeEquip { int32 item_id = 1; }
message R2C_CheckMakeEquip { int32 error = 1; bool exclusive = 2; }
message C2G_MakeEquipment { int32 item_id = 1; bool exclusive = 2; }
message C2G_Decompose { int64 item_index = 1; }
8.3 Luban 配置(替代 ROK 的 SqlCipher)
5 张表照搬:
EquipConfig.xlsx↔ EquipDefine(field 列表见 §3.1)EquipAttConfig.xlsx↔ EquipAttDefineEquipComposeConfig.xlsx↔ EquipComposeDefineEquipMaterialConfig.xlsx↔ EquipMaterialDefineEquipMaterialGroupConfig.xlsx↔ EquipMaterialGroupDefine
注意 List<int> 字段在 Luban 用 list,int 类型表达;attAddEx 千分比整数保留 ROK 写法(不改成浮点)。
8.4 数据迁移
走 tools/rok-migrate/export.py 扩展支持 5 张 Equip 表,从 Configs.data 导出 JSON → 转 Luban xlsx。禁止手输。
9. 实现优先级
| 阶段 | 任务 | 备注 |
|---|---|---|
| P2 | Hero 8 槽位字段 + 穿戴 / 卸下 / 战力计算(含套装) | 与英雄养成同期,撑起 Hero 详情面 |
| P3 | 锻造(含 exclusive 双段 RPC)+ 分解 | 撑起背包→装备生产闭环 |
| P3+ | 材料合成 / 分解(复用 UseItem) | 优先级低,先把装备能玩通再说 |
| P4+ | "天赋装备选择" UI_IF_EquipTalentChoose |
特殊节点弹窗,非主路径 |
10. 与原 openspec 的差异提醒
之前 openspec/changes/port-rok-hero-bag-system/specs/equipment-system/spec.md 与 tasks.md 里的几个表述需要根据本调研修正:
| openspec 原写法 | ROK 真实情况 | 应改为 |
|---|---|---|
| "EquipItemType 4 个" | 实际 6 个 subType + 8 个槽位 | 改为 "8 槽位 / 6 subType(accessories 2 槽共享)" |
| "锻造概率(必中 / 随机)" | 锻造必中,专属 50% 概率(在预检 RPC 里) | 区分"装备产出"必中 vs "专属属性"50% |
| "MakeEquipment 单 RPC" | 实际是 CheckMakeEquip + MakeEquipment 两个 RPC | tasks 3.4 加 C2G_CheckMakeEquip |
| "EquipDefine 含 List" | 实际是 att[] + attAddEx[] 两个 List 平行 |
bean schema 按平行 List 设计 |
| "套装效果 = 4 件套" | 实际是 2/4/6/8 件逐级叠加 | bean schema 拆 4 段 |