# ROK 装备系统业务规则(1:1 迁移依据) > 来源:`E:\Game\gmd\ROK` 服务端 Lua(`HeroLogic.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 个整型字段(值为 `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` | > **注意两个细节**: > 1. 客户端"装备索引" `equipIndex` 用 **1-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 | 当前穿戴该装备的英雄 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\ | 锻造所需材料 itemId 列表 | | `makeMaterialNum` | List\ | 对应的数量(与 makeMaterial 等长) | | `costGold` | int | 锻造消耗金币 | | `decomposeMaterial` | List\ | 分解返还材料 itemId 列表 | | `decomposeMaterialNum` | List\ | 对应数量 | | `att` | List\ | 装备属性 ID 列表(指向 EquipAttDefine.ID) | | `attAddEx` | List\ | 服务端属性数值(**千分比**整数) | | `attAdd` | List\ | 客户端展示用属性数值 | | `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\ | 满 2 件触发的属性 ID 列表 | | `compose2Add` | List\ | 客户端展示数值 | | `compose2AddEx` | List\ | 服务端千分比数值 | | `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 协议 ```protobuf 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` ↔ EquipAttDefine - `EquipComposeConfig.xlsx` ↔ EquipComposeDefine - `EquipMaterialConfig.xlsx` ↔ EquipMaterialDefine - `EquipMaterialGroupConfig.xlsx` ↔ EquipMaterialGroupDefine 注意 `List` 字段在 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 段 |