# ROK 背包/道具系统业务规则(1:1 迁移依据) > 来源:`E:\Game\gmd\ROK` 服务端 Lua(`ItemLogic.lua` / `Item.lua` proxy)+ 客户端 C#(`BagProxy.cs` / `BagMediator.cs` / `PlayerCmd.cs` / `ItemConfig.cs` / `ItemEnum.lua` / `Common.sproto` / `ErrorCode.cs`) > 用途:作为 Survivors 工程 `cn.etetet.bag` 包实现的业务依据,**不直接复制源代码**,只摊开业务规则与数据结构 > 调研时间:2026-05-27 > 关键约定:表名加 `s_` 前缀(如 `s_Item`)的为服务端 ConfigEntity;客户端对应 `ItemDefine`、`ItemPackageDefine` 等 --- ## 1. 整体业务范围 | # | 子模块 | 一句话定位 | |---|---|---| | 1 | 通用道具增/删 | `ItemLogic:addItem / delItem / delItemById / checkItemEnough`,所有业务发奖/扣道具的唯一入口 | | 2 | 道具分类 | 服务端 `Enum.ItemType`(6 类,HEAD 不入背包);客户端 `BagItemType`(5 个 Tab) | | 3 | 堆叠规则 | 非装备同 `itemId` 合并到同一 `itemIndex`;装备每件独立 `itemIndex`、`overlay=1` | | 4 | 道具使用 | `Item.lua:response.ItemUse`,按 `s_Item.itemFunction` 分支 if/elseif 处理(**无独立脚本表**) | | 5 | 资源类道具兑换 | `Item.lua:response.ItemChangeResource`,把"资源类道具"换成等额玩家货币(金币/木料/粮食/VIP/行动力) | | 6 | 礼包/选包奖励 | `ItemLogic:getGroupPackage / giveReward / getItemPackage` + `s_ItemPackage` + `s_ItemRewardChoice` | | 7 | 新道具/红点标记 | **服务端不存**,全部用 `PlayerPrefs` 在客户端持久化(按 `rid + itemIndex`) | | 8 | 头像框解锁 | `Enum.ItemType.HEAD` 类型道具**不进背包**,直接走 `RoleLogic:unlockRoleHead` | | 9 | 资源货币 | 粮/木/石/金/宝石/行动力/VIP/远征币等**作为 Role 字段**存储,不进 Item 表(详见 §5.7) | | 10 | 同步推送 | 唯一同步协议 `Item_ItemInfo`(30101)增量推 | | 11 | 创角初始道具 | `s_Config.initialItemType + initialItemNum` 硬编码 8 个道具 | | 12 | 资源阈值告警 | `s_GameWarning.num` 单道具数量超阈值时 `Common.sendResourceAlarm` 给运营 | | 13 | 日志审计 | 每次增删都 `LogLogic:itemChange(rid, iggid, itemId, changeNum, oldNum, newNum, logType, logType2)` | > ⚠️ ROK 工程中**没有 `ItemScript.lua` 实质实现**——同名文件只剩 3 个空函数。所有"道具使用脚本"逻辑都在 `Item.lua` proxy 的 `ItemUse` 函数里用 `if/elseif (sitem.itemFunction)` 巨型分支硬编码。 --- ## 2. 道具分类 BagItemType ### 2.1 服务端 `Enum.ItemType`(`ItemEnum.lua`) | 名称 | 值 | 含义 | 是否入背包 | 备注 | |---|---|---|---|---| | `RESOURCE` | 1 | 资源(金/木/石/粮/宝石/VIP/行动力类道具袋) | ✅ | 通过 `Item_ItemChangeResource` 兑换玩家货币字段 | | `SPEED` | 2 | 加速(建筑/训练/研究/治疗/通用 5 类) | ✅ | 在各功能界面用,背包 Tab2 | | `GAIN` | 3 | 增益(采集加成/护盾/反侦察/部队扩编等) | ✅ | 大多对应 City Buff,Tab3 | | `EQUIP` | 4 | 装备(武器/头盔/胸甲/手套/裤/鞋/饰品 + 装备材料 + 图纸) | ✅ | 每件独立 itemIndex | | `OTHER` | 5 | 其他(改名卡、迁城卡、经验书、英雄雕像、钥匙等) | ✅ | 杂项 Tab | | `HEAD` | 6 | 头像框 | ❌ | 进 `addItem` 后**直接走 `RoleLogic:unlockRoleHead`** 返回空 syncInfo | **itemId 编码约定**:客户端 `BagProxy.GetItemTypeById(itemId) = (int)itemId / 100000000`,即 itemId 最高位决定 type。 - 1xxxxxxxx → RESOURCE(如 `101010001`) - 2xxxxxxxx → SPEED (如 `201010010`) - 3xxxxxxxx → GAIN - 4xxxxxxxx → EQUIP (如 `401010001` 材料、`401020001` 装备) - 5xxxxxxxx → OTHER (如 `502030004` 改名卡、`502070001` 经验书) ### 2.2 客户端 `BagItemType`(`BagProxy.cs`) | 名称 | 值 | UI Tab | 与服务端关系 | |---|---|---|---| | `Resource` | 1 | 资源 | == ItemType.RESOURCE | | `Speedup` | 2 | 加速 | == ItemType.SPEED | | `Boost` | 3 | 增益 | == ItemType.GAIN | | `Equipment` | 4 | 装备 | == ItemType.EQUIP | | `Other` | 5 | 其他 | == ItemType.OTHER | | `Icon` | 6 | 不显示 | == ItemType.HEAD(实际客户端 BagPanel 只 5 个 Tab) | > `BagMediator` 实测**只显示 5 个 Tab**(1-5),HEAD 类型在 `for i = 1; i < 6` 循环里被略过。 ### 2.3 `Enum.ItemSubType`(46 个细分子类型,关键值) `ItemEnum.lua` 中所有 subType 都是 5 位数: | 段位 | 含义 | 示例 | |---|---|---| | `101xx` | RESOURCE 子类 | 10101=VIP, 10102=GOLD, 10103=STONE, 10104=WOOD, 10105=GRAIN | | `201xx` | SPEED 子类 | 20101=全加速, 20102=建筑, 20103=训练, 20104=研究, 20105=治疗 | | `301xx` | GAIN 子类 | 30101-30111 各种产出/Buff | | `401xx` | 装备材料 | 40101 羽毛 / 40102 皮革 / 40103 铁石矿 / 40104 兽骨 / 40105 水晶 / 40106 丝绸 / 40107 乌木 | | `402xx` | 装备本体 | 40201=武器/40202=头盔/40203=胸甲/40204=手套/40205=裤/40206=鞋/40207=饰品 | | `403xx` | 装备图纸 | 40301-40307 对应 7 个部位 | | `501xx-509xx` | OTHER 杂项 | 50101=经验书 / 50201=改名卡 / 50202=工人招募 / 50203=迁城 / 50204=钥匙 / 50205=文明更换 / 50206=天赋重置 / 50207=建筑升级 / 50208=行动力 / 50301-50304=升星材料 / 50305=技能材料 / 50901=英雄雕像 | ### 2.4 每类道具的堆叠/唯一性特征 | 类 | 堆叠 | maxStack | 唯一字段 | |---|---|---|---| | 装备 (`isEquipItem(subType) == true`,即 40201/40202/40203/40204/40205/40206/40207) | ❌ 每件独立 | 1(强制) | `itemIndex` + `exclusive`(专属英雄)+ `heroId`(穿戴者) | | 其他全部 | ✅ 无限叠 | **代码中无 maxStack 限制**(`overlay` int 累加) | 只按 itemId 合并到同一 itemIndex | > ⚠️ **没有 maxStack 限制**:`addItem` 只在装备分支才循环创建多条;其他都是 `overlay = oldOverlay + itemNum` 直接累加。配置表 `ItemDefine` 也**没有 maxStack 字段**。 --- ## 3. Item 数据模型 ### 3.1 `.ItemInfo` sproto(`Common.sproto` L308) | 字段 | 类型 | 含义 | |---|---|---| | `itemIndex` | integer | **背包槽位**,自增正整数,玩家维度唯一(`getFreeItemIndex` 找最小空位) | | `uniqueIndex` | integer | 唯一索引(实测 Lua 中未使用,可能为后续扩展预留) | | `itemId` | integer | 道具配置 ID,关联 `s_Item.ID` | | `overlay` | integer | 叠加数量(装备恒为 1) | | `exclusive` | integer | 装备专属标志,`0` 不专属;非 0 时表示绑定英雄 ID(影响装备分解和穿戴) | | `heroId` | integer | 当前穿戴此装备的英雄 ID(0 = 未穿戴) | ### 3.2 服务端存储(`MSM.d_item[rid]` snax service) `MSM.d_item[rid].req` 提供的操作: - `Get(rid)` → 全部 items dict - `Get(rid, itemIndex)` → 单条 - `Get(rid, itemIndex, fields)` → 部分字段 - `Set(rid, itemIndex, field, value)` → 改单字段 - `Add(rid, itemIndex, itemInfo)` → 新增 - `Delete(rid, itemIndex)` → 删除 `Enum.Item` 字段名常量:`itemIndex / itemId / overlay / exclusive / heroId`(即 ItemInfo 5 字段) ### 3.3 客户端缓存(`BagProxy.cs`) ``` public Dictionary Items; // itemIndex → 道具实体 private List m_itemList; // 顺序列表 private Dictionary m_itemNumMap; // itemId → 累计数量(懒计算,-1 表示需重算) private Dictionary m_materialItemInfos; // 装备材料分组 private Dictionary m_equipItemInfos; // 装备本体分组 private Dictionary> m_reddotRecord; // type → (itemIndex → 新增数量) private Dictionary m_reddotTotalDic; // type → 红点总数缓存 ``` 派生类: - `ItemInfo` 基础:`ItemIndex / ItemID / ItemNum / ItemType / HeroID` - `EquipItemInfo : ItemInfo` 额外:`Exclusive / Order / Group`(来自 `EquipDefine`) - `MaterialItem : ItemInfo` 额外:`MaterialDefine` + `MaterialType`(Material / Equip / Drawing / DrawingMaterial) ### 3.4 新道具标记存储(**纯客户端 PlayerPrefs**) Key 格式:`{rid}/itemIndex:{itemIndex}` → int | 值 | 含义 | |---|---| | 不存在 | 还没在客户端被看到 | | `1` | 新道具(红点亮) | | `-1` | 旧道具(已被标记/查看过) | | `0` | 已清除 | 特殊情况:装备 `heroId != 0`(已穿戴)→ 强制 `SetLocalItemToOld`,不亮红点。 --- ## 4. 配置表清单 ### 4.1 `ItemDefine`(`ItemConfig.cs`,对应服务端 `s_Item`) | 字段 | 类型 | 含义 | |---|---|---| | `ID` | int | 主键,道具 ID(同 itemId) | | `l_nameID` | int | 道具名称语言包 ID | | `l_tipsID` | int | tips 道具名称语言包 ID | | `subType` | int | 子分组标签(`Enum.ItemSubType`,5 位数) | | `type` | int | 功能分组(`Enum.ItemType`,1-6) | | `l_typeDes` | int | 功能分组语言包 | | `typeGroup` | int | 类型组(装备页用,1=Material 2=Equip 3=Drawing 4=DrawingMaterial) | | `itemIcon` | string | 道具图标资源路径 | | `lv` | int | 使用等级要求(实测大部分==0) | | `quality` | int | 品质 1-5(白/绿/蓝/紫/橙,对应 `Enum.ItemQualityType`) | | `batchUse` | int | 是否可批量使用(`Enum.ItemBatchUse`,0/1) | | `l_buttonDes` | int | 按钮文字语言包(`<1` 表示无按钮) | | `itemFunction` | int | **道具功能类型**,对应 `Enum.ItemFunctionType`(见 §4.2) | | `data1` | int | 功能参数 1(数值,如 VIP 加多少点/兑换比例) | | `data2` | int | 功能参数 2(关联 ID,如 `s_ItemPackage` group ID / `s_ItemRewardChoice` ID / cityBuff ID) | | `l_desID` | int | 道具描述语言包(`string.format(text, desData1, desData2)`) | | `desData1` | int | 描述参数 1 | | `desData2` | int | 描述参数 2 | | `l_topID` | int | 图标顶部信息语言包(`<1` 不显示) | | `topData` | int | 顶部信息参数 | | `get` | List\ | 获得途径列表,关联 `s_ItemGet` | | `shortcutPrice` | int | 快捷使用价格(钻石购买后直接使用,用于行动力/VIP 道具) | | `shopPrice` | int | 商城道具价格 | | `redDotPrompt` | int | 是否要红点提示(0/1) | | `rank` | int | 显示排序(背包内同 type 按 rank 升序,rank 相同按 itemId 升序) | > ⚠️ **没有 `maxStack` / `bind` / `tradeable` 字段**(与同名 IGG 老项目 `item.json` 形成对比,那个有 `MaxNum / CanTrade / IsDecompose`,但不是 ROK 用的)。 ### 4.2 `Enum.ItemFunctionType` 枚举(**核心扩展点**) | 值 | 名称 | 服务端处理(`Item.lua:ItemUse`) | 客户端处理(`BagMediator.BtnOperate` + `PlayerCmd`) | |---|---|---|---| | 0 | NOT_USE | 直接返回 `ITEM_NOT_USE` 错误 | 无按钮 | | 1 | OPEN_ITEMPACKAGE | 调用 `getItemPackage(data2)` 发奖(`data2` = s_ItemPackage group) | 直接发请求 | | 2 | CHOOSE_ITEMPACKAGE | 校验 `s_ItemRewardChoice[data2][id]`,按选择 ID 发对应 reward | 弹 `BagGiftOpenView`(ShowType=1)让玩家选 | | 3 | RECYCLE | 同 OPEN_ITEMPACKAGE(用 data2 兑换包) | 弹 `BagGiftOpenView`(ShowType=2)确认兑换 | | 4 | CITY_BUFF | `RoleLogic:addCityBuff(data2)` 加 buff | `m_cityBuffProxy.SendUseItem(data2, 1, cb)` | | 5 | VIP | `RoleLogic:addVip(data1 * itemNum)` | 走 VIP 满级校验后直接发 | | 6 | ACTION_FORCE | `RoleLogic:addActionForce(data1 * itemNum)` | 直接发 | | 10 | KINGDOM_MAP | 调 `DenseFogLogic:openNearDenseFog(pos)`,开雾失败则 `getItemPackage(data2)` 补偿 | 检测迷雾,否则弹 `OpenFogShow` 让玩家选位置;返回奖励则弹 `ItemCollection` | | 12 | SUMMON_MONSTER | `MSM.MonsterSummonMgr.req.summonMonster(data2)` 召唤野怪 | 关闭背包,相机飞向召唤点,播放召唤特效 | | 13 | SECONDE_QUEUE | 工人小屋:满建造队列则发 `data2` 补偿;否则 `BuildingLogic:unlockQueue(workQueueTime)` | 根据 `result.status` 分别提示成功/延长/补偿 | | 14 | TRAIN_NUM | 预备部队增容:`itemAddTroopsCapacity = data1`,`itemAddTroopsCapacityCount += itemNum` | 若已有不同档位 `data1` 则二次确认 | | 35 | LEAGUE_POINTS | 校验入盟后 `GuildLogic:addGuildCurrency(leaguePoints, data1*num)`,返回 `rewardInfo.leaguePoints` | 直接发 | | 7 | (客户端独有)跳转界面 | — | `SystemOpen.IsCanOpenByUiId(data1)` + `OpenUI2(data1, data2)` | | 17 | (客户端独有)暂未实现 | — | Tip 提示"暂未开放" | | 30 | (客户端独有)通用迁城 | — | 弹 `MoveCity` 界面 | | 31 | (客户端独有)随机迁城 | — | 二次确认后发 `Map_MoveCity{type=4}` | > 关键约束:服务端枚举值与客户端的 `BtnOperate` 分支**必须一一对应**,新增 itemFunction 时两边都要改。 ### 4.3 其他 Item 相关 Define | 客户端类 | 服务端表 | 用途 | |---|---|---| | `ItemPackageDefine` | `s_ItemPackage` | 奖励组(按 randomGroup 分组随机,含 type/typeData/odds/number/civilization_limit/numberStep_lv/numberStep_increment/numberFloat_min/numberFloat_max) | | `ItemRewardChoiceDefine` | `s_ItemRewardChoice` | 选包奖励配置(按 data2 关联,每条 `{id, reward}` 表示一个选项对应的 itemPackage group) | | `ItemHeroDefine` | `s_Hero` 关联 | 英雄相关道具映射(英雄碎片 → 英雄 ID) | | `ItemGetDefine` | `s_ItemGet` | 获得途径配置(关联 `ItemDefine.get[]`) | | `ItemPackageShowDefine` | `s_ItemPackageShow` | 礼包展示配置 | | `ItemPlayerHeadDefine` | `s_ItemPlayerHead` | 头像框配置 | | `EquipDefine` | `s_Equip` | 装备本体配置(含 makeMaterial / makeMaterialNum / order / group) | | `EquipMaterialDefine` | `s_EquipMaterial` | 装备材料配置(含 mix / split / mixCostNum 用于合成分解) | ### 4.4 配置条目数 ROK 客户端使用 SQLite/Bin 序列化(`Assets/StreamingAssets/Config/Bin/Item.bin` ≈ 87KB),**没有可读的 item.json**。`gmd/Unity/Assets/Bundles/Config/item.json` 是另一个老项目残留(字段为 `MaxNum / CanTrade / FunctionID` 等,与 ROK 完全不同结构),**不属于本次调研范围**。 实际条目数无法从源代码直接计数,但根据 itemId 编码(5 个一级类 × 数百子分类)+ initialItemType 已出现 `502070003`、`201010013` 等量级,**估算总条目数在 200-500 范围**。 ### 4.5 `ItemPackageType` 奖励类型枚举(`s_ItemPackage.type`) | 值 | 名称 | typeData 含义 | |---|---|---| | 0 | NONE | 空 | | 100 | CURRENCY | `Enum.CurrencyType`(food/wood/stone/gold/denar/actionForce/vip/expeditionCoin/individualPoints/leaguePoints/activityActivePoint) | | 200 | ITEM | `s_Item.ID`(普通道具) | | 300 | SOLDIER | `s_Arms.ID` | | 400 | HERO | `s_Hero.ID` | | 500 | SUB_ITEM_TYPE | `Enum.ZeroEmptyType.SUB_ITEM_TYPE` 关联表 ID(再从子表 Random 出具体 itemId) | | 600 | GUILD_GIFT | 联盟礼物类型 | ### 4.6 `CurrencyType`(`OtherEnum.lua` L101,**不属于 Item,是 Role 字段**) | 值 | 名称 | 含义 | |---|---|---| | 100 | food | 粮食 | | 101 | wood | 木材 | | 102 | stone | 石料 | | 103 | gold | 金币 | | 104 | denar | 宝石(钻石) | | 105 | actionForce | 行动力 | | 106 | individualPoints | 联盟个人积分 | | 107 | leaguePoints | 联盟积分 | | 108-111 | allianceFood/Wood/Stone/Gold | 联盟资源 | | 112+ | vip / expeditionCoin / activityActivePoint | VIP 点 / 远征币 / 活动积分 | --- ## 5. 业务规则详细 ### 5.1 添加道具 `ItemLogic:addItem(args)` **入参** `args` 表:`{rid, itemId, itemNum=1, exclusive=nil, noSync=nil, eventType=nil, eventArg=nil}` **流程**(`ItemLogic.lua` L69-164): 1. 查 `s_Item:Get(itemId)`,不存在 → `LOG_ERROR` 返回 nil 2. **特殊分支**:若 `sitemInfo.type == HEAD`,调 `RoleLogic:unlockRoleHead(rid, itemId)` 解锁头像框后**直接返回 `{}`**,不进背包 3. **是否堆叠**: - **装备类**(`isEquipItem(subType)` = subType 在 ARMS/HELMET/BREASTPLATE/GLOVES/PANTS/ACCESSORIES/SHOES 中)→ 跳过查找,必新增 - **其他**:遍历 `getItem(rid)` 查同 `itemId` 的现有 itemIndex 4. **堆叠分支**:找到现有同 itemId → `overlay = oldOverlay + itemNum`,`Set(rid, itemIndex, overlay, newNum)` 5. **新建分支**: - 非装备:`getFreeItemIndex(rid)` 取最小空 index,新建一条 `{itemId, overlay=itemNum, rid, itemIndex, exclusive, heroId=0}`,`Add(rid, itemIndex, itemInfo)` - 装备:**循环 itemNum 次**,每次 `getFreeItemIndex` + `overlay=1` 单独建条目(**装备永远不堆叠**) 6. 同步:`syncItem(rid, nil, syncItemInfo, true)` 推 `Item_ItemInfo` 7. 日志:若 `eventType` 非空,调 `LogLogic:itemChange({rid, iggid, itemId, changeNum, oldNum, newNum, logType, logType2})` 8. 资源阈值告警:若 `s_GameWarning:Get(itemId, "num")` 存在且 `newNum > 阈值` → `Common.sendResourceAlarm(rid, itemId, newNum)` **返回值**:`syncItemInfo` dict(itemIndex → 新建/更新后的 ItemInfo) **触发新道具标记**:服务端**不主动标**,客户端 `BagProxy.UpdateItemInfo` 收到推送时根据 `m_isFirstGetItemInfo == false`(即非首次登录) + `redDotPrompt == 1` + isNewItem 或 overlay 增加,则在 `m_reddotRecord[type][itemIndex]` 写入新增数量。 **协议**:`Item_ItemInfo` (30101) 增量推送 **错误码**:无(addItem 自身只 LOG_ERROR,不返回错误码) --- ### 5.2 移除道具 #### 5.2.1 按 itemIndex 移除:`ItemLogic:delItem(rid, itemIndex, itemNum, noSync, logType, logExtraType)` **流程**(`ItemLogic.lua` L167-198): 1. `getItem(rid, itemIndex)` 取道具实体 2. **数量判定**: - `overlay <= itemNum` → 整条删除:`MSM.d_item[rid].req.Delete(rid, itemIndex)`,`newNum = 0` - `overlay > itemNum` → 扣减:`Set(rid, itemIndex, overlay, overlay - itemNum)` 3. 同步推送 `{[Enum.Item.overlay] = newNum}` 4. 日志:`LogLogic:itemChange({rid, iggid, logType, logType2, itemId, changeNum, oldNum, newNum})` **返回值**:`syncItems` dict > ⚠️ `delItem` **不校验** `itemNum > overlay`,传过量会直接清零(业务方需自己先 `checkItemEnough`)。 #### 5.2.2 按 itemId 移除:`ItemLogic:delItemById(rid, itemId, itemNum, noSync, logType, logExtraType)` **流程**(`ItemLogic.lua` L241-250):遍历所有 items 找第一个匹配 itemId,转发到 `delItem`。 > ⚠️ 业务侧约束:因为非装备同 itemId 必合并在同一 itemIndex,所以只有一条匹配;**装备类不应该用 delItemById**(每个 itemIndex 独立)。 #### 5.2.3 数量检查:`ItemLogic:checkItemEnough(rid, itemId, itemNum)` **返回**:`(boolean, overlay)`。 **特点**:因为非装备无限叠加 + 必合并,**只检查第一条匹配的 itemId.overlay 是否 >= itemNum**。 **装备移除前是否校验穿戴**:`delItem` 本身**不校验** `heroId != 0`;具体校验由调用方在装备分解逻辑里处理(`Build_DecompositionEquipment` proxy handler 校验,本次未深入展开)。 **RPC**:无独立移除 RPC(移除都从其他业务里调用,如使用消耗、装备分解、资源兑换扣道具等) **错误码**:无(delItem 不报错);上层有 `ITEM_NOT_ENOUGH(8000)` `ITEM_NOT_EXIST(8002)` --- ### 5.3 通用奖励/消耗结构 #### 5.3.1 数据结构 | 名称 | 结构 | |---|---| | 奖励项(sproto) | `.RewardItem { itemId, itemNum }` | | 完整奖励信息 | `.RewardInfo { food, wood, stone, gold, denar, items:*RewardItem, soldiers:*SoldierInfo, groupId, actionForce, guildGifts, heros:*Heros, expeditionCoin, guildPoint, vip, leaguePoints, activityActivePoint }` | | 消耗项(sproto) | `.Items { itemId, itemNum }`(用于商店购买/合成消耗) | > 注意:ROK **没有统一的 `CostInfo`**,消耗信息分散在各 RPC 的 request 里(如 `Shop_BuyShopItem.itemId/itemNum`)。 #### 5.3.2 批量接口 | 函数 | 用途 | |---|---| | `ItemLogic:giveReward(rid, rewards, groupId, noSync, noHeroShow, block, isBuyGift, packageNameId)` | **核心发奖入口**,遍历 RewardInfo 各字段调对应 `RoleLogic:add*` / `addItem` / `ArmyTrainLogic:addSoldiers` / `HeroLogic:addHero` | | `ItemLogic:getItemPackage(rid, groupId, noAdd, noSync, noHeroShow, noMerge, block, isBuyGift, openNum, mergeHero, packageNameId)` | **打开礼包**:调 `getGroupPackage` 计算实际奖励,再调 `giveReward` 发放 | | `ItemLogic:getGroupPackage(rid, groupId, noMerge, openNum, mergeHero)` | **奖励计算**:按 randomGroup 加权随机 + 等级增量 + 浮动比例,输出合并后的 reward 结构 | | `ItemLogic:mergeReward(rawReward, addReward)` | **奖励合并**:把两个 RewardInfo 累加(货币求和,items/soldiers/heros 按 ID 合并) | | `ItemLogic:checkItemEnough(rid, itemId, itemNum)` | 单道具数量校验 | #### 5.3.3 事务性 **ROK 没有事务**。`giveReward` 遍历各字段顺序发放,任一中途失败: - 货币 `add*` 失败:仅 LOG_ERROR,已发的不会回滚 - `addItem` 失败:同上 - **业务方需保证 reward 结构合法**(数量 > 0 / itemId 存在),失败属于配置错误 业务实现"先检查再扣除"模式:典型如 `Shop_BuyShopItem`: 1. `checkItemEnough(rid, costItemId, costNum)` → false 返回 `ITEM_NOT_ENOUGH` 2. `delItemById(rid, costItemId, costNum)` 扣除 3. `addItem(rid, productItemId, productNum)` 发放 **约束**:检查/扣除/发放之间**没有锁**(单线程 Lua 协程模型,actor 内串行执行天然安全;跨 actor 调用需 RPC 等待) --- ### 5.4 道具使用 `Item.lua:response.ItemUse(msg)` #### 5.4.1 触发流程(客户端) ``` 玩家点击道具 → BagMediator.RefreshItemDetail 显示按钮 ↓ 玩家点"使用" BagMediator.BtnOperate 按 itemFunction 分支: ├─ itemFunction == 2 → 弹 BagGiftOpenView(选包),用户选完调 Send(itemIndex, num, selectId) ├─ itemFunction == 3 → 弹 BagGiftOpenView(兑换确认),用户确认后 Send(itemIndex, num, 0) ├─ itemFunction == 4 → cityBuffProxy.SendUseItem(data2) → Send(itemIndex, 1, 0) ├─ itemFunction == 10 → 检测迷雾,否则弹 OpenFogShow 选坐标后发请求(含 pos) ├─ itemFunction == 14 → 若已有不同档位预备部队 buff,二次确认 ├─ itemFunction == 31 → 二次确认后发 Map_MoveCity(非 Item_ItemUse) └─ 其他 → Send(itemIndex, num, 0) → 发 Item_ItemUse.request ``` `Send` 实现: ```csharp var sp = new Item_ItemUse.request { itemIndex = itemIndex, itemNum = itemNum, id = id, // >0 时才赋值,给 CHOOSE_ITEMPACKAGE 选包用 }; AppFacade.GetInstance().SendSproto(sp); ``` #### 5.4.2 服务端校验流程 `Item.lua:response.ItemUse` 完整流程(L82-221): 1. **参数校验**:`itemIndex` / `itemNum` 必填 → 否则 `ITEM_ARG_ERROR` 2. **道具存在**:`getItem(rid, itemIndex)` → 空则 `ITEM_NOT_EXIST` 3. **可用性**:`sitem.itemFunction == NOT_USE(0)` → `ITEM_NOT_USE` 4. **批量限制**:`itemNum > 1 && sitem.batchUse == NO(0)` → `ITEM_NOT_BATCH_USE` 5. **业务前置**: - LEAGUE_POINTS:未入盟 → `ITEM_NOT_JOIN_GUILD` 6. **数量足够**:`itemInfo.overlay < itemNum` → `ITEM_NOT_ENOUGH` 7. **召唤怪物分支**:SUMMON_MONSTER 时先 `MonsterSummonMgr.req.summonMonster` 校验成功,失败 → `ITEM_SOMMON_MONSTER_FAILED` 8. **选包校验**:CHOOSE_ITEMPACKAGE 时 `s_ItemRewardChoice[data2][id]` 不存在 → `ITEM_PACKAGEID_NOT_EXIST` 9. **扣除道具**:`delItem(rid, itemIndex, itemNum, nil, USE_BAG_ITEM_COST_ITEM)` 10. **任务进度**:`TaskLogic:updateItemUseTaskSchedule(rid, nil, itemNum, sitem)` 11. **按 itemFunction 分发**: - OPEN_ITEMPACKAGE / RECYCLE → `rewardId = data2` - CHOOSE_ITEMPACKAGE → `rewardId = s_ItemRewardChoice[data2][id].reward` - CITY_BUFF → `addCityBuff(data2)` 直接返回 - VIP / ACTION_FORCE / LEAGUE_POINTS → 调 `add*` 直接返回 - KINGDOM_MAP → `openNearDenseFog(pos)`,开雾失败则 `getItemPackage(data2)` 补偿,含 rewardInfo 返回 - SECONDE_QUEUE → 满队列返补偿包,否则 `unlockQueue` - TRAIN_NUM → 修改 `itemAddTroopsCapacity` + `Count`,syncSelf 推送 12. **最终发奖**:若 `rewardId > 0` → `getItemPackage(rid, rewardId, openNum=itemNum)` 发放 13. **响应**:`{itemId, itemNum, rewardInfo, objectIndex(召怪用), pos(召怪位置), status(队列状态)}` #### 5.4.3 选择数量使用 / 选择目标使用 | 模式 | 实现 | |---|---| | **批量数量** | request.itemNum 字段;服务端 `batchUse == YES` 时支持;`getItemPackage` 内部 `openNum = itemNum` 表示开 N 次抽奖 | | **选包 ID** | request.id 字段;只在 CHOOSE_ITEMPACKAGE 时传,对应 `s_ItemRewardChoice[data2][id].reward` 之 itemPackage group | | **选择坐标** | request.pos 字段;只用于 KINGDOM_MAP(开迷雾选位置) | | **选择英雄** | **不支持**——经验书等给英雄加经验的道具走的是 `Hero_ExchangeHeroItem`(602)等独立 RPC,**不走 ItemUse** | #### 5.4.4 涉及 RPC | RPC | ID | request | response | |---|---|---|---| | `Item_ItemUse` | 852 | itemIndex, itemNum, id(选包), pos(王国地图) | itemId, itemNum, rewardInfo, status, objectIndex, pos | #### 5.4.5 错误码 | 错误码 | 值 | 含义 | |---|---|---| | `ITEM_NOT_ENOUGH` | 8000 | 道具不足 | | `ITEM_ARG_ERROR` | 8001 | 参数错误 | | `ITEM_NOT_EXIST` | 8002 | 道具不存在 | | `ITEM_NOT_BATCH_USE` | 8004 | 不支持批量使用 | | `ITEM_NOT_USE` | 8005 | 不可使用 | | `ITEM_PACKAGEID_NOT_EXIST` | 8006 | 所选礼包组不存在 | | `ITEM_KINGDOM_MAP_NO_POS` | 8007 | 王国地图未传坐标(实测代码中未使用,预留) | | `ITEM_NOT_JOIN_GUILD` | 8008 | 未加入联盟 | | `ITEM_SOMMON_MONSTER_FAILED` | 8009 | 召怪失败(周围乱军太多) | --- ### 5.5 道具"脚本"机制(**实际上没有独立脚本表**) #### 5.5.1 真实实现 `ItemScript.lua` 仅有 3 个空函数(`requireAllScript / useItem / dropItem`),**没有任何注册机制**。所有"脚本"逻辑都是 `Item.lua:ItemUse` 函数内的 `if/elseif (sitem.itemFunction)` 巨型分支。 **没有的东西**: - ❌ 没有 itemId → Lua 函数的映射表 - ❌ 没有动态加载脚本 - ❌ 没有 `s_ItemScript` 配置表 **有的东西**: - ✅ `ItemDefine.itemFunction`(int 枚举)一一对应硬编码 if 分支 - ✅ `ItemDefine.data1 / data2` 作为该分支的参数 #### 5.5.2 "脚本类型"清单(即 §4.2 ItemFunctionType 表) 按业务能力归类: | 业务能力 | itemFunction | 实现位置 | |---|---|---| | 直接发奖励组 | 1 / 3 | `getItemPackage(data2)` | | 选包奖励 | 2 | `s_ItemRewardChoice[data2][id].reward` → `getItemPackage` | | 时效 Buff | 4 (CITY_BUFF) | `RoleLogic:addCityBuff(data2)` | | 加 VIP / 行动力 | 5 / 6 | `RoleLogic:addVip / addActionForce` | | 解锁迷雾 | 10 | `DenseFogLogic:openNearDenseFog(pos)` | | 召唤野怪 | 12 | `MonsterSummonMgr.req.summonMonster(data2)` | | 工人小屋扩展 | 13 | `BuildingLogic:unlockQueue` | | 预备部队增容 | 14 | 修改 Role 字段 | | 联盟积分 | 35 | `GuildLogic:addGuildCurrency(leaguePoints)` | #### 5.5.3 选包奖励的客户端选择流程 ``` 点击使用 → BagMediator.BtnOperate 检测 itemFunction == 2 → 弹 BagGiftOpenView (ShowType=1) → m_rewardGroupProxy.GetChoiceRewardDataByGroup(define.data2) // 查 s_ItemRewardChoice[data2] 列表,每条 {id, reward(=itemPackage group)} → 用户点击某选项 → m_selectId = data.id → 点确定 → Item_ItemUse.request { itemIndex, itemNum, id = m_selectId } → 服务端按 id 查到具体 reward group → getItemPackage 发放 ``` --- ### 5.6 新道具标记 / 红点 #### 5.6.1 服务端/客户端分工 - **服务端**:**完全不维护新道具状态**,只负责增量推 `Item_ItemInfo` - **客户端**:完全在 `BagProxy` 中维护: - 持久化:`PlayerPrefs` 用 `{rid}/itemIndex:{itemIndex}` 存"新/旧/已清"三态 - 内存:`m_reddotRecord[type][itemIndex] = 新增数量` + `m_reddotTotalDic[type] = 红点总数` #### 5.6.2 触发时机(`BagProxy.UpdateItemInfo` + `ReddotRecord`) 1. **首次登录**(`m_isFirstGetItemInfo == true`):**全部不算新道具**(避免登录时所有道具都亮红点),并强制 `SetLocalItemToOld` 把所有 itemIndex 标为 -1 2. **非首次**:服务端推来一条 ItemInfo,且 `ItemDefine.redDotPrompt == 1`,且: - 新增 `itemIndex` → 红点+=overlay - 同 `itemIndex` overlay 增加 → 红点+=增量 - 同 `itemIndex` overlay 减少 → 移除红点记录 3. **装备已穿戴**(`heroId != 0`):直接 `SetLocalItemToOld`,不亮 #### 5.6.3 清除时机 | 函数 | 时机 | |---|---| | `ClearReddotRecordByIndex(index, itemId)` | 用户在 BagPanel 选中该道具看详情时(`RefreshItemDetail` 内调) | | `ClearReddotRecordByType(type)` | 用户切换 Bag Tab 时(`SwitchMenu` 内对原 type 调) | | `ClearAllReddotRecord()` | BagPanel 关闭时(`BagMediator.OnRemove`) | > ⚠️ 设计后果:**只要打开过背包并选了道具,红点就消**。不需要服务端记账。 #### 5.6.4 红点数量统计 `GetBagReddotTotal()` = sum over type `GetBagReddotNumByType(type)` = sum over reddotRecord[type][index] --- ### 5.7 资源类道具(金币 / 钻石 / 等) #### 5.7.1 双轨制设计(**关键决策点**) ROK 采取**双轨制**: | 维度 | 玩家货币(Role 字段) | 资源类道具(Bag Item) | |---|---|---| | 存储位置 | `Role.food / wood / stone / gold / denar / vip / actionForce / expeditionCoin / individualPoints / leaguePoints / activityActivePoint` | `s_Item` 配置 + 背包 Item 条目 | | 操作接口 | `RoleLogic:addGold / addStone / addFood / addVip / addActionForce …` | `ItemLogic:addItem / delItem` | | 同步协议 | `Role_RoleInfo` 推送 | `Item_ItemInfo` 推送 | | 上限 | 单字段 int64,配置表内有 individualPointsLimit/alliancePointsLimit 等业务上限 | `s_GameWarning.num` 仅做告警,**无硬上限** | | 红点 | 无 | 有(按 type 统计) | | 例子 | 玩家身上的"500 金币"是 Role.gold 字段 | 背包里"金币袋 x3(每袋可换 1000 金币)"是 itemId 在 RESOURCE 类下的 Item | #### 5.7.2 资源类道具如何转成货币 `Item_ItemChangeResource`(851): ``` request: { itemIndex, itemNum } → getItem(rid, itemIndex) 取 itemInfo → 校验 s_Item.type == RESOURCE 或 subType == ACTION_FORCE → 否则 ITEM_NOT_RESOURCE_ITEM → checkOverlay >= itemNum → 否则 ITEM_NOT_ENOUGH → delItem 扣除 → 按 subType 分支调对应 RoleLogic:add*: - GOLD → addGold(data1 * itemNum) - STONE → addStone - WOOD → addWood - GRAIN → addFood - VIP → addVip - ACTION_FORCE → addActionForce → updateItemUseTaskSchedule response: { result, itemId, itemNum } ``` > ⚠️ **没有 DENAR(钻石)/ DIAMOND 这条分支**——钻石不能通过道具兑换得来(设计上钻石是付费货币)。 #### 5.7.3 资源大额变动审计 - 全部走 `LogLogic:itemChange` / `LogLogic:roleChange`(每个 RoleLogic:add* 内部都会调) - 阈值告警:`s_GameWarning.num` 配置单 itemId 阈值 → `Common.sendResourceAlarm(rid, itemId, newNum)` 发给监控 --- ### 5.8 初始资源发放 `ItemLogic:createRoleGiveItems(rid)`(L49-66): ``` 读取 s_Config:Get("initialItemType") 和 ("initialItemNum") 两个 list 长度必须一致(不一致以小的为准) 循环调 addItem(rid, itemId, itemNum, eventType=GUILD_CREATE_ROLE_GAIN_ITEM) ``` **实际配置值**(`Server/common/config/gen/Configs.data` L13376 `s_Config[0]`): ``` initialItemType = { 502030004, 502010001, 502020001, 502040002, 201010010, 201010013, 502070001, 502070003 } initialItemNum = { 8, 1, 1, 1, 30, 8, 10, 10 } initialFood = 100000 initialWood = 100000 initialStone = 0 initialGold = 0 initialDiamond = 300 // 钻石(denar) initialArmsType = { 10101 } // 士兵类型 initialArmsNum = { 1000 } // 士兵数 initialBuff = 30105 // 初始 City Buff(治疗加速) ``` - 道具初始有 8 种 × 各若干(共 69 个道具实例) - 货币初始:粮 10W、木 10W、钻 300,金/石/行动力默认 0 - 士兵初始:10101 类型 1000 个 > 资源发放走 `RoleLogic:createRole`(或 `setRole` 直接赋值字段),不走 `addItem`。 --- ## 6. 协议清单 ### 6.1 RPC 协议(请求-响应) | 协议名 | ID | 方向 | request | response | 用途 | |---|---|---|---|---|---| | `Item_ItemChangeResource` | 851 | C→S | `itemIndex, itemNum` | `result, itemId, itemNum` | 使用资源类道具兑换玩家货币(金/木/石/粮/VIP/行动力) | | `Item_ItemUse` | 852 | C→S | `itemIndex, itemNum, id, pos` | `itemId, itemNum, rewardInfo, status, objectIndex, pos` | 使用任意可用道具(按 itemFunction 分发) | ### 6.2 推送协议(服务端 → 客户端) | 协议名 | ID | 方向 | request | 用途 | |---|---|---|---|---| | `Item_ItemInfo` | 30101 | S→C | `itemInfo : *ItemInfo(itemIndex)` | 增量推送道具变更(新增/数量变更/删除),客户端按 `itemIndex` 合并到本地 `Items` dict | ### 6.3 间接相关协议(涉及道具但不属于 Item 模块) | 协议 | 用途 | |---|---| | `Shop_BuyShopItem (901)` | 普通商店购买,含 itemId/itemNum | | `Shop_BuyPostItem (902)` | 驿站道具 | | `Shop_BuyVipStore (904)` | VIP 商店购买 | | `Shop_BuyExpeditionStore (905)` | 远征商店购买 | | `Build_ProduceMaterial / Build_MaterialDecomposition / Build_MaterialSynthesis / Build_MakeEquipment / Build_DecompositionEquipment / Build_CheckMakeEquip` | 装备/材料生产/合成/分解 | | `Hero_ExchangeHeroItem (605)` | 英雄碎片兑换 | | `Role_RoleInfo (30251)` | 含 RoleInfo(货币字段 food/wood/stone/gold/denar 等) | --- ## 7. 错误码清单(`ITEM_` 前缀) | 错误码 | 值 | 含义 | |---|---|---| | `ITEM_NOT_ENOUGH` | 8000 | 道具不足 | | `ITEM_ARG_ERROR` | 8001 | 道具模块参数错误 | | `ITEM_NOT_EXIST` | 8002 | 道具不存在 | | `ITEM_NOT_RESOURCE_ITEM` | 8003 | 不是资源类型道具(ChangeResource 用) | | `ITEM_NOT_BATCH_USE` | 8004 | 道具不支持批量使用 | | `ITEM_NOT_USE` | 8005 | 道具无法使用(itemFunction=0 或前置不满足) | | `ITEM_PACKAGEID_NOT_EXIST` | 8006 | 所选道具礼包组不存在 | | `ITEM_KINGDOM_MAP_NO_POS` | 8007 | 使用王国地图未上传坐标(代码中预留,未实际触发) | | `ITEM_NOT_JOIN_GUILD` | 8008 | 未加入任何联盟(用 LEAGUE_POINTS 道具时) | | `ITEM_SOMMON_MONSTER_FAILED` | 8009 | 召唤怪物失败 | ### 7.1 关联模块道具相关错误码(散落在其他模块前缀) | 错误码 | 值 | 含义 | |---|---|---| | `ROLE_NAME_ITEM_DENAR_NOT_ENOUGH` | 1046 | 改名道具+钻石都不足 | | `ROLE_BUFF_ITEM_NOT_ENOUGH` | 1048 | buff 道具不足 | | `ROLE_SPEED_ITEM_NOT_ENOUGH` | 1066 | 加速道具不足 | | `MAP_MOVE_CITY_ITEM_NOT_ENOUGH` | 2030 | 迁城道具不足 | | `HERO_SUMMON_ITEM_NOT_ENOUGH` | 3001 | 召唤统帅道具不足 | | `HERO_LEVEL_ITEM_NOT_ENOUGH` | 3005 | 升级技能道具不足 | | `HERO_EXCHANGE_ITEM_NOT_ENOUGH` | 3010 | 通用雕像不足 | | `HERO_ITEM_ERROR` | 3015 | 升星材料类型错误 | | `HERO_ITEM_TO_MUCH` | 3016 | 道具不足无法升星 | | `BUILDING_ITEM_ERROR` | 6021 | 解锁建筑道具类型错误 | | `BUILDING_SMITHY_ITEM_ERROR` | 6024 | 无法生产该道具 | | `BUILDING_SMITHY_ITEM_NOT_ENOUGH` | 6027 | 该材料不足 | | `BUILDING_EQUIP_ITEM_NO_ENOUGH` | 6030 | 装备道具不足 | | `SHOP_ITEM_NOT_SELL` | 10000 | 商品没在商店售卖 | | `SHOP_POST_ITEM_NOT_EXIST` | 10001 | 驿站道具不存在 | | `SHOP_POST_ITEM_HAVE_BUY` | 10002 | 驿站道具已购买 | | `SHOP_EXPEDITION_ITEM_COUNT_MAX` | 10007 | 远征商店购买超上限 | | `GUILD_SHOP_ITEM_NOT_EXIST` | 12073 | 联盟商店商品不存在 | | `GUILD_SHOP_ITEM_NOT_ENOUGH` | 12074 | 联盟商店商品不足 | > ⚠️ **`BAG_` 前缀错误码不存在**——背包功能复用 `ITEM_` 前缀。 --- ## 8. UI 流程(`BagMediator` / `BagGiftOpenMediator` / `UI_Win_UseItemMediator`) ### 8.1 BagPanel 分页 - **5 个 Tab**:资源(1) / 加速(2) / 增益(3) / 装备(4) / 其他(5) - Tab 切换:`OnMenuRes/Speed/Gain/Equip/Other` → `SwitchMenu(type)` → 切 Tab 时**自动清除当前 Tab 的红点记录** - 每个 Tab 显示:4 列 × N 行(`m_itemCol = 4`,`m_itemLineCount = ceil(count/4)`) - 数据源:`m_bagProxy.Items` 全部过滤 `overlay > 0`,按 `type = itemId / 100000000` 分组 - Tab 角标红点数:`m_pageReddotImgList[i]` + `GetBagReddotNumByType(i+1)` ### 8.2 排序规则 `DataSort` 在每个 Tab 数据列表上: 1. 按 `ItemDefine.rank` 升序 2. rank 相同则按 itemId 升序 ### 8.3 道具详情 Tooltip 显示内容 `RefreshItemDetail` 渲染: - 品质背景图(`m_qualityDic[quality]`,5 档) - 道具图标(`itemDefine.itemIcon`) - 顶部小卡描述(仅 `l_topID >= 1` 时显示,格式 `format(text, topData)`) - 名称(`l_nameID`) - **装备**(`typeGroup == 2`)→ 显示 `UI_Item_EquipAtt` 属性卡片 - **非装备** → 显示 `l_desID` 格式化文本 `format(text, desData1, desData2)` - **可批量使用**(`batchUse == 1`)→ 显示 `+/-` 数量调整 + 输入框 + Max 按钮 - **使用按钮**:仅 `l_buttonDes >= 1` 时显示(文字来自语言包) ### 8.4 使用流程的二次确认 | 情况 | 是否二次确认 | |---|---| | 普通礼包(itemFunction=1) | 无确认,直接发请求 | | 选包奖励(itemFunction=2) | 弹 `BagGiftOpenView` 必选,本身就是确认界面 | | 兑换确认(itemFunction=3) | 弹 `BagGiftOpenView` 显示前后对比 | | 王国地图(itemFunction=10) | 选坐标本身就是确认 | | 预备部队(itemFunction=14) | 若已有不同档位 buff 弹 `Alert` 二次确认 | | VIP 道具(UseItemView) | 计算 overflow,超出时弹 `Alert` 二次确认 | | 随机迁城(itemFunction=31) | 强制弹 `Alert` 二次确认 | | 其他 | 无 | ### 8.5 ItemUse 响应处理(`PlayerCmd.cs:Item_ItemUse.TagName`) 1. 错误响应 → `ErrorCodeHelper.ShowErrorCodeTip` 2. 成功:`Tip.CreateTip("使用了 {道具名} x{数量}")` 3. 按 `itemFunction` 分发后续 UI: - 10/13 → 弹 `s_itemCollection` 显示奖励 - 12 → 相机飞向召唤点 + 召唤特效 - 其他 → 若 `HasRewardInfo` 弹 `s_rewardGetWin` 奖励展示 --- ## 9. 与 Survivors 实现的对照点 ### 9.1 模块对应表 | ROK 模块 | Survivors 对应(计划) | 说明 | |---|---|---| | `BagProxy.cs` + 客户端缓存 | `BagComponent`(挂玩家 Entity) + `BagComponentSystem` | ET 风格的 Component/System,业务逻辑放 System | | `ItemLogic.lua:addItem/delItem` | `BagComponentSystem.AddItems / RemoveItems` | 统一入口,但 **Survivors 设计是"所有奖励都走 Item"**,比 ROK 更激进 | | `Item.lua:ItemUse` 巨型 if/elseif | C# `Dictionary` 注册机制 | 比 Lua 硬编码更可扩展,便于单测 | | `s_Item` + `ItemDefine` | Luban 配置 `ItemConfig` | 字段保留 quality/type/subType/itemFunction/data1/data2,删掉 ROK 残留(`exclusive` 改名 `bindType` 等) | | `Item_ItemInfo` 增量推送 | `M2C_ItemInfoSync`(protobuf) | 用 MemoryPack 序列化 | | `Item_ItemUse` (852) | `C2M_UseItem` / `M2C_UseItemResponse` | RPC + 推送分离 | | `BagPanel.prefab`(5 Tab) | `BagPanel.prefab`(YIUI,4 Tab:资源/装备/道具/材料) | Survivors 设计简化成 4 Tab | | `BagGiftOpenView`(选包) | `GiftOpenPanel` | 同流程 | | 红点 PlayerPrefs | ET `RedDotComponent` + 持久化到 Mongo | 服务端记账,多端同步 | | 创角初始道具 `initialItemType/Num` | `InitialResourceConfig`(Luban) | 列表配置,启动时遍历 AddItems | | 错误码 8000-8009 | `ErrorCode.cs` 同段位 | 直接复用 8000-8009 | ### 9.2 关键差异决策 | 维度 | ROK | Survivors 设计 | |---|---|---| | **货币/道具是否合并** | **分离**(双轨制) | **合并**(所有可获得资源 = Item,金币也是 itemId) | | **使用脚本机制** | 硬编码 if/elseif(itemFunction 枚举) | C# `IItemUseHandler` 注册(每个 itemFunction 一个 handler 类) | | **新道具红点** | 客户端 PlayerPrefs | 服务端 Mongo + 客户端缓存 | | **配置表** | SQLite + 自研 Define | Luban 生成 | | **协议** | sproto | protobuf3 + MemoryPack | | **持久化** | 服务端 Redis + 落 SQL | Mongo(IDBComponent) | | **装备穿戴** | item.heroId 字段 | 同 | | **奖励合并** | `mergeReward` Lua | C# 通用 RewardEntry 列表 | ### 9.3 设计陷阱(迁移时易踩) 1. **不要直接搬"双轨制"**:Survivors 设计目标是统一 Item。金币/钻石如果也作为 Item,则没有 `Item_ItemChangeResource` 这个 RPC,相关 `ItemType.RESOURCE` 子类型大量道具变成"普通礼包道具" 2. **`exclusive` 字段**:ROK 是装备专属英雄 ID,Survivors 设计中可能不需要装备绑定,可去除 3. **`uniqueIndex` 字段**:ROK 中预留未用,Survivors 直接删掉 4. **`maxStack` 缺失**:必须新增(防溢出 int 攻击) 5. **`itemFunction` 拆分**:ROK 的 35 个值分散且历史包袱重,Survivors 应只实现 P1-P3 核心 5-8 个 6. **`itemId` 编码约定**:ROK 用 `itemId / 100000000` 划分 type,Survivors 应改为 ItemConfig 显式 type 字段(itemId 不再有语义) --- ## 10. 实现优先级建议 ### P1 必须(核心基础设施,无 UI) | # | 任务 | 对应 ROK | 备注 | |---|---|---|---| | 1 | `ItemConfig` Luban 配置定义(含 type/subType/quality/itemFunction/data1/data2/icon/name/desc/maxStack) | `s_Item` + `ItemDefine` | 删 exclusive/uniqueIndex,加 maxStack | | 2 | `BagComponent`(玩家身上挂) + `ItemUnit`(子 Entity,itemIndex 主键) | `MSM.d_item[rid]` | Mongo 持久化 | | 3 | `BagComponentSystem.AddItems / RemoveItems / HasItems / GetItemNum` | `addItem / delItem / checkItemEnough` | **统一发奖/扣资源入口**,所有业务都调它 | | 4 | `M2C_ItemInfoSync` 增量推送 | `Item_ItemInfo` (30101) | 客户端 `BagComponentClient` 合并 | | 5 | `RewardEntry { itemId, count }` 通用结构 | `.RewardItem` | 一切奖励/消耗都用它 | | 6 | `InitialResourceConfig` + 创角发放 | `s_Config.initialItemType/Num` | Login 时调 | | 7 | 错误码 8000-8009 | ErrorCode.cs | 直接复用 | ### P2 必须(道具使用 + 核心 itemFunction) | # | 任务 | 对应 ROK | 备注 | |---|---|---|---| | 1 | `C2M_UseItem` Handler | `Item.lua:ItemUse` | 校验/扣道具/调 handler/返回结果 | | 2 | `IItemUseHandler` 注册机制 + `ItemUseHandlerComponent` | 巨型 if/elseif | C# 字典查找替代硬编码 | | 3 | `ItemUseHandler_OpenPackage`(itemFunction=1) | OPEN_ITEMPACKAGE | 调 `ItemPackageSystem.GetReward` | | 4 | `ItemUseHandler_ChoosePackage`(itemFunction=2) | CHOOSE_ITEMPACKAGE | 含 request.id 参数 | | 5 | `ItemPackageConfig` + `ItemPackageSystem.GetReward(groupId, openNum)` | `s_ItemPackage` + `getItemPackage` | 加权随机 | | 6 | `ItemRewardChoiceConfig` | `s_ItemRewardChoice` | 选包配置 | | 7 | 操作日志 `LogLogic:itemChange` 等效 | `LogLogic` | jsonl 落盘 | ### P3 可推迟(业务扩展类 itemFunction) | # | 任务 | 备注 | |---|---|---| | 1 | itemFunction=5/6 VIP/行动力(**如果 Survivors 把它们作为 Item**,则这俩 handler 不需要,直接发 itemId) | | | 2 | itemFunction=4 CityBuff(如果 Survivors 有 buff 系统才做) | | | 3 | itemFunction=10 解锁迷雾(Survivors 无大地图,**跳过**) | | | 4 | itemFunction=12 召唤怪物(**跳过**) | | | 5 | itemFunction=13/14 工人/部队(SLG 专有,**跳过**) | | | 6 | itemFunction=30/31 迁城(**跳过**) | | | 7 | itemFunction=35 联盟积分(无联盟,**跳过**) | | ### P4 UI 集中实现 | # | 任务 | 对应 ROK | |---|---|---| | 1 | `BagPanel`(4 Tab:资源/装备/道具/材料) | `BagView` | | 2 | `BagItemUICommon` 通用道具显示组件 | `UI_Item_Bag` + `UI_Model_Item` | | 3 | `BagItemTooltip` 道具详情浮层 | `RefreshItemDetail` 内的详情面板 | | 4 | `GiftOpenPanel` 选包/兑换 UI | `BagGiftOpenView` | | 5 | `RewardGetPanel` 奖励展示弹窗 | `s_rewardGetWin` | | 6 | Tab 红点 | `m_pageReddotImgList` | ### P5 后续完善 | # | 任务 | |---|---| | 1 | 资源阈值告警(`GameWarning` 配置 + Alarm) | | 2 | 大额变动审计日志 | | 3 | 装备分解/合成(如果做装备系统) | | 4 | 装备图纸(如果做装备系统) | --- ## 附录 A:ROK 关键源代码位置索引 | 内容 | 路径 | |---|---| | 服务端 ItemLogic(addItem/delItem/giveReward/getItemPackage) | `Server/server/game_server/logic/lualib/ItemLogic.lua` | | 服务端 Item proxy(ItemUse/ItemChangeResource RPC handler) | `Server/server/game_server/logic/service/proxy/Item.lua` | | 服务端 Item 数据 service | `Server/server/game_server/logic/service/data/d_item.lua`(snax 服务,本次未深入) | | 道具枚举(ItemType/SubType/FunctionType/PackageType/BatchUse) | `Server/common/lualib/enum/ItemEnum.lua` | | CurrencyType 枚举(货币不是 Item) | `Server/common/lualib/enum/OtherEnum.lua` L101 | | ItemScript(**空壳**,无实质内容) | `Server/server/game_server/logic/lualib/itemscript/ItemScript.lua` | | Common.sproto(ItemInfo / RewardInfo / RewardItem) | `Server/common/protocol/Common.sproto` L308 / L381 / L362 | | Protocol.sproto(Item_ItemUse 852 / Item_ItemChangeResource 851 / Item_ItemInfo 30101) | `Server/common/protocol/Protocol.sproto` | | 服务端 s_Config 初始道具配置(initialItemType/Num) | `Server/common/config/gen/Configs.data` L13376(id=0 行) | | 客户端 BagProxy(增量同步/红点/装备/材料) | `Client/Assets/Scripts/Hotfix/MVC/Proxy/BagProxy.cs` | | 客户端 BagMediator(5 Tab + 使用按钮分支) | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Bag/BagMediator.cs` | | 客户端 BagGiftOpenMediator(选包/兑换 UI) | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/Bag/BagGiftOpenMediator.cs` | | 客户端 UI_Win_UseItemMediator(行动力/VIP 快捷使用) | `Client/Assets/Scripts/Hotfix/MVC/View_Mediator/UseItem/UI_Win_UseItemMediator.cs` | | 客户端 PlayerCmd.cs(Item_ItemUse 响应处理) | `Client/Assets/Scripts/Hotfix/MVC/CMD/PlayerCmd.cs` L38-128 | | 客户端 ItemDefine(25 字段) | `Client/Assets/Scripts/Hotfix/Config/ItemConfig.cs` | | 客户端 ItemPackageConfig / ItemRewardChoiceConfig 等 | `Client/Assets/Scripts/Hotfix/Config/Item*Config.cs`(7 个) | | 错误码 ITEM_ 前缀(8000-8009) | `Client/Assets/Scripts/Hotfix/MVC/CMD/ErrorCode.cs` L255-264 | | 客户端配置 Bin 文件 | `Client/Assets/StreamingAssets/Config/Bin/Item.bin`(87 KB) | ## 附录 B:未深入调研的边角 - `s_Item.lua` 的 ConfigEntity 加载机制(只看了 stub,没看 ConfigEntity 父类) - `MSM.d_item[rid]` snax 服务实现细节 - `Build_*` 装备制造/合成 RPC 详细参数(涉及 EquipDefine + EquipMaterialDefine 联动) - `Hero_ExchangeHeroItem` 等英雄相关道具用法(属于 Hero 模块) - 钻石(denar)的付费充值入口(RechargeLogic,属于充值模块) - 联盟礼物 `guildGifts` 在奖励组中的具体发放机制(属于 Guild 模块) - `LogLogic:itemChange` 的具体日志格式(输出到 log_server)