460 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tasks: Port ROK Hero & Bag System
> **交付策略**P1-P3 服务端业务先行,**用 GM 命令 + 日志验证****不写 UI**。所有 YIUI 面板集中在 P4 一次性补齐。每个新 RPC Handler MUST 同时新增对应 GM 命令。
>
> **复用原则**(详见 `Doc/ET-Packages-Audit.md`38 个 ET 包已覆盖 GM / 日志 / BSON / 协议 / 资源热更 / UI / 网络。本变更**严格遵守"能复用不重写"**:日志扩展 `Log` 类、GM 直接用 `[GM]`/`[ConsoleHandler]` 属性、BSON 复用 `MongoHelper`、登录扩展 `C2G_LoginGateHandler`。仅以下能力新建业务包:`db` / `logging` / `config` / `hero` / `bag` / `gacha`。
## 1. P0 - 基础设施准备
### 1.1 客户端编译产物
- [ ] 1.1.1 在 Unity 中执行 `ET/Loader/Compile` (F6) 生成客户端热更 DLL
- [x] 1.1.2 检查 `Packages/cn.etetet.loader/Bundles/Code/` 下存在 `ET.Model.dll.bytes` / `ET.Hotfix.dll.bytes` / `ET.ModelView.dll.bytes` / `ET.HotfixView.dll.bytes`
- [ ] 1.1.3 执行 `ET/HybridCLR/Init` 初始化 AOT 元数据(如未执行过)
### 1.2 服务端编译产物
- [ ] 1.2.1 执行 `dotnet build "My project/ET.sln" -c Debug` 编译服务端
- [x] 1.2.2 检查 `My project/Bin/ET.App.dll` 存在
- [x] 1.2.3 编写 `tools/build-server.ps1` 一键编译脚本
### 1.3 跑通 Login Demo
- [ ] 1.3.1 在 Unity Build Settings 中添加 `Packages/cn.etetet.loader/Scenes/Init.unity`
- [ ] 1.3.2 启动 ET Server (单进程模式)`ET/Loader/ServerTools` → "Start Server"
- [ ] 1.3.3 启动 Unity Play验证 `C2R_Login``R2C_Login``C2G_LoginGate``G2C_LoginGate` 链路打通
- [ ] 1.3.4 验证 Gate Scene 创建了 `Player` Entity 并关联 `Session`
### 1.4 DBComponent 业务封装(**基于现成 MongoHelper + MongoDB.Driver**
- [x] 1.4.1 **复用确认**`cn.etetet.core/MongoHelper` 已有 BSON 序列化、`com.etetet.init/Plugins/MongoDB` 已有原生 DLL仅缺业务层封装
- [x] 1.4.2 新建 `Packages/cn.etetet.db/` 包目录与 package.json依赖 cn.etetet.core
- [x] 1.4.3 定义 `IDBComponent` 接口Save<T>/Query<T>/QueryByEntityId<T>/Update<T>/Delete<T>
- [x] 1.4.4 实现 `MongoDBComponent`:用 `IMongoClient` 直连 + `MongoHelper.ToBson/FromBson` 序列化Collection 命名约定 = `typeof(T).Name`
- [x] 1.4.5 实现 `MemoryDBComponent``Dictionary<long, byte[]> + MongoHelper.Clone`)开发期默认
- [x] 1.4.6 在 Gate Scene 启动时根据 `StartConfig.DBConnection` 二选一挂载
- [x] 1.4.7 实现 `DBSaveComponent`(脏标记 + 30 秒定时批量 Save + Player 下线强制 Flush
- [x] 1.4.8 添加 `Entity.SetDirty()` 扩展方法(写入 dirty set
- [ ] 1.4.9 单元测试save → query → update → delete 闭环(内存模式 + Mongo 模式各一遍)
### 1.5 Player 数据加载(**扩展现有 `cn.etetet.login`,不重写**
- [x] 1.5.1 **复用确认**`C2G_LoginGateHandler` 已有,目前只创建内存 Player扩展点 = 在 `await CreatePlayer(...)` 后插入 DB 加载分支
- [x] 1.5.2 在 `cn.etetet.login/Hotfix/Server/Gate/C2G_LoginGateHandler.cs` 加入 `await DBHelper.LoadOrCreatePlayer(player, dbComponent)` 调用(约 5-10 行)
- [ ] 1.5.3 新建 `Packages/cn.etetet.config/Datas/InitialResource.xlsx`(首次登录发放 RewardEntry 列表)
- [ ] 1.5.4 实现 `DBHelper.LoadOrCreatePlayer`DB 查 → 有则反序列化挂 Hero/Bag/Gacha Component无则新建并按 InitialResource 配置调 `BagComponentSystem.AddItems`
- [x] 1.5.5 注册 Session Dispose 事件(用 ET 现有 EventSystem玩家离线时 `DBSaveComponent.FlushNow(playerId)`
- [ ] 1.5.6 验证:登录 → DB 中存在 → 重启服务端 → 二次登录数据完整恢复
### 1.6 协议工具链
- [ ] 1.6.1 验证 `ET/Proto/Proto2CS` 工具可正常运行
- [x] 1.6.2 在 `Packages/cn.etetet.proto/Proto/` 新建占位 `HeroOuter_C_3000.proto`(仅含 1 个 ping 消息测试)
- [ ] 1.6.3 运行 Proto2CS 验证代码生成到 `CodeMode/Model/{Client|Server|ClientServer}/HeroOuter_C_3000.cs`
- [ ] 1.6.4 添加新消息后客户端能正常发送、服务端 Handler 能接收
- [x] 1.6.5 给所有 C2G/C2R 消息基类加入 `reqId` 字段uuid 字符串,便于日志全链路追踪)
### 1.7 Luban 配置工具链(**替代 ExcelExporter**
- [ ] 1.7.1 下载 luban 工具dotnet luban放到 `tools/luban/`
- [x] 1.7.2 新建 `Config/Defines/Root.bean.xml` + `Config/Datas/` 目录结构
- [x] 1.7.3 编写 `tools/luban/gen.ps1` 一键导出脚本(生成 C# 到 `Packages/cn.etetet.config/Scripts/Model/Share/cfg/`json 到 `Bundles/Config/`
- [ ] 1.7.4 写一张示例表 `ItemConfig.xlsx` + `Item.bean.xml`(含 itemId / itemType / name / icon / useType / maxStack / quality / desc / 通用 RewardEntry 字段示例)
- [ ] 1.7.5 跑通 gen.ps1 → 生成代码 + json 文件
- [ ] 1.7.6 在 YooAsset `AssetBundleCollectorSetting``Bundles/Config/` 收集规则
- [x] 1.7.7 新建 `Packages/cn.etetet.config/` 包,实现 `ConfigComponent` 挂在 Scene 上加载 Luban Tables
- [x] 1.7.8 实现 `SceneExtensions.Cfg()` 扩展方法
- [ ] 1.7.9 服务端 + 客户端 都能调 `scene.Cfg().TbItem.Get(1)` 查到金币配置
- [x] 1.7.10 编写 Luban 使用文档 `Doc/Luban-Guide.md`
### 1.8 日志扩展(**基于现成 `Log` + NLog仅加扩展方法**
- [x] 1.8.1 **复用确认**`cn.etetet.core/Log.cs` 已有 `Log.Debug/Info/Warning/Error/Trace``cn.etetet.loader` 已集成 NLog 后端;**不重写不替换**
- [x] 1.8.2 新建 `Packages/cn.etetet.logging/` 包(依赖 cn.etetet.core + cn.etetet.loader
- [x] 1.8.3 在新包内加扩展方法:`Log.Audit(module, action, payload)` / `Log.Slow(module, action, durationMs, ctx)` / `Log.Module(tag, level, msg)`**全部走 ET 现有 Log 入口,仅做格式化前缀**
- [x] 1.8.4 提供 `NLog.config.template`3 个 target 分别输出 `Logs/Business/` / `Logs/Audit/{module}/` / `Logs/Error/`jsonl 行格式)+ rolling by day
- [x] 1.8.5 提供 `LoggingInstaller`:服务端启动时合并 template 到现有 `Packages/cn.etetet.loader/Scripts/Loader/Server/NLog.config`
- [x] 1.8.6 实现 `Log.Audit` 同步落盘NLog `<target type="AsyncWrapper">` 关闭 + `KeepFileOpen="true"` 强制 flush
- [x] 1.8.7 约定模块标签前缀:`[Hero]` `[Bag]` `[Gacha]` `[Equip]` `[DB]` `[Login]` `[GM]` `[Net]`**仅写文档约定,不做编译期校验**
- [x] 1.8.8 实现 RPC Handler 基类 `RpcMessageHandler<TReq,TResp>`:在 OnRun 前后自动 `Log.Info($"[{module}] {action} in: ..." )` + 耗时计算(**业务 Handler 改继承即可,老 Handler 不强制**
- [x] 1.8.9 实现 reqId 透传:在 C2G/C2R proto 基类加 `reqId` 字段(**任务 1.6.5 已包含**+ Handler 基类把 reqId 放到 `AsyncLocal<string>`,扩展 `Log.Info` 自动拼前缀
- [x] 1.8.10 实现慢操作告警:包装 `IDBComponent` 入口 + `MessageDispatcher`超阈值DB 查询 100ms / 写入 200ms / Handler 500ms`Log.Slow`
- [x] 1.8.11 实现错误码降频:同 playerId 同 errorCode 1 分钟内仅首条 + 计数(用 `MemoryCache` 1 分钟 TTL
- [ ] 1.8.12 用 1.9 注册 `gm SetLogLevel <module> <level>` 运行时调级(写 `NLogManager.Configuration.LoggingRules`
- [ ] 1.8.13 实现敏感字段脱敏(配置化 MaskFieldsproto attribute 标记或 Json 路径配置)
- [ ] 1.8.14 实现客户端日志桥接 `C2G_ClientLogReport` + 服务端 `Logs/Client/{date}.log`默认开发期开启Release 关闭)
- [ ] 1.8.15 日志单测:扩展方法格式正确 / 模块过滤 / 同步落盘验证
### 1.9 GM 业务命令接入(**完全复用 `cn.etetet.yiuigm` + `cn.etetet.console`,零框架开发**
- [x] 1.9.1 **复用确认**
- `cn.etetet.yiuigm`:已有 `[GM]` 属性 + `IGMCommand` 接口 + 完整 GMPanel UI参数 6 类型)
- `cn.etetet.console`:已有 `[ConsoleHandler]` 属性 + `IConsoleHandler` 接口 + stdin REPL
- **不新建 GM 框架**,仅按这两个属性写业务 GM 类
- [x] 1.9.2 新建文档 `Doc/GM-Commands.md`:约定服务端 console 用 `cn.etetet.console`、客户端 panel 用 `cn.etetet.yiuigm`、共享业务实现走 `GMHelper` 静态类
- [x] 1.9.3 新建 `cn.etetet.gachainvoke` / `cn.etetet.heroinvoke` / `cn.etetet.baginvoke` 跨包调用契约(用 `cn.etetet.yiuiinvoke`),让 GM 类不需要直接依赖业务包
- [x] 1.9.4 在 `cn.etetet.logging` 提供 `Log.AuditGM(operator, action, targetPlayer, args, success)` 专用方法,所有 GM 业务类调用前自动审计
- [ ] 1.9.5 用 `#if ENABLE_GM` 包裹所有 `[GM]` 业务类(参考 `cn.etetet.yiuigm/Scripts/HotfixView/Client/GM/GM_Command_Test.cs` 已有的条件编译模式)
- [x] 1.9.6 编写 CI 校验脚本 `tools/ci/check-release-no-gm.ps1`Release IL 不含 `EGMType` 字符串
- [x] 1.9.7 编写 CI 校验脚本 `tools/ci/check-rpc-has-gm.ps1`:扫描每个 `*Handler.cs`,确认对应 `GM_*.cs``[ConsoleHandler]` 类存在
### 1.10 错误码体系
- [x] 1.10.1 新建 `HeroBagErrorCode.cs` 集中定义所有 Hero/Bag/Equipment/Gacha 错误码数值段位约定3xxxx Hero、4xxxx Bag、5xxxx Equip、6xxxx Gacha
- [x] 1.10.2 客户端添加 `ErrorMessageHelper.GetMessage(int errorCode)` 本地化文案;**展示走** `cn.etetet.yiuitips``TipsHelper.OpenSync<TipsTextViewComponent>(scene, ErrorMessageHelper.GetMessage(code))`**禁止自写 Toast**
- [x] 1.10.3 服务端 Handler 返回错误码时自动写 L3 错误日志
### 1.11 通用 Item 基础设施(**整个系统的基础**
- [x] 1.11.1 在 Luban `Common.bean.xml` 定义 `RewardEntry { itemId, count }` / `CostEntry { itemId, count }`
- [x] 1.11.2 在 proto `Common_3000.proto` 定义同名 message与 Luban 一致)
- [x] 1.11.3 在 `BagComponentSystem` 上预留 `AddItems(rewards)` / `RemoveItems(costs)` / `GetItemCount(itemId)` / `HasItems(costs)` 接口签名
- [ ] 1.11.4 在 Luban `ItemConfig` 中提前定义货币 itemId1=金币、2=钻石、3=PvP币写入 Datas/ItemConfig.xlsx 首批条目)
- [x] 1.11.5 写设计文档约定:**全游戏所有可获得资源必须用 itemId 表示,禁止在 Player Entity 上加货币字段**
### 1.12 GM Web Console策划/产品可见)— **基于 console + http 包,最小化新增**
- [x] 1.12.1 **复用确认**:服务端 stdin REPL 已现成(`cn.etetet.console``cn.etetet.http` 已现成轻量 HTTP Server
- [x] 1.12.2 新建 `tools/gm-web/index.html`input + fetch + 命令历史)作为静态资源
- [x] 1.12.3 实现 `[HttpHandler("/gm")] HttpGMHandler`:转发 POST body 到 `ConsoleComponentSystem` 命令分发器(**直接复用现有 console 命令注册**
- [ ] 1.12.4 简单密码校验StartConfig 配 `GMWebPassword`
- [x] 1.12.5 默认仅 Debug 包加载Release 不监听端口
### 1.13 ROK 配置迁移工具链(**新任务:取代手工录入**
- [x] 1.13.1 **背景沉淀**:在 `Doc/ROK-Config-Source.md` 记录数据源真相ROK 服务端只有 `Configs.data`Lua 表tabtoy 生成ROK 客户端有加密 SQLite + 205 个 `*Define.cs` DTO**无 Excel 源文件**
- [x] 1.13.2 拉取姐妹工程 `E:/Game/gmd/Tools/export_hero_json_from_configs_data.py` 作为基础(已验证可解析 Hero/HeroLevel/HeroStar
- [x] 1.13.3 在 `tools/rok-migrate/export.py` 扩展支持本次需要的表Hero/HeroLevel/HeroStar/HeroSkill/HeroTalent/Item/Equip/GachaPool/GachaWeight 等)
- [x] 1.13.4 在 `tools/rok-migrate/define_to_bean.py` 写 Roslyn-free 文本扫描器:读 `ROK/Client/.../*Define.cs` → 提取字段名 + 类型 → 输出 Luban `*.bean.xml`
- [x] 1.13.5 在 `tools/rok-migrate/json_to_luban_xlsx.py` 写 JSON → Luban `*.xlsx` 转换器(首行类型 / 二行字段名 / 三行注释 / 数据行)
- [x] 1.13.6 一键脚本 `tools/rok-migrate/run.ps1`bean.xml + xlsx 一次出齐到 `Config/Defines/``Config/Datas/`
- [ ] 1.13.7 跑通 Hero + Item 两张表端到端:`run.ps1``tools/luban/gen.ps1` → 服务端 `scene.Cfg().TbHero.Get(10001)` 拿到 ROK 真实数据
- [x] 1.13.8 文档化:每张表的 ROK 源字段 ↔ Luban bean 字段映射表
---
## 2. P1 - 抽卡 + 通用 Item + 英雄基础(**纯服务端 + GM 验证,零 UI**
> 执行顺序调整2026-05-27 用户确认):**先跳过 2.0 抽卡**,优先做 2.1+ 英雄/背包抽卡并入“酒馆Tavern发奖”语义后再回填 2.0。
### 2.0 抽卡系统(**已延期:并入酒馆 Tavern 语义后再落地**
#### 2.0.1 抽卡 Luban 配置
- [ ] 2.0.1.1 在 `Config/Defines/Gacha.bean.xml` 定义 `GachaPool` beanid / name / singleCost / multiCost / guaranteeCount / guaranteeRare / multiGuaranteeRare / openTime / closeTime
- [ ] 2.0.1.2 定义 `GachaWeightItem` beanpoolId / entryIndex / reward: RewardEntry / weight / rare
- [ ] 2.0.1.3 创建 `Config/Datas/GachaPool.xlsx` + `GachaWeight.xlsx`填充测试卡池1 个普池(消耗金币)+ 1 个高级池(消耗钻石)
- [ ] 2.0.1.4 跑 gen.ps1 验证代码生成 + json 输出
#### 2.0.2 抽卡协议
- [ ] 2.0.2.1 创建 `Packages/cn.etetet.gacha/Proto/GachaOuter_C_3200.proto`
- [ ] 2.0.2.2 定义 DTO`GachaPoolStats { poolId, totalCount, guaranteeRemain }`
- [ ] 2.0.2.3 定义 RPC`C2G_GachaDraw { poolId, drawType }` / `R2C_GachaDraw { error, rewards: repeated RewardEntry, isNewHero: repeated bool }`
- [ ] 2.0.2.4 定义推送:`Gacha_GachaInfo { stats: repeated GachaPoolStats }`
- [ ] 2.0.2.5 运行 Proto2CS 生成代码
#### 2.0.3 服务端 GachaComponent
- [ ] 2.0.3.1 创建 `cn.etetet.gacha/Scripts/Model/Server/GachaComponent`(挂在 Player 下)
- [ ] 2.0.3.2 含 `Dictionary<int, GachaPoolStats> PoolStats`,标记 `[MongoElement]` 持久化
- [ ] 2.0.3.3 实现 `GachaComponentSystem.Draw(poolId, drawType)` 完整算法(消耗→随机→保底→发奖→更新统计→审计日志)
- [ ] 2.0.3.4 实现 `UpdateGuarantee(poolId, rare)` 保底计数更新
- [ ] 2.0.3.5 实现 `ReplaceWithGuaranteed(rewards, weights, minRare)` 强制保底
- [ ] 2.0.3.6 实现 `RandomNumberGenerator` 工具类CSPRNG `System.Security.Cryptography.RandomNumberGenerator`**禁止使用 UnityEngine.Random**
- [ ] 2.0.3.7 抽卡过程中**严格走** `BagComponentSystem.RemoveItems` 扣消耗 + `AddItems` 发奖励,禁止绕过
#### 2.0.4 服务端 Handler + 日志
- [ ] 2.0.4.1 `C2G_GachaDrawHandler`:继承自带埋点的 RpcMessageHandler 基类(入口/出口自动 L1 日志)
- [ ] 2.0.4.2 抽卡完成后写 L2 审计日志playerId / poolId / drawType / cost / rewards / guaranteeRemain / reqId
- [ ] 2.0.4.3 推送 R2C_GachaDraw + Gacha_GachaInfo
- [ ] 2.0.4.4 接口限速:单玩家 100ms 最多 1 次请求,超过返回 ERR_RATE_LIMIT 并写 L3
- [ ] 2.0.4.5 服务端单元测试:模拟 10000 次抽卡,验证概率分布与配置误差 < 1%
- [ ] 2.0.4.6 服务端单元测试连续 90 1 次不出保底品质 90 次必出
- [ ] 2.0.4.7 服务端单元测试自然抽到保底品质 计数重置
#### 2.0.5 客户端 GachaComponent
- [ ] 2.0.5.1 创建 `cn.etetet.gacha/Scripts/Model/Client/GachaComponent`
- [ ] 2.0.5.2 实现 `Gacha_GachaInfoHandler` 同步保底进度
- [ ] 2.0.5.3 发布 `GachaUpdatedEvent`
#### 2.0.6 抽卡 GM 命令(**P1 唯一验证手段**
- [ ] 2.0.6.1 `gm Gacha <poolId> <times>` 触发抽卡默认 1 可指定 N
- [ ] 2.0.6.2 `gm SimulateGacha <poolId> <times>` 服务端模拟 N 次抽卡不修改真实玩家数据输出概率统计
- [ ] 2.0.6.3 `gm ShowGachaStats [playerId]` 输出玩家所有卡池的累计/保底进度
- [ ] 2.0.6.4 `gm ResetGachaGuarantee <poolId> [playerId]` 重置保底计数
- [ ] 2.0.6.5 `gm SetGachaCount <poolId> <count> [playerId]` 直接设置抽数测试用
#### 2.0.7 抽卡验收GM + 日志)
- [ ] 2.0.7.1 `gm AddItem 2 100` `gm Gacha 1 1` `gm ShowBag` 钻石 -1奖励入包
- [ ] 2.0.7.2 `gm Gacha 1 10` 收到 10 个奖励钻石按十连消耗扣除
- [ ] 2.0.7.3 `gm SetGachaCount 1 89` `gm Gacha 1 1` 必出保底品质查审计日志确认
- [ ] 2.0.7.4 自然抽到保底品质 `gm ShowGachaStats` 显示保底计数重置为 0
- [ ] 2.0.7.5 不同卡池保底独立抽卡池 A 不影响卡池 B 进度
- [ ] 2.0.7.6 `gm SimulateGacha 1 10000` 概率与配置误差 < 1%
- [ ] 2.0.7.7 `gm Snapshot before` `gm Gacha 1 10` 重启 `gm DumpPlayer` 对比 数据完整恢复
- [ ] 2.0.7.8 篡改 reqId / 客户端伪造字段 日志记录但服务端结果不受影响
- [ ] 2.0.7.9 **端到端验证**`gm Gacha 1 100` 抽到英雄碎片 `gm SummonHero` 招募成功
### 2.1 英雄 Luban 配置
- [x] 2.1.1 `Config/Defines/Hero.bean.xml` 定义 `Hero` bean List<int> skillIds、List<int> talentIds、fragmentItemId、recruitLimit 等)
- [x] 2.1.2 定义 `HeroLevel` / `HeroSkill` / `HeroSkillEffect` / `HeroSkillLevel` bean
- [ ] 2.1.3 在 bean 中使用 `List<RewardEntry>` / `List<CostEntry>` 表达消耗与奖励
- [x] 2.1.4 填充测试数据:每张表 5 个条目
- [ ] 2.1.5 跑 gen.ps1 验证客户端/服务端读到 `scene.Cfg().TbHero.Get(heroId)`
### 2.2 英雄协议
- [x] 2.2.1 创建 `Packages/cn.etetet.hero/Proto/HeroOuter_C_3000.proto`
- [x] 2.2.2 定义 DTO`HeroInfo``SkillInfo`
- [x] 2.2.3 定义 RPC`C2G_SummonHero` / `C2G_HeroAddExp` / `C2G_HeroSkillUp` 及响应
- [x] 2.2.4 定义推送:`Hero_HeroInfo``Hero_HeroInfoList`
- [ ] 2.2.5 运行 Proto2CS
### 2.3 服务端 Hero 实体与持久化
- [x] 2.3.1 创建 `Hero` Entity含 level/exp/star/starExp/skills/talents
- [x] 2.3.2 创建 `HeroComponent`(挂在 Player 下)
- [x] 2.3.3 实现 Awake/Destroy + AddHero / RemoveHero / GetHero / GetAllHeroes
- [x] 2.3.4 给所有持久化字段加 `[MongoElement]`
- [ ] 2.3.5 验证:登录后服务端能加载玩家所有 Hero 实体
### 2.4 服务端业务 Handler + 日志
- [x] 2.4.1 `C2G_SummonHeroHandler`:校验碎片(`BagComponentSystem.HasItems`+ 不重复 + 上限 → AddHero + `BagComponentSystem.RemoveItems` 扣碎片
- [x] 2.4.2 `C2G_HeroAddExpHandler`:校验道具 → 累加经验 → 触发升级 → 受星级上限约束
- [x] 2.4.3 `C2G_HeroSkillUpHandler`:校验道具 → 技能等级 +1 → 受 maxLevel 约束
- [x] 2.4.4 `HeroNoticeHelper.NotifyHeroUpdate(player, hero)` 封装推送
- [x] 2.4.5 登录后自动推送 `Hero_HeroInfoList`
- [x] 2.4.6 所有 Handler 走 RpcMessageHandler 基类自动埋点
- [x] 2.4.7 关键操作(招募、首次满级、首次满技能)写 L2 审计
### 2.5 客户端 Hero 缓存
- [x] 2.5.1 创建客户端 `HeroComponent` 缓存
- [x] 2.5.2 实现 `Hero_HeroInfoHandler` 增量更新 / `Hero_HeroInfoListHandler` 全量同步
- [x] 2.5.3 发布 `HeroUpdatedEvent` / `HeroListUpdatedEvent`
### 2.6 战力计算(客户端 + 服务端共享)
- [x] 2.6.1 在 `cn.etetet.hero/Scripts/Model/Share/HeroPowerCalculator.cs` 实现 GetBaseScore / GetLevelScore / GetSkillScore / GetPower
- [x] 2.6.2 单元测试:相同数据 → 客户端与服务端算出相同战力
### 2.7 英雄 GM 命令(**P1 唯一验证手段**
- [x] 2.7.1 `gm AddHero <heroId>` 直接创建英雄
- [x] 2.7.2 `gm SetHeroLevel <heroId> <level>` 设置等级
- [x] 2.7.3 `gm SetHeroSkill <heroId> <skillIndex> <level>` 设置技能等级
- [x] 2.7.4 `gm ShowHeroes [playerId]` 输出 jsonl
- [x] 2.7.5 `gm SummonHero <heroId>` 走完整招募流程(验证碎片消耗逻辑)
- [x] 2.7.6 `gm HeroAddExp <heroId> <itemId> <count>` 走完整升级流程
### 2.8 P1 验收GM + 日志,零 UI
- [ ] 2.8.1 `gm AddItem <碎片itemId> 30``gm SummonHero <heroId>` → 招募成功 + 碎片扣 30
- [ ] 2.8.2 `gm HeroAddExp <heroId> <expBookId> 100` → 等级提升 + 经验书减少
- [ ] 2.8.3 升级技能 → 技能等级 +1 → 战力变化(用 `gm ShowHeroes` 对比)
- [ ] 2.8.4 重启客户端 + 服务端 → `gm DumpPlayer` 输出对比 → 数据完整恢复
- [ ] 2.8.5 审计日志:`tail Logs/Audit/Hero/2026-*.jsonl` 含完整招募/升级记录
- [ ] 2.8.6 错误日志:尝试用 0 碎片招募 → L3 日志含错误码 + reqId
---
## 3. P2 - 背包与装备穿戴(**服务端 + GM 验证,零 UI**
### 3.1 背包/装备 Luban 配置
- [ ] 3.1.1 完善 `Config/Defines/Item.bean.xml`(含 BagItemType 枚举、useType 枚举)
- [ ] 3.1.2 定义 `Equip` bean含 List<AttrEntry> 属性、equipItemType 枚举)
- [ ] 3.1.3 填充约 30 个测试道具 + 10 个测试装备到 xlsx
- [ ] 3.1.4 跑 gen.ps1 验证
### 3.2 背包/装备协议
- [x] 3.2.1 创建 `Packages/cn.etetet.bag/Proto/BagOuter_C_3100.proto`
- [x] 3.2.2 定义 DTO`ItemInfo``EquipSlot`
- [x] 3.2.3 定义 RPC`C2G_UseItem` / `C2G_HeroWearEquip` / `C2G_TakeOffEquip`
- [x] 3.2.4 定义推送:`Item_ItemInfo``Item_ItemInfoList``Item_RemoveItem`
- [ ] 3.2.5 运行 Proto2CS
### 3.3 服务端 BagComponent**通用 Item 入口**
- [x] 3.3.1 创建 `cn.etetet.bag/Scripts/Model/Server/BagComponent`
- [x] 3.3.2 创建 `ItemUnit` 子 Entity含 itemIndex / itemId / count / heroId
- [x] 3.3.3 实现 `AddItem(itemId, count)` - 自动堆叠(非装备类)/ 新建条目(装备类)
- [x] 3.3.4 实现 `RemoveItem(itemId, count)``RemoveItemByIndex(itemIndex)`
- [x] 3.3.5 实现 `SetHeroIdOnEquip(itemIndex, heroId)`
- [x] 3.3.6 实现 `AddItems(List<RewardEntry>)` / `RemoveItems(List<CostEntry>)` / `HasItems(List<CostEntry>)` 批量接口(事务性:任一失败全失败)
- [x] 3.3.7 实现 `GetItemCount(itemId)` 通用查询
- [x] 3.3.8 所有变更通过 `BagNoticeHelper.NotifyItemChange` 推送
- [x] 3.3.9 资源大额变动(>1000写 L2 审计日志
- [x] 3.3.10 玩家首次登录通过 `InitialResourceConfig``AddItems` 发初始金币/钻石(**走通用接口**
### 3.4 服务端业务 Handler
- [ ] 3.4.1 `C2G_UseItemHandler`:通用 dispatcher按 ItemConfig.useType 分发
- [x] 3.4.2 `C2G_HeroWearEquipHandler`:校验装备 + 卸旧穿新 + 同步 Hero + Item
- [x] 3.4.3 `C2G_TakeOffEquipHandler`:清空槽位 + 装备 heroId = 0
- [x] 3.4.4 登录后推送 `Item_ItemInfoList`
### 3.5 客户端 BagComponent
- [x] 3.5.1 创建客户端 `BagComponent`(按 6 大类分类缓存)
- [x] 3.5.2 实现 `Item_ItemInfoHandler` / `Item_ItemInfoListHandler`
- [ ] 3.5.3 实现新道具标记本地存储OctoberStudio SaveManager 集成)
- [x] 3.5.4 发布 `ItemChangedEvent` / `BagUpdatedEvent`
### 3.6 背包/装备 GM 命令
- [x] 3.6.1 `gm AddItem <itemId> <count>` 通用发放(已在 P0/P1 准备)
- [x] 3.6.2 `gm RemoveItem <itemId> <count>` 通用扣除
- [x] 3.6.3 `gm ShowBag [playerId]` 输出 jsonl
- [x] 3.6.4 `gm BagClear [playerId]` 清空背包(保留绑定装备)
- [x] 3.6.5 `gm HeroWearEquip <heroId> <itemIndex>` 穿戴
- [x] 3.6.6 `gm HeroTakeOffEquip <heroId> <slotIndex>` 卸下
- [x] 3.6.7 `gm UseItem <itemIndex> [count]` 使用道具
### 3.7 P2 验收GM + 日志)
- [ ] 3.7.1 `gm AddItem <装备itemId> 1``gm ShowBag` 出现 → 查属性
- [ ] 3.7.2 `gm HeroWearEquip``gm ShowHeroes` 看战力变化
- [ ] 3.7.3 `gm HeroTakeOffEquip` → 装备回背包未绑定状态
- [ ] 3.7.4 切换英雄穿同一装备 → 自动从原英雄卸下
- [ ] 3.7.5 重启验证装备穿戴关系持久化
- [ ] 3.7.6 通用 Item 测试:金币、钻石、碎片、装备、材料都用 `gm AddItem` 发放,全部走 BagComponent.AddItems
---
## 4. P3 - 高级养成(**服务端 + GM 验证,零 UI**
### 4.1 升星系统
- [ ] 4.1.1 Luban 配置 `HeroStar` / `HeroStarExp` bean
- [ ] 4.1.2 proto `C2G_HeroStarUp` / `R2C_HeroStarUp`
- [ ] 4.1.3 服务端 Handler扣升星材料 + 幸运计算 + 星级提升
- [ ] 4.1.4 GM 命令:`gm HeroStarUp <heroId>` 走完整流程,`gm SetHeroStar <heroId> <star>` 直接设置
- [ ] 4.1.5 单元测试:连续升星,验证幸运系数概率分布
### 4.2 觉醒系统
- [ ] 4.2.1 给 Hero Entity 增加 `IsAwakening` 字段
- [ ] 4.2.2 proto `C2G_HeroAwake`
- [ ] 4.2.3 服务端 Handler校验前 4 技能满级 + 扣觉醒材料 + 解锁第 5 技能
- [ ] 4.2.4 GM 命令:`gm HeroAwake <heroId>` / `gm ForceAwake <heroId>`(跳过校验,测试用)
### 4.3 天赋系统
- [ ] 4.3.1 Luban 配置 `HeroTalent` / `HeroTalentTree` / `HeroTalentMastery` bean
- [ ] 4.3.2 给 Hero Entity 增加 `TalentIndex` + 3 套 `TalentTreeData`
- [ ] 4.3.3 proto `C2G_TalentUp` / `C2G_ChangeTalentIndex` / `C2G_ResetTalent`
- [ ] 4.3.4 服务端 Handler扣点数 + 节点等级 +1 + 前置依赖校验
- [ ] 4.3.5 GM 命令:`gm TalentUp <heroId> <nodeId>` / `gm TalentReset <heroId>` / `gm ChangeTalentIndex <heroId> <index>`
### 4.4 装备锻造
- [ ] 4.4.1 Luban 配置扩展:`Equip` bean 增加 `List<CostEntry> requireMaterials` / `List<RewardEntry> resolveRewards`
- [ ] 4.4.2 proto `C2G_MakeEquipment` / `C2G_ResolveEquipment` / `C2G_MixMaterial`
- [ ] 4.4.3 服务端 Handler扣材料 + 创建装备 / 分解装备 + 返还
- [ ] 4.4.4 GM 命令:`gm MakeEquip <equipId>` / `gm ResolveEquip <itemIndex>` / `gm MixMaterial <materialId>`
### 4.5 P3 验收
- [ ] 4.5.1 `gm HeroStarUp` 重复使英雄升至 5 星
- [ ] 4.5.2 前 4 技能升满 → `gm HeroAwake` 成功 → 第 5 技能可用
- [ ] 4.5.3 `gm TalentUp` 学习天赋 → `gm ChangeTalentIndex` 切换天赋页 → `gm TalentReset` 重置
- [ ] 4.5.4 `gm MakeEquip` 用图纸 + 材料锻造装备 → 装备出现在背包
- [ ] 4.5.5 `gm ResolveEquip` 分解装备 → 材料返还
- [ ] 4.5.6 全流程审计日志完整:每个操作都有对应 jsonl 记录
---
## 5. P4 - UI 集中实现(**深度复用 YIUI 全套子包,零造轮**
> **前提**P0-P3 服务端业务已稳定,所有协议、错误码、推送已定型。
>
> **强制约束**(详见 `Doc/ET-Packages-Audit.md` 中 "YIUI 框架" 一节):
> - 面板打开/关闭**只用** `scene.YIUIRoot().OpenPanelAsync<T>()`**禁止自建 PanelHelper**
> - 长列表**只用** `YIUILoopScrollChild.SetDataRefresh`**禁止自写 ScrollRect 池化**
> - 按钮事件**只用** `UIEventBind*` + `[YIUIInvoke]`**禁止 `Button.onClick.AddListener`**
> - 数据 → UI 刷新**只用** `u_Data*.SetValue` + `UIDataBind*`**禁止手写 Refresh 样板**
> - 弹窗**只用** `TipsHelper.OpenWait/OpenSync`**禁止自写 ConfirmDialog/Toast**
> - 红点**只在叶子节点 `SetCount`**,父节点 DAG 自动汇总
### 5.1 YIUI 通用组件准备
- [ ] 5.1.1 复用现有 `CommonHeader` + `YIUICloseCommon`(不再造顶栏/关闭)
- [ ] 5.1.2 创建 `BagItemUICommon.prefab`:图标用 `UIDataBindImage` + `u_DataIcon`、数量用 `UIDataBindText` + `u_DataCount`、品质边框用 `UIDataBindSprite` + `u_DataQualityFrame`、新道具标记用 `UIDataBindActive` + `u_DataNew`、不可用用 `UIDataBindGray` + `u_DataEnabled``cn.etetet.yiuieffect`
- [ ] 5.1.3 `BagItemUICommonComponentSystem.Show(itemConfigId, count, options)` 入口:内部全走 `u_Data*.SetValue`**禁止手写 `Image.sprite = xxx`**
- [ ] 5.1.4 多尺寸预设:`BagItemUICommon_Small/Mid/Large.prefab` 三个变体(仅 RectTransform 不同),共享同一 Component 代码
- [ ] 5.1.5 点击事件统一发布 `BagItemClickedEvent`ET EventSystem让宿主 Panel 处理
### 5.2 抽卡 UI
- [ ] 5.2.1 用 YIUI 自动化工具生成 `GachaPanel` Component + System
- [ ] 5.2.2 Prefab 用 `UIEventBindClick` + `[YIUIInvoke]` 接线(单抽/十连/切池按钮);按钮可用性用 `UIDataBindGray` + `u_DataCanDraw`(钻石不足自动灰显)
- [ ] 5.2.3 保底进度用 `UIDataBindSlider` + `u_DataGuaranteeProgress` + `UIDataBindText` + `u_DataGuaranteeText`
- [ ] 5.2.4 卡池列表用 `cn.etetet.yiuiloopscrollrectasync``YIUILoopScrollChild.SetDataRefresh(poolList)`
- [ ] 5.2.5 RPC 期间挡点击:`scene.YIUIMgr().BanLayerOptionForever()` 开 → 收响应 close 关
- [ ] 5.2.6 抽卡 CD 用 `CountDownMgr``cn.etetet.yiuiframework`**禁止自建 Timer**
- [ ] 5.2.7 `GachaResultPanel`10 个奖励槽用 `BagItemUICommon` 渲染;新英雄高亮用 `UIEffect` 发光 + `UIParticle` 粒子(`cn.etetet.yiuieffect`
- [ ] 5.2.8 "确认消耗"用 `TipsHelper.OpenWait<TipsMessageViewComponent>(scene, "消耗 X 钻石?")` 模态等待
- [ ] 5.2.9 抽卡完成飘字用 `TipsHelper.OpenSync<TipsTextViewComponent>(scene, "招募成功")`
### 5.3 英雄 UI
- [ ] 5.3.1 `HeroListPanel` 长列表用 `YIUILoopScrollChild.SetDataRefresh(heroList)` + `ReRenderer` 增量刷新
- [ ] 5.3.2 三段分组(已拥有 / 可召唤 / 未集齐):在数据层 IList 中插入 Header Item**不改 Loop 框架**
- [ ] 5.3.3 `HeroListItem` 字段(等级/星级/战力/碎片数)全用 `u_Data*` + `UIDataBind*`
- [ ] 5.3.4 SortType 切换:`UIEventBindToggleGroup` + 重排 IList → `Loop.ReRenderer()`
- [ ] 5.3.5 `HeroDetailPanel`3D 模型用 `YIUI3DDisplayChild.ShowAsync("HeroModel_10001")``cn.etetet.yiui3ddisplay`**禁止自建 RT/Camera**
- [ ] 5.3.6 升级/升星/技能/觉醒 按钮 → `UIEventBindClick` + `[YIUIInvoke]` → RPC
- [ ] 5.3.7 召唤按钮带"消耗 N 碎片"模态:`TipsHelper.OpenWait`
### 5.4 背包 UI
- [ ] 5.4.1 `BagPanel` 6 Tab + `YIUILoopScrollChild`(每 Tab 复用同一 Loop切 Tab 仅换 IList
- [ ] 5.4.2 道具 Tooltip自建 `BagItemTooltipView`**tips 包无现成 Tooltip View**),容器走 `TipsHelper.Open<BagItemTooltipViewComponent>(scene, itemId)` 承载
- [ ] 5.4.3 Tab 切换:`UIEventBindToggleGroup` 自动响应 + `Loop.SetDataRefresh(filteredList)`
- [ ] 5.4.4 点击 Item → 发布 `BagItemClickedEvent` → Panel 弹 Tooltip → "使用 / 穿戴"按钮走 RPC
- [ ] 5.4.5 操作确认用 `TipsHelper.OpenWait`;操作结果飘字用 `TipsHelper.OpenSync`
### 5.5 装备穿戴 UI
- [ ] 5.5.1 `HeroDetailPanel` 8 装备槽:每个槽 = `BagItemUICommon` + `UIEventBindClick`
- [ ] 5.5.2 `EquipSelectPanel``YIUILoopScrollChild` 列表 + 按 SubType 过滤 IList
- [ ] 5.5.3 点击空槽位 → `OpenPanelParamAsync<EquipSelectPanelComponent>(slotIndex, subType)`
- [ ] 5.5.4 点击已穿 → `TipsHelper.OpenWait` "卸下 / 替换 / 取消"三选一
### 5.6 天赋 UI
- [ ] 5.6.1 `HeroTalentPanel` 天赋树可视化:**YIUI 无图编辑器组件**,需自绘(普通 RectTransform 拼节点 + LineRenderer 连线)
- [ ] 5.6.2 节点状态用 `UIDataBindActive`(已点亮)+ `UIDataBindGray`(不可点)
- [ ] 5.6.3 切换 / 重置交互:`UIEventBindClick` + `[YIUIInvoke]` + 二次确认走 `TipsHelper.OpenWait`
### 5.7 锻造 UI
- [ ] 5.7.1 `EquipForgePanel` 3 Tab`UIEventBindToggleGroup`
- [ ] 5.7.2 材料 / 产物用 `BagItemUICommon` 渲染
- [ ] 5.7.3 锻造按钮:足够材料时正常,不足时 `UIDataBindGray` 灰显
- [ ] 5.7.4 成功后 `TipsHelper.OpenSync` 飘字 + `UIParticle` 粒子动画
### 5.8 红点系统(**完全复用 `cn.etetet.yiuireddot`**
- [ ] 5.8.1 扩展 `ERedDotKeyType` 枚举HeroCanUpgrade / HeroCanStarUp / HeroCanAwake / TalentHasPoint / BagHasNew / BagHasUsable / GachaHasFree / EquipCanForge 等
- [ ] 5.8.2 在 `UIRedDotConfigDAG` 编辑器配置父子关系HeroEntry = 4 个 Hero* 之和BagEntry = Bag* 之和MainMenu = HeroEntry + BagEntry + GachaEntry + EquipForge
- [ ] 5.8.3 业务侧**只写叶子**`RedDotMgr.Inst.SetCount(ERedDotKeyType.HeroCanUpgrade, count)`**禁止给中间节点 SetCount**
- [ ] 5.8.4 Panel / Button 上挂 `RedDotBind` 选 Key**不写代码**自动显隐
- [ ] 5.8.5 红点检查逻辑统一封装到 `RedDotChecker.RefreshAll()`,业务变化时调用
### 5.9 UI 与 OctoberStudio 集成
- [ ] 5.9.1 在 OctoberStudio Main Menu 添加"英雄"/"背包"/"抽卡"三个按钮(普通 Button因 OctoberStudio 不是 YIUI 体系)
- [ ] 5.9.2 按钮 OnClick → `scene.YIUIRoot().OpenPanelAsync<HeroListPanelComponent>()`**不写 PanelHelper**
- [ ] 5.9.3 处理"未联网"状态:按钮 OnClick 前检查 Session → 未连接时 `TipsHelper.OpenSync<TipsTextViewComponent>(scene, "请先登录")`
### 5.10 P4 验收
- [ ] 5.10.1 全功能 UI 走查:抽卡 → 招募 → 升级 → 穿戴 → 升星 → 觉醒 → 天赋 → 锻造
- [ ] 5.10.2 UI 操作与 GM 命令产生相同的业务结果
- [ ] 5.10.3 性能:背包列表 1000+ 道具滑动 60 FPS`YIUILoopScrollChild` 池化保证)
- [ ] 5.10.4 红点正确显示与清除(叶子 SetCount → 父节点自动汇总)
- [ ] 5.10.5 **代码扫描验证零造轮**CI 脚本 `tools/ci/check-no-ui-wheel.ps1` 校验业务包内不含 `Button.onClick.AddListener` / `Image.sprite =` / `Resources.Load` / 自写 ScrollRect 池化)
---
## 6. 跨阶段任务
### 6.1 单元测试与集成测试
- [ ] 6.1.1 服务端 Handler 单元测试覆盖率 ≥ 60%
- [ ] 6.1.2 客户端 Component 缓存逻辑测试
- [ ] 6.1.3 协议序列化往返测试
- [ ] 6.1.4 DBComponent 持久化测试(内存模式 + MongoDB 模式各跑一遍)
- [ ] 6.1.5 端到端测试:登录 → 抽卡 → 招募 → 升级 → 穿戴 → 退出 → 重新登录验证
- [ ] 6.1.6 抽卡概率回归测试10000 次抽卡,各品质实际概率与配置误差 < 1%
- [ ] 6.1.7 抽卡保底回归测试200 保底触发次数与理论值匹配
### 6.2 文档
- [x] 6.2.1 `Doc/Luban-Guide.md` Luban 使用文档
- [x] 6.2.2 `Doc/GM-Commands.md` 所有 GM 命令列表 + 用法
- [x] 6.2.3 `Doc/Logging-Guide.md` 日志体系说明 + grep/jq 示例
- [x] 6.2.4 `Doc/Hero-System-Guide.md` 业务文档
- [x] 6.2.5 `Doc/Bag-System-Guide.md`
- [x] 6.2.6 `Doc/Server-Setup.md`如何起服 + mongo + 编译
- [x] 6.2.7 在工程根 `README.md` 中添加新业务模块入口指引
### 6.3 性能与监控
- [ ] 6.3.1 验证慢操作告警实际生效构造 100ms+ DB 操作触发 L3 日志
- [ ] 6.3.2 验证单服务器 100 个模拟玩家并发登录 + 业务操作不崩溃
- [ ] 6.3.3 客户端背包列表 1000+ 道具滑动 60 FPSLoopScrollRect 确保
- [ ] 6.3.4 日志写盘性能测试1000 QPS 业务日志 + 100 QPS 审计日志稳定
### 6.4 CI 校验
- [x] 6.4.1 `tools/ci/check-release-no-gm.ps1` Release IL 不含 GM 代码
- [x] 6.4.2 `tools/ci/check-rpc-has-gm.ps1` 每个 RPC Handler 有对应 GM 命令
- [x] 6.4.3 `tools/ci/check-logging.ps1` 每个 Handler 必含日志埋点继承 RpcMessageHandler 或显式声明
- [x] 6.4.4 `tools/ci/check-no-ui-wheel.ps1` 业务包 `cn.etetet.hero/bag/gacha` 内禁止出现 `Button.onClick.AddListener` / `Image.sprite =` / `Resources.Load` / 手写 `ScrollRect` 池化等 YIUI 已覆盖能力的造轮代码
- [x] 6.4.5 `openspec validate --strict` CI 中跑
### 6.5 上线前检查
- [ ] 6.5.1 所有 GM 命令仅 Debug 包启用Release 包剥离 + IL 校验通过
- [x] 6.5.2 错误码本地化覆盖中文 + EN
- [x] 6.5.3 数据备份策略MongoDB 每日自动备份脚本
- [x] 6.5.4 服务端日志切割与归档L2 OSS
- [ ] 6.5.5 HybridCLR IL2CPP 包下验证`ET/HybridCLR/Generate/All`
- [ ] 6.5.6 概率审计所有卡池跑 SimulateGacha 10000 截图保留