XtGameKit/Doc/ROK-Equip-Spec.md

21 KiB
Raw Blame History

ROK 装备系统业务规则1:1 迁移依据)

来源:E:\Game\gmd\ROK 服务端 LuaHeroLogic.lua / BuildingLogic.lua / ItemLogic.lua / proxy Hero.lua / proxy Build.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 个整型字段(值为 itemIndex0 表示空槽):

字段名 槽位 接受的 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

注意两个细节

  1. 客户端"装备索引" equipIndex1-8(不是 0-7代码里到处写 equips[1]...equips[8]
  2. accessories 有两槽且接同一种 subType一个英雄能同时戴两件饰品但要分别选 6 号槽还是 7 号槽)

2.2 装备 Item 字段(在 BagItem 之上扩展)

装备 = BagItem 但多了几个语义字段:

字段 类型 含义
itemIndex int 背包唯一索引(与普通 Item 一致)
itemId int 装备配置 ID指向 EquipDefine
count int 永远是 1装备不堆叠
heroId int 当前穿戴该装备的英雄 ID0 = 未穿
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 套装 ID0 = 非套装),指向 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 的 BagItemexclusive 字段

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 协议 实现:

  • 客户端 UIUI_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=trueUI 突出显示
  • 玩家确认 → 调 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 ↔ EquipDefinefield 列表见 §3.1
  • EquipAttConfig.xlsx ↔ EquipAttDefine
  • EquipComposeConfig.xlsx ↔ EquipComposeDefine
  • EquipMaterialConfig.xlsx ↔ EquipMaterialDefine
  • EquipMaterialGroupConfig.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.mdtasks.md 里的几个表述需要根据本调研修正:

openspec 原写法 ROK 真实情况 应改为
"EquipItemType 4 个" 实际 6 个 subType + 8 个槽位 改为 "8 槽位 / 6 subTypeaccessories 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 段