feat: initial XtGameKit (hero/bag/tavern/config/db/invoke packages + design docs + et-framework-dev skill)
This commit is contained in:
commit
910477d015
274
.cursor/skills/et-framework-dev/SKILL.md
Normal file
274
.cursor/skills/et-framework-dev/SKILL.md
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
---
|
||||||
|
name: et-framework-dev
|
||||||
|
description: >-
|
||||||
|
Develop ET-Framework + YIUI + Luban + Proto + HybridCLR business code in the
|
||||||
|
Survivors Unity project. Use when adding/editing any code under
|
||||||
|
`My project/Packages/cn.etetet.*`, creating new packages, adding RPC/Proto
|
||||||
|
messages, defining Entity/Component, writing GM commands, or touching
|
||||||
|
Hotfix/HotfixView/Model/ModelView assemblies. Enforces the ET source
|
||||||
|
generator rules (ET0001~ET0032), assembly-layout (asmref) discipline, Proto
|
||||||
|
workflow, EntityRef usage, and the MCP-Unity debug loop.
|
||||||
|
---
|
||||||
|
|
||||||
|
# ET Framework Development — Survivors Project
|
||||||
|
|
||||||
|
ET 的 Roslyn 分析器和程序集结构非常严格,**违反规则会一次性触发几十个编译错误**。先按下面的"动手前必查"走一遍,再写代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 动手前必查(每次都先做这 5 件事)
|
||||||
|
|
||||||
|
```
|
||||||
|
- [ ] 1. 看包是否激活:包目录下 Scripts/**/AssemblyReference.asmref 必须齐全,
|
||||||
|
Ignore.*.asmdef 必须保持 IGNORE constraint(否则会顶替 asmref 失效)。
|
||||||
|
- [ ] 2. 看 Proto 是否生成:cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs 是否存在;
|
||||||
|
缺则跑 ET/Proto/Proto2CS 菜单。
|
||||||
|
- [ ] 3. 看每个新类放到了正确程序集层:
|
||||||
|
- 静态工具 / 占位 DTO → Model + [EnableClass]
|
||||||
|
- Entity/Component → Model/Server 或 Model/Client
|
||||||
|
- System / Service / Handler / Helper(业务)→ Hotfix
|
||||||
|
- UI 数据绑定 / 工具 → ModelView + [EnableClass]
|
||||||
|
- YIUI 事件 / GM 命令 → HotfixView
|
||||||
|
- [ ] 4. 看是否有静态字段:Hotfix/HotfixView 程序集禁止任何非 const 字段和属性。
|
||||||
|
- [ ] 5. 看静态工具类是否互相调用:BagNoticeHelper ↔ BagComponentSystem 这种环
|
||||||
|
会触发 ET0013,要拆成单向依赖。
|
||||||
|
```
|
||||||
|
|
||||||
|
把这 5 条贴到 todo 里,写代码前一项一项打勾。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET 分析器铁律(高频踩坑顺序)
|
||||||
|
|
||||||
|
| ID | 规则 | 怎么改 |
|
||||||
|
|---|---|---|
|
||||||
|
| **ET0001** | `AddChild<T>` 的 T 必须在 `[ChildOf]` 声明里 | Entity 类加 `[ChildOf(typeof(ParentEntity))]` |
|
||||||
|
| **ET0002** | 访问别人的 Entity 字段需 `[FriendOf(typeof(T))]` | 在调用方类上加 `[FriendOf(typeof(Hero))]` 等 |
|
||||||
|
| **ET0003** | Entity 子类不能写构造函数 / 不能 `new` | 用 `AddChild`/`AddComponent` 创建 |
|
||||||
|
| **ET0004** | **Hotfix/HotfixView 程序集禁止非 const 字段和属性**(包括 `=> "..."` 的 expression-bodied property)| 字段挪到 Model 程序集 + `[EnableClass]`;属性删掉,用 `typeof(T).Name` 之类直接 inline |
|
||||||
|
| **ET0013** | 静态类间环依赖 | 让一边 inline 内嵌另一边的小逻辑,打破环 |
|
||||||
|
| **ET0015** | static 字段必须 `[StaticField]` | 加 attribute;但若同时在 Hotfix 会与 ET0004 冲突 → 必须挪走 |
|
||||||
|
| **ET0024/0025** | `EntitySystem` 必须在 `[EntitySystemOf]` 标记的 partial 类里 | `[EntitySystemOf(typeof(Hero))] partial class HeroSystem` |
|
||||||
|
| **ET0030** | NetMessage 必须从 Proto 生成 | 不要手写 Message 类 |
|
||||||
|
| **ET0031** | 禁止 `new XxxMessage()`(Proto 类) | 用 `XxxMessage.Create()` 工厂方法 |
|
||||||
|
| **ET0032** | Model/ModelView 程序集禁止非 Entity 类(除非加 `[EnableClass]`) | 静态工具类、占位 DTO 加 `[EnableClass]` |
|
||||||
|
|
||||||
|
完整规则解读 → 见 [analyzer-rules.md](analyzer-rules.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 程序集分层硬规则
|
||||||
|
|
||||||
|
每个 ET 包 `cn.etetet.<name>` **必须**在子目录放 `AssemblyReference.asmref`,否则代码不会参与编译(一个隐藏到不报错的"代码静默失踪"陷阱)。
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.mypkg/
|
||||||
|
├── Ignore.ET.MyPkg.asmdef # 占位 asmdef(IGNORE constraint,不参与编译)
|
||||||
|
├── Proto/ # 仅放 .proto,由 Proto2CS 产 cs 到 cn.etetet.proto
|
||||||
|
├── Scripts/
|
||||||
|
│ ├── Model/
|
||||||
|
│ │ ├── Share/AssemblyReference.asmref → ET.Model
|
||||||
|
│ │ ├── Client/AssemblyReference.asmref → ET.Model
|
||||||
|
│ │ └── Server/AssemblyReference.asmref → ET.Model
|
||||||
|
│ ├── Hotfix/
|
||||||
|
│ │ ├── Share/AssemblyReference.asmref → ET.Hotfix
|
||||||
|
│ │ ├── Client/AssemblyReference.asmref → ET.Hotfix
|
||||||
|
│ │ └── Server/AssemblyReference.asmref → ET.Hotfix
|
||||||
|
│ ├── ModelView/Client/AssemblyReference.asmref → ET.ModelView
|
||||||
|
│ └── HotfixView/Client/AssemblyReference.asmref → ET.HotfixView
|
||||||
|
```
|
||||||
|
|
||||||
|
`AssemblyReference.asmref` 内容固定为 `{ "reference": "ET.<ProgramAssembly>" }`。
|
||||||
|
|
||||||
|
**临时屏蔽整个包**:把该包 `Scripts/**/AssemblyReference.asmref`(含 .meta)全删,保留 `Ignore.*.asmdef`。本次会话就用这招屏蔽了 `cn.etetet.gacha`。
|
||||||
|
|
||||||
|
**判断要放哪一层**(最常错的决策):
|
||||||
|
|
||||||
|
| 代码类型 | 放哪 | 原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| Entity / Component 定义 | `Model/Server` 或 `Model/Client` | Model 程序集只允许 Entity 派生类 |
|
||||||
|
| Entity 的 `System`(Awake/Update/Destroy)| `Hotfix/Server`(带 `[EntitySystemOf]`)| 业务逻辑层 |
|
||||||
|
| Service / Helper / Mapper / Handler | `Hotfix/Server` 或 `Hotfix/Client` | 业务工具 |
|
||||||
|
| Proto 消息类 | **不要自己写**,跑 Proto2CS | 自动生成到 `cn.etetet.proto/CodeMode/` |
|
||||||
|
| 普通占位 DTO / Luban 生成类 / Lock 对象 | `Model/Share` + `[EnableClass]` | 需要持有静态状态时必须在 Model |
|
||||||
|
| YIUI 视图组件 / UIBindCDETable 派生 | `ModelView/Client` | YIUI 框架约定 |
|
||||||
|
| YIUI 事件 handler / GM 命令 / 客户端 RPC handler | `HotfixView/Client` | 视图层业务 |
|
||||||
|
|
||||||
|
更多分层细节 → 见 [assembly-layout.md](assembly-layout.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proto 工作流(不跑等于不存在)
|
||||||
|
|
||||||
|
ET 不直接编译 `.proto`,必须先跑生成器。
|
||||||
|
|
||||||
|
**1. 命名规则**(Proto2CS 用文件名解析 opcode):
|
||||||
|
|
||||||
|
```
|
||||||
|
<Name>_<C|S|CS>_<startOpcode>.proto
|
||||||
|
例:HeroOuter_C_3000.proto # C=客户端服务端通用,opcode 从 3000 开始递增
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. RPC 注解**(缺一个就不识别为 RPC):
|
||||||
|
|
||||||
|
```proto
|
||||||
|
// ResponseType R2C_SummonHero
|
||||||
|
message C2G_SummonHero // ISessionRequest
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
string reqId = 2;
|
||||||
|
int32 configId = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message R2C_SummonHero // ISessionResponse
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
int32 Error = 2;
|
||||||
|
string Message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 RPC 单向通知用这个
|
||||||
|
message Hero_HeroInfo // IMessage
|
||||||
|
{
|
||||||
|
HeroInfo hero = 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 生成步骤**:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 写完 .proto,Unity 菜单 ET → Proto → Proto2CS
|
||||||
|
2. 生成结果在 cn.etetet.proto/CodeMode/Model/{Client,Server,ClientServer}/<Name>.cs
|
||||||
|
3. Assets → Refresh,让 Unity 索引到新文件
|
||||||
|
4. recompile_scripts 或 F6
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. 用 MCP 一键触发**:
|
||||||
|
|
||||||
|
```
|
||||||
|
execute_menu_item({"menuPath": "ET/Proto/Proto2CS"})
|
||||||
|
execute_menu_item({"menuPath": "Assets/Refresh"})
|
||||||
|
recompile_scripts({"waitForCompletion": true})
|
||||||
|
```
|
||||||
|
|
||||||
|
更多 → 见 [proto-workflow.md](proto-workflow.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EntityRef\<T\> 使用规则
|
||||||
|
|
||||||
|
`EntityRef<T>` 是 ET 的弱引用包装结构体,**编译器不会自动 unwrap**。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 编译错误 CS1061
|
||||||
|
if (bag.ItemMap.TryGetValue(idx, out EntityRef<ItemUnit> unit))
|
||||||
|
{
|
||||||
|
unit.ItemId // EntityRef<ItemUnit> 没有 ItemId 字段
|
||||||
|
unit.Dispose() // 也没有 Dispose
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 用隐式转换拆包
|
||||||
|
if (bag.ItemMap.TryGetValue(idx, out EntityRef<ItemUnit> unitRef))
|
||||||
|
{
|
||||||
|
ItemUnit unit = unitRef; // 触发 implicit operator T(EntityRef<T> v)
|
||||||
|
unit.ItemId
|
||||||
|
unit.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Linq 场景
|
||||||
|
self.ItemMap.Values
|
||||||
|
.Select(x => (ItemUnit)x) // 显式 cast 也会调 implicit operator
|
||||||
|
.Where(x => x.ItemId == 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP-Unity 调试循环
|
||||||
|
|
||||||
|
调试代码不要手动开 Unity 看日志,用 MCP 闭环。
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 改完代码 → CallMcpTool execute_menu_item Assets/Refresh
|
||||||
|
2. CallMcpTool recompile_scripts {"waitForCompletion": true}
|
||||||
|
3. 若 errors > 0 → 看 logs 列表,分类批量修
|
||||||
|
4. errors == 0 → CallMcpTool execute_menu_item "Tools/Survivors/Open Init Scene And Play"
|
||||||
|
5. CallMcpTool get_console_logs {"types": ["error", "warning"], "count": 50}
|
||||||
|
```
|
||||||
|
|
||||||
|
**MCP 连接失败时**:不要立刻重试,先确认 Unity 编辑器没卡在编译/导入(往往是上一次 Refresh 还在跑)。等 5-10s 再发请求。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新建一个业务包的标准流程
|
||||||
|
|
||||||
|
参考 cn.etetet.hero / cn.etetet.bag 的结构。**写代码前先把骨架建好**。
|
||||||
|
|
||||||
|
```
|
||||||
|
Task Progress:
|
||||||
|
- [ ] 1. 拷贝 cn.etetet.statesync 作为模板,改包名
|
||||||
|
- [ ] 2. 删掉 Runtime/Model 下的 ET.Model.asmdef(保留 Ignore.*.asmdef 即可)
|
||||||
|
- [ ] 3. 保留 / 重建以下 7 个 AssemblyReference.asmref:
|
||||||
|
Scripts/Model/{Share,Client,Server}, Scripts/Hotfix/{Share,Client,Server},
|
||||||
|
Scripts/ModelView/Client, Scripts/HotfixView/Client(按需)
|
||||||
|
- [ ] 4. package.json dependencies 加 cn.etetet.core / login / logging / console / bag 等
|
||||||
|
- [ ] 5. Proto/<Pkg>_C_<opcode>.proto 写消息
|
||||||
|
- [ ] 6. ET → Proto → Proto2CS
|
||||||
|
- [ ] 7. Model 层写 Entity,配 [ChildOf]/[ComponentOf]
|
||||||
|
- [ ] 8. Hotfix 层写 System([EntitySystemOf])/ Service / Handler
|
||||||
|
- [ ] 9. recompile_scripts 验证 0 错误
|
||||||
|
- [ ] 10. 写 GM 命令到 HotfixView/Client/GM 验证业务
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已知 demo 噪音(看到时直接忽略,不要去修)
|
||||||
|
|
||||||
|
当前 `GlobalConfig.SceneName = StateSync`,走 `cn.etetet.statesync` + `cn.etetet.yiuistatesync` 这两个**仅参考** demo 包的流程。这两个错每次都会出现,**不影响业务**:
|
||||||
|
|
||||||
|
| 错误关键字 | 触发时机 | 来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| `actor id is 0` @ `MessageLocationSenderComponentSystem.cs:67` | 走 demo 大厅 → 进入地图 (EnterMapAsync) | statesync M2M 跨地图转移弱实现 |
|
||||||
|
| `cant set parent because parent iSence is null: CoroutineLockComponent` | 按停止键退出 Play | demo `LobbyPanelComponentSystem.OnEventEnterMapInvoke` 在 OnApplicationQuit 后还在 await CloseAsync |
|
||||||
|
|
||||||
|
排查规则:先看错误栈是否在 `cn.etetet.statesync` / `cn.etetet.yiuistatesync` 内部。是的话直接跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 这次会话踩过的坑(不要再犯)
|
||||||
|
|
||||||
|
1. **新包没 asmref → 代码静默失踪**。报错全在引用方报"找不到类型",而不是说"包没编译"。第一时间检查 asmref。
|
||||||
|
2. **Proto 写好不跑 Proto2CS → 类不存在**。F6 不会触发,必须走菜单。
|
||||||
|
3. **Hotfix 里写 `protected virtual string Module => "Net"`** → ET0004 把属性也当字段。要么挪到 Model + `[EnableClass]`,要么完全删除。
|
||||||
|
4. **`static readonly Dictionary` 当查找表** → Hotfix 禁。改 `switch` 表达式,或挪到 Model。
|
||||||
|
5. **`StringSplitOptions.TrimEntries`** → Unity 的 .NET Standard 2.1 不支持。用 `Split(' ', RemoveEmptyEntries).Select(x => x.Trim())`。
|
||||||
|
6. **`new HeroInfo { ... }`** → ET0031。用 `HeroInfo.Create()` + 逐字段赋值。
|
||||||
|
7. **`bag.ItemMap.TryGetValue(.., out EntityRef<T> u); u.Field`** → CS1061。先 `T t = u;`。
|
||||||
|
8. **Helper 之间互调形成环** → ET0013。让低层 helper 内联自己的小逻辑,不调上层。
|
||||||
|
9. **登录后 GM 面板没自定义命令** → 先查 `cn.etetet.<pkg>/Scripts/HotfixView/Client/GM/*.cs` 是否在 HotfixView 程序集(必须有 asmref),再查 `[GM(EGMType.Common, ...)]` 标签是否正确。
|
||||||
|
10. **MCP 调用超时** → Unity 还在 Import/Compile,先等 5-10s。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 链路速记(启动到 GM 出现)
|
||||||
|
|
||||||
|
```
|
||||||
|
Init.unity (cn.etetet.loader)
|
||||||
|
→ Entry.Start (cn.etetet.core)
|
||||||
|
→ FiberInit_Main (SceneType 由 GlobalConfig.SceneName 决定)
|
||||||
|
→ EntryEvent1/2/3
|
||||||
|
→ EntryEvent3_InitClient (cn.etetet.yiuistatesync, SceneType.StateSync)
|
||||||
|
→ YIUIMgrComponent.Initialize()
|
||||||
|
→ YIUIEventInitializeAfter
|
||||||
|
→ YIUIEventInitializeAfterGMHandler (cn.etetet.yiuigm)
|
||||||
|
→ GMCommandComponent.Awake → 扫 GMAttribute → 显示在 GM 面板
|
||||||
|
```
|
||||||
|
|
||||||
|
要让自定义 GM 出现:cs 文件**必须**位于 `<pkg>/Scripts/HotfixView/Client/...`(HotfixView 程序集),并标 `[GM(EGMType.XXX, 1, "标题", "tooltip")]`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细参考
|
||||||
|
|
||||||
|
- [analyzer-rules.md](analyzer-rules.md) — ET0001~ET0032 全规则解读 + 触发示例 + 修复模板
|
||||||
|
- [assembly-layout.md](assembly-layout.md) — 程序集分层完整规则、asmref 模板、新建包步骤
|
||||||
|
- [proto-workflow.md](proto-workflow.md) — Proto 命名、注解、生成、MCP 一键流程
|
||||||
248
.cursor/skills/et-framework-dev/analyzer-rules.md
Normal file
248
.cursor/skills/et-framework-dev/analyzer-rules.md
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
# ET Source Generator 分析器规则
|
||||||
|
|
||||||
|
源码位置:`My project/Packages/cn.etetet.sourcegenerator/DotNet~/ET.SourceGenerator/Analyzer/`
|
||||||
|
|
||||||
|
只对**新写或新改**的代码生效。下面按"踩坑频率从高到低"排序。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0004 — Hotfix 程序集禁止非 const 字段(含属性)
|
||||||
|
|
||||||
|
**最爱挖坑的一条**。HotfixProjectFieldDeclarationAnalyzer 把所有 **属性** 也当成"非const字段"报错。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ Hotfix 程序集
|
||||||
|
public abstract class RpcMessageHandler<TReq, TResp> : MessageSessionHandler<TReq, TResp>
|
||||||
|
{
|
||||||
|
protected virtual string Module => "Net"; // ET0004: 字段: Module
|
||||||
|
protected virtual string Action => typeof(TReq).Name; // ET0004: 字段: Action
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Hotfix 程序集
|
||||||
|
public static class LogExtensions
|
||||||
|
{
|
||||||
|
private static readonly object AuditLock = new(); // ET0004 + ET0015
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 修复 1:完全去掉属性,在 Run 里 inline
|
||||||
|
public abstract class RpcMessageHandler<TReq, TResp> : MessageSessionHandler<TReq, TResp>
|
||||||
|
{
|
||||||
|
protected sealed override async ETTask Run(Session session, TReq request, TResp response)
|
||||||
|
{
|
||||||
|
string action = typeof(TReq).Name;
|
||||||
|
Log.Info($"[Net] {action} in");
|
||||||
|
await OnRun(session, request, response);
|
||||||
|
}
|
||||||
|
protected abstract ETTask OnRun(Session session, TReq req, TResp resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 修复 2:挪到 Model + [EnableClass],Model 程序集不被本规则约束
|
||||||
|
// 文件路径从 Scripts/Hotfix/Share/ 移到 Scripts/Model/Share/
|
||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
[EnableClass]
|
||||||
|
public static class LogExtensions
|
||||||
|
{
|
||||||
|
private static readonly object AuditLock = new(); // ✅ Model 允许
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
适用程序集:`ET.Hotfix`、`ET.HotfixView`、`ET.Hotfix.Test`、`ET.HotfixView.Test`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0002 — Entity 字段访问需 FriendOf
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class BagComponent : Entity, IAwake
|
||||||
|
{
|
||||||
|
public Dictionary<long, EntityRef<ItemUnit>> ItemMap = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 触发 ET0002
|
||||||
|
public static class BagService
|
||||||
|
{
|
||||||
|
public static bool TryUseItem(Player player, long idx)
|
||||||
|
{
|
||||||
|
var bag = player.GetComponent<BagComponent>();
|
||||||
|
bag.ItemMap.TryGetValue(...); // 错:跨类访问字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public static class BagService
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**属性不算字段**。`ItemUnit` 用 `{ get; set; }` 属性,外部访问无需 FriendOf。
|
||||||
|
**子类访问父类**字段无需 FriendOf,本规则只针对**跨类访问**。
|
||||||
|
**FriendOf 写在使用方类上**,不是被使用的类。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0031 — 禁止 new Proto / Message 类
|
||||||
|
|
||||||
|
`MessageObject`(Proto 生成)走对象池,必须用工厂:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌
|
||||||
|
HeroInfo hero = new() { heroId = 1 };
|
||||||
|
Hero_HeroInfo notice = new() { hero = info };
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
HeroInfo hero = HeroInfo.Create();
|
||||||
|
hero.heroId = 1;
|
||||||
|
|
||||||
|
Hero_HeroInfo notice = Hero_HeroInfo.Create();
|
||||||
|
notice.hero = info;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0032 — Model/ModelView 禁止非 Entity 类
|
||||||
|
|
||||||
|
Model 程序集理论上只放 Entity / Component / Proto / `struct`。普通工具类/DTO 要么挪走,要么加 `[EnableClass]`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌ 在 Scripts/Model/Share/ 下
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
public sealed class MongoDBComponent : IDBComponent { ... } // ET0032
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[EnableClass]
|
||||||
|
public sealed class MongoDBComponent : IDBComponent { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实战上经常用 Model 层放 `static` 工具:因为 Hotfix 不允许静态字段,需要持有锁/字典之类的静态状态时**只能**放在 Model + `[EnableClass]`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0013 — 静态类间环依赖
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌
|
||||||
|
public static class BagComponentSystem
|
||||||
|
{
|
||||||
|
public static void AddItem(this BagComponent self, ...) {
|
||||||
|
BagNoticeHelper.NotifyItemChange(...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BagNoticeHelper
|
||||||
|
{
|
||||||
|
public static void NotifyItemList(BagComponent bag) {
|
||||||
|
bag.GetAllItems(); // 调到 BagComponentSystem,形成环
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 让 BagNoticeHelper 内联自己的小逻辑,不再调上层
|
||||||
|
public static class BagNoticeHelper
|
||||||
|
{
|
||||||
|
public static void NotifyItemList(BagComponent bag) {
|
||||||
|
foreach (var unitRef in bag.ItemMap.Values) {
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
list.items.Add(BagMapper.ToProto(unit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
排查:报错信息会列出所有 `A -> B` 和 `B -> A` 的调用点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0015 — static 字段必须 `[StaticField]`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌
|
||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
private static int counter; // ET0015
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
[StaticField]
|
||||||
|
private static int counter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Hotfix 程序集里 ET0015 + ET0004 会同时触发,唯一出路是**挪到 Model**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ET0024 / ET0025 — EntitySystem 必须在 partial 类里
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ❌
|
||||||
|
public static class HeroSystem
|
||||||
|
{
|
||||||
|
private static void Awake(this Hero self) { ... } // ET0024
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅
|
||||||
|
[EntitySystemOf(typeof(Hero))]
|
||||||
|
[FriendOf(typeof(Hero))]
|
||||||
|
public static partial class HeroSystem
|
||||||
|
{
|
||||||
|
[EntitySystem]
|
||||||
|
private static void Awake(this Hero self, int configId)
|
||||||
|
{
|
||||||
|
self.ConfigId = configId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ET 用源码生成器在 partial 的另一半生成 `HeroAwakeSystem` 类。**忘记 `partial`** 是常见错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 其他规则(出错概率低,简要列出)
|
||||||
|
|
||||||
|
| ID | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| ET0001 | `AddChild<T>` 的 T 必须在 `[ChildOf]` 声明里 |
|
||||||
|
| ET0003 | Entity 子类不能写构造函数 |
|
||||||
|
| ET0005 | Hotfix 类必须 static |
|
||||||
|
| ET0006 | Entity 类不能写实例方法(要写成扩展方法在 System 里) |
|
||||||
|
| ET0007 | Component 必须有 `[ComponentOf]` 声明 |
|
||||||
|
| ET0008 | 同步方法里不能调 `await ETTask` |
|
||||||
|
| ET0009 | async 方法里不能丢弃 ETTask |
|
||||||
|
| ET0010 | 不允许 delegate 字段 |
|
||||||
|
| ET0011 | UniqueId 范围检查 |
|
||||||
|
| ET0012 | UniqueId 重复 |
|
||||||
|
| ET0014 | Entity 内不能用 child component |
|
||||||
|
| ET0016~0019 | ETCancellationToken 链路检查 |
|
||||||
|
| ET0020 | Entity 字段声明位置 |
|
||||||
|
| ET0021 | async 方法返回类型必须 ETTask |
|
||||||
|
| ET0022 | Client 类不能在 Server 程序集 |
|
||||||
|
| ET0023 | LSEntity float 字段检查(确定性帧同步) |
|
||||||
|
| ET0026 | FiberLog 使用规则 |
|
||||||
|
| ET0027 | EntityHashCode 检查 |
|
||||||
|
| ET0028 | EntityComponentChild 检查 |
|
||||||
|
| ET0029 | Entity 不能声明泛型类型 |
|
||||||
|
| ET0030 | NetMessage 必须从 Proto 生成 |
|
||||||
|
| ET1001 | ETSystem 方法必须在 static partial 类 |
|
||||||
|
|
||||||
|
具体规则触发时去看对应分析器源码:
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.sourcegenerator/DotNet~/ET.SourceGenerator/Analyzer/<RuleName>.cs
|
||||||
|
cn.etetet.sourcegenerator/DotNet~/ET.SourceGenerator/Config/DiagnosticIds.cs
|
||||||
|
```
|
||||||
171
.cursor/skills/et-framework-dev/assembly-layout.md
Normal file
171
.cursor/skills/et-framework-dev/assembly-layout.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# ET 程序集分层与 asmref 完整规则
|
||||||
|
|
||||||
|
## 6 个核心程序集
|
||||||
|
|
||||||
|
| 程序集 | 由谁定义 | 装哪些代码 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ET.Core` | `cn.etetet.core/Runtime/Core/ET.Core.asmdef` | 框架基础,不可修改 |
|
||||||
|
| `ET.Model` | `cn.etetet.statesync/Runtime/Model/ET.Model.asmdef` | Entity/Component 数据定义、Proto 消息类、占位/工具类(加 `[EnableClass]`) |
|
||||||
|
| `ET.ModelView` | `cn.etetet.statesync/Runtime/ModelView/ET.ModelView.asmdef` | YIUI 视图组件、UIBindCDETable 派生类 |
|
||||||
|
| `ET.Hotfix` | `cn.etetet.statesync/Runtime/Hotfix/ET.Hotfix.asmdef` | 业务 System / Service / Helper / Handler |
|
||||||
|
| `ET.HotfixView` | `cn.etetet.statesync/Runtime/HotfixView/ET.HotfixView.asmdef` | YIUI 事件、GM 命令、客户端 RPC handler |
|
||||||
|
| `ET.Loader` | `cn.etetet.loader/...` | 启动器 |
|
||||||
|
|
||||||
|
> ET.Model.asmdef 的 `defineConstraints` 是 `["INITED", "IS_COMPILING || UNITY_EDITOR"]` — 项目必须先被 ET 框架"初始化"过(写入 `INITED` symbol),所有 asmref 才会激活。新克隆的工程要先走一次 ET 初始化流程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## asmref:让你的包加入框架程序集
|
||||||
|
|
||||||
|
每个 `cn.etetet.<pkg>` 包**不能**自己定义 asmdef,必须用 asmref 把代码"塞"进上面 6 个程序集。
|
||||||
|
|
||||||
|
### 标准目录布局
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.mypkg/
|
||||||
|
├── package.json # 声明依赖
|
||||||
|
├── Ignore.ET.MyPkg.asmdef # 占位(IGNORE constraint)
|
||||||
|
├── Ignore.ET.MyPkg.asmdef.meta
|
||||||
|
├── Proto/ # 只放 .proto
|
||||||
|
│ └── MyPkgOuter_C_4000.proto
|
||||||
|
└── Scripts/
|
||||||
|
├── Model/
|
||||||
|
│ ├── Share/AssemblyReference.asmref # → ET.Model
|
||||||
|
│ ├── Client/AssemblyReference.asmref # → ET.Model
|
||||||
|
│ └── Server/AssemblyReference.asmref # → ET.Model
|
||||||
|
├── ModelView/
|
||||||
|
│ └── Client/AssemblyReference.asmref # → ET.ModelView
|
||||||
|
├── Hotfix/
|
||||||
|
│ ├── Share/AssemblyReference.asmref # → ET.Hotfix
|
||||||
|
│ ├── Client/AssemblyReference.asmref # → ET.Hotfix
|
||||||
|
│ └── Server/AssemblyReference.asmref # → ET.Hotfix
|
||||||
|
└── HotfixView/
|
||||||
|
└── Client/AssemblyReference.asmref # → ET.HotfixView
|
||||||
|
```
|
||||||
|
|
||||||
|
### AssemblyReference.asmref 内容(固定)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "reference": "ET.Model" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "reference": "ET.ModelView" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "reference": "ET.Hotfix" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "reference": "ET.HotfixView" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ignore.<Name>.asmdef 内容(固定)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Ignore.ET.MyPkg",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [],
|
||||||
|
"defineConstraints": ["IGNORE"],
|
||||||
|
"autoReferenced": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`defineConstraints: ["IGNORE"]` 让这个 asmdef 永远不参与编译 — 它的存在只是为了占位(避免 Unity 找不到 .meta),真正生效的是各子目录里的 asmref。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 哪个代码放哪一层(决策表)
|
||||||
|
|
||||||
|
| 你想写的代码 | 放哪 | 关键约束 |
|
||||||
|
|---|---|---|
|
||||||
|
| `class Hero : Entity, IAwake<int>` | `Scripts/Model/Server/Hero.cs` | 只能字段,不能写方法。需要 `[ChildOf(typeof(HeroComponent))]` |
|
||||||
|
| `[ComponentOf(typeof(Player))] class HeroComponent : Entity, IAwake` | `Scripts/Model/Server/HeroComponent.cs` | 同上 |
|
||||||
|
| 客户端缓存 `class HeroComponent : Entity, IAwake { Dictionary<long, HeroInfo> }` | `Scripts/Model/Client/HeroModels.cs` | 同名服务端类客户端可独立定义 |
|
||||||
|
| `[EntitySystemOf(typeof(Hero))] partial class HeroSystem { Awake/Update }` | `Scripts/Hotfix/Server/HeroSystem.cs` | partial,函数标 `[EntitySystem]` |
|
||||||
|
| `static class HeroService { TrySkillUp(player, hero) }` | `Scripts/Hotfix/Server/HeroService.cs` | 加 `[FriendOf(typeof(Hero))]` |
|
||||||
|
| `RpcMessageHandler<C2G_X, R2C_X>` 子类 | `Scripts/Hotfix/Server/C2G_XHandler.cs` | **不能**有非 const 字段/属性 |
|
||||||
|
| `MessageHandler<Scene, X>` 客户端 | `Scripts/Hotfix/Client/X_Handler.cs` | 同上 |
|
||||||
|
| 锁对象 / 字典 / 静态状态工具类 | `Scripts/Model/Share/` + `[EnableClass]` | 因为 Hotfix 不允许静态字段 |
|
||||||
|
| Proto 生成的 Message 类 | 不要自己写,跑 Proto2CS | 输出到 `cn.etetet.proto/CodeMode/` |
|
||||||
|
| YIUI 视图组件 `class XXXViewComponent : Entity` | `Scripts/ModelView/Client/XXX.cs` | |
|
||||||
|
| `[GM(EGMType.Common, ...)]` 命令 | `Scripts/HotfixView/Client/GM/X.cs` | 必须在 HotfixView 才被 GMAttribute 扫到 |
|
||||||
|
| `AEvent<Scene, YIUIEventInitializeAfter>` handler | `Scripts/HotfixView/Client/` | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 新建一个业务包:完整步骤
|
||||||
|
|
||||||
|
```
|
||||||
|
Task Progress:
|
||||||
|
- [ ] 1. 复制 cn.etetet.statesync 整个目录为 cn.etetet.<newpkg>
|
||||||
|
- [ ] 2. 删除 Runtime/ 下的 ET.*.asmdef 文件(这些是核心程序集,不能在新包里)
|
||||||
|
- [ ] 3. 改 package.json 的 name / displayName / dependencies
|
||||||
|
- [ ] 4. 修改 Ignore.ET.Statesync.asmdef → Ignore.ET.<NewPkg>.asmdef,name 字段同步改
|
||||||
|
- [ ] 5. 确保 Scripts/**/AssemblyReference.asmref 都还在(content 不需改)
|
||||||
|
- [ ] 6. 如果不需要 ModelView / HotfixView,删对应目录的 asmref(避免无意义编译)
|
||||||
|
- [ ] 7. Unity Assets/Refresh,确认 Console 无报错
|
||||||
|
- [ ] 8. 写 Proto → 跑 Proto2CS → 写 Model → 写 Hotfix
|
||||||
|
```
|
||||||
|
|
||||||
|
### package.json 依赖最小集
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "cn.etetet.mypkg",
|
||||||
|
"displayName": "ET.MyPkg",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"cn.etetet.core": "1.0.0",
|
||||||
|
"cn.etetet.login": "1.0.0",
|
||||||
|
"cn.etetet.logging": "1.0.0",
|
||||||
|
"cn.etetet.console": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
按需补:`cn.etetet.bag` / `cn.etetet.hero` / `cn.etetet.yiuigm` 等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 临时屏蔽 / 启用整个包
|
||||||
|
|
||||||
|
**屏蔽**:删除 `Scripts/**/AssemblyReference.asmref` 及对应 `.meta`,保留 `Ignore.*.asmdef`。包代码全部不参与编译。
|
||||||
|
|
||||||
|
**启用**:把 asmref 加回来,内容如上节模板。Unity 会自动识别。
|
||||||
|
|
||||||
|
本次会话用这招屏蔽了 `cn.etetet.gacha`:
|
||||||
|
|
||||||
|
```
|
||||||
|
删除:cn.etetet.gacha/Scripts/Hotfix/{Client,Server}/AssemblyReference.asmref
|
||||||
|
删除:cn.etetet.gacha/Scripts/Model/{Client,Server}/AssemblyReference.asmref
|
||||||
|
保留:cn.etetet.gacha/Ignore.ET.Gacha.asmdef
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见踩坑
|
||||||
|
|
||||||
|
1. **代码静默失踪**:新加的 cs 文件**所在目录上行**没有 asmref → 文件不参与任何程序集 → 调用方报"找不到类型"。检查方法:
|
||||||
|
|
||||||
|
```
|
||||||
|
Glob: cn.etetet.<pkg>/**/*.asmref
|
||||||
|
```
|
||||||
|
|
||||||
|
每个有 cs 文件的子目录必须有 asmref 上溯。
|
||||||
|
|
||||||
|
2. **方向错误**:把 Helper 写到 `Scripts/Model/Server/` → ET0032。把 Entity 写到 `Scripts/Hotfix/Server/` → 各种诡异错误(Hotfix 不允许字段)。决策表见上。
|
||||||
|
|
||||||
|
3. **客户端代码引用了 UnityEngine**:`Scripts/Hotfix/Client/` 是 `ET.Hotfix` 程序集,**不能** `using UnityEngine`。需要 UnityEngine API 的代码必须放 `Scripts/HotfixView/Client/` 或 `Scripts/ModelView/Client/`。
|
||||||
|
|
||||||
|
4. **Model 程序集报 `Define` / `GlobalConfig` / `Resources` 未找到**:同样 UnityEngine 引用问题。Model 层禁止依赖 UnityEngine。需要的话挪去 ModelView 或者用反射延后调用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 跨包引用的最佳实践
|
||||||
|
|
||||||
|
- 在 `package.json.dependencies` 显式声明依赖。
|
||||||
|
- **避免环依赖**:`login` 不要依赖 `bag/hero` 这种业务包;让业务包依赖 `login` 暴露的接口。如果非要双向,用 Invoke 契约(看 `cn.etetet.<pkg>invoke` 包模式)。
|
||||||
|
- 不允许 `Hotfix/Client` 调用 `Hotfix/Server` 代码(编译会过但语义错)。共享逻辑放 `Hotfix/Share`。
|
||||||
216
.cursor/skills/et-framework-dev/proto-workflow.md
Normal file
216
.cursor/skills/et-framework-dev/proto-workflow.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# Proto 完整工作流
|
||||||
|
|
||||||
|
## 1. 命名与放置
|
||||||
|
|
||||||
|
`.proto` 文件必须放在某个 `cn.etetet.<pkg>/Proto/` 目录下,且文件名遵守:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Module>_<Direction>_<StartOpcode>.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 取值 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Module` | 任意(如 `HeroOuter`、`BagOuter`、`LoginInner`)| 模块标识 |
|
||||||
|
| `Direction` | `C` / `S` / `CS` | C=外网消息(客户端可见),S=内网消息(仅服务端),CS=同时 |
|
||||||
|
| `StartOpcode` | 整数 | Opcode 从这里递增分配给每条 message |
|
||||||
|
|
||||||
|
opcode 分配规范(避免冲突):
|
||||||
|
|
||||||
|
| 模块 | Outer 起始 | Inner 起始 |
|
||||||
|
|---|---|---|
|
||||||
|
| Login | 1000 | 20001 |
|
||||||
|
| Router | 1100 | - |
|
||||||
|
| StateSync | 11001 | 21001 |
|
||||||
|
| ActorLocation | - | 20100 |
|
||||||
|
| Hero | 3000 | - |
|
||||||
|
| Bag | 3100 | - |
|
||||||
|
| Gacha | 3200 | - |
|
||||||
|
| 业务新模块 | 4000+ | 30000+ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 消息注解(决定生成的 C# 类继承谁)
|
||||||
|
|
||||||
|
### RPC 请求 / 响应
|
||||||
|
|
||||||
|
```proto
|
||||||
|
// ResponseType R2C_SummonHero ← 必须在 message 上一行
|
||||||
|
message C2G_SummonHero // ISessionRequest
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
string reqId = 2; ← 推荐字段,前 2 个固定 RpcId + reqId
|
||||||
|
int32 configId = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message R2C_SummonHero // ISessionResponse
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
int32 Error = 2; ← 前 3 个固定 RpcId / Error / Message
|
||||||
|
string Message = 3;
|
||||||
|
HeroInfo hero = 4;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
生成结果继承 `MessageObject, ISessionRequest` 和 `MessageObject, ISessionResponse`,并通过 `[ResponseType(nameof(R2C_SummonHero))]` 自动配对。
|
||||||
|
|
||||||
|
### 单向通知(服务端推客户端)
|
||||||
|
|
||||||
|
```proto
|
||||||
|
message Hero_HeroInfo // IMessage
|
||||||
|
{
|
||||||
|
HeroInfo hero = 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 嵌套 DTO
|
||||||
|
|
||||||
|
```proto
|
||||||
|
message HeroInfo
|
||||||
|
{
|
||||||
|
int64 heroId = 1;
|
||||||
|
int32 level = 2;
|
||||||
|
repeated SkillInfo skills = 3; // List<SkillInfo>
|
||||||
|
}
|
||||||
|
|
||||||
|
message SkillInfo
|
||||||
|
{
|
||||||
|
int32 skillId = 1;
|
||||||
|
int32 skillLevel = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
不加任何 `//` 注解的 message 默认继承 `MessageObject`,可作为 DTO 嵌入其他消息。
|
||||||
|
|
||||||
|
### 完整可识别的父类标记
|
||||||
|
|
||||||
|
Proto2CS 解析时,把 `message X // Y` 里的 `Y` 直接拼到生成类的继承列表。常见值:
|
||||||
|
|
||||||
|
- `IRequest` / `IResponse` — 内网请求/响应
|
||||||
|
- `ISessionRequest` / `ISessionResponse` — 客户端 ↔ Gate 的 RPC
|
||||||
|
- `IMessage` — 单向通知
|
||||||
|
- `IActorMessage` / `IActorRequest` / `IActorResponse` — Actor 模型消息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 生成
|
||||||
|
|
||||||
|
**Unity 菜单触发**:
|
||||||
|
|
||||||
|
```
|
||||||
|
ET → Proto → Proto2CS
|
||||||
|
```
|
||||||
|
|
||||||
|
**MCP 触发**:
|
||||||
|
|
||||||
|
```
|
||||||
|
CallMcpTool execute_menu_item {"menuPath": "ET/Proto/Proto2CS"}
|
||||||
|
```
|
||||||
|
|
||||||
|
工具会:
|
||||||
|
1. 扫描所有 `cn.etetet.*/Proto/*.proto`
|
||||||
|
2. 按文件名解析 Opcode
|
||||||
|
3. 输出三份 C# 到:
|
||||||
|
- `cn.etetet.proto/CodeMode/Model/Client/<Name>.cs`
|
||||||
|
- `cn.etetet.proto/CodeMode/Model/Server/<Name>.cs`
|
||||||
|
- `cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs`
|
||||||
|
|
||||||
|
> CodeMode 下只有 `ClientServer/` 子目录有 `AssemblyReference.asmref`(指向 ET.Model),所以在 `CodeMode: 3 = ClientServer` 模式下,**只有 ClientServer 那份会被编译**进 ET.Model。`Client/` 和 `Server/` 是预留给纯客户端/纯服务端模式的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 调用
|
||||||
|
|
||||||
|
### 客户端发 RPC
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
C2G_SummonHero request = C2G_SummonHero.Create();
|
||||||
|
request.configId = 1;
|
||||||
|
R2C_SummonHero response = await clientScene
|
||||||
|
.GetComponent<ClientSenderComponent>()
|
||||||
|
.Call(request, false) as R2C_SummonHero;
|
||||||
|
if (response.Error != 0)
|
||||||
|
{
|
||||||
|
Log.Error(response.Message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务端 RPC Handler
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Scripts/Hotfix/Server/C2G_SummonHeroHandler.cs
|
||||||
|
[MessageSessionHandler(SceneType.Gate)]
|
||||||
|
public class C2G_SummonHeroHandler : RpcMessageHandler<C2G_SummonHero, R2C_SummonHero>
|
||||||
|
{
|
||||||
|
protected override async ETTask OnRun(
|
||||||
|
Session session, C2G_SummonHero request, R2C_SummonHero response)
|
||||||
|
{
|
||||||
|
Player player = session.GetComponent<SessionPlayerComponent>()?.Player;
|
||||||
|
if (player == null) {
|
||||||
|
response.Error = ErrorCode.ERR_RpcFail;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 业务...
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- **不要**写 `protected override string Module => "X"` 这种属性,ET0004 会报。
|
||||||
|
- 文件放 `Scripts/Hotfix/Server/`。
|
||||||
|
|
||||||
|
### 服务端推单向通知
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Hero_HeroInfo notice = Hero_HeroInfo.Create();
|
||||||
|
notice.hero = HeroMapper.ToProto(hero);
|
||||||
|
session.Send(notice);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 客户端通知 Handler
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Scripts/Hotfix/Client/Hero_HeroInfoHandler.cs
|
||||||
|
[MessageHandler(SceneType.Main)]
|
||||||
|
[FriendOf(typeof(HeroComponent))]
|
||||||
|
public class Hero_HeroInfoHandler : MessageHandler<Scene, Hero_HeroInfo>
|
||||||
|
{
|
||||||
|
protected override async ETTask Run(Scene scene, Hero_HeroInfo message)
|
||||||
|
{
|
||||||
|
var comp = scene.GetComponent<HeroComponent>() ?? scene.AddComponent<HeroComponent>();
|
||||||
|
comp.HeroMap[message.hero.heroId] = message.hero;
|
||||||
|
EventSystem.Instance.Publish(scene, new HeroUpdatedEvent { HeroId = message.hero.heroId });
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常见 Proto 坑
|
||||||
|
|
||||||
|
1. **写完 .proto 不跑 Proto2CS → 引用方报"找不到类型"**:必须先生成。
|
||||||
|
2. **缺 `// ISessionRequest`**:消息生成出来不继承 `ISessionRequest`,handler 的 `MessageSessionHandler<TReq, TResp>` 类型约束失败 → 编译报错。
|
||||||
|
3. **缺 `// ResponseType R2C_X`**:客户端 `await session.Call(req)` 无法推断响应类型。
|
||||||
|
4. **文件名 opcode 重复**:和别的 proto 撞 opcode → 运行时崩。规范分配规则见上面表格。
|
||||||
|
5. **手写 `new HeroInfo {...}`** → ET0031。改 `HeroInfo.Create()`。
|
||||||
|
6. **修改 .proto 字段顺序 / 类型** → MemoryPack 序列化兼容性问题。线上版本要么不改,要么走版本兼容方案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 一键自检 checklist
|
||||||
|
|
||||||
|
写完一个新 .proto 后:
|
||||||
|
|
||||||
|
```
|
||||||
|
- [ ] 文件名格式 <Module>_<Direction>_<Opcode>.proto
|
||||||
|
- [ ] Opcode 不与其他 .proto 冲突(grep "_C_<同段号>_")
|
||||||
|
- [ ] RPC 消息有 // ResponseType
|
||||||
|
- [ ] 请求带 // ISessionRequest 或 // IRequest
|
||||||
|
- [ ] 响应带 // ISessionResponse 或 // IResponse
|
||||||
|
- [ ] 通知带 // IMessage
|
||||||
|
- [ ] 跑了 ET/Proto/Proto2CS
|
||||||
|
- [ ] 检查 cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs 已生成
|
||||||
|
- [ ] Assets/Refresh 让 Unity 索引到新文件
|
||||||
|
- [ ] recompile_scripts 验证 0 错误
|
||||||
|
```
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Unity generated
|
||||||
|
[Ll]ibrary/
|
||||||
|
[Tt]emp/
|
||||||
|
[Oo]bj/
|
||||||
|
[Bb]uild/
|
||||||
|
[Bb]uilds/
|
||||||
|
[Ll]ogs/
|
||||||
|
[Mm]emoryCaptures/
|
||||||
|
[Uu]ser[Ss]ettings/
|
||||||
|
|
||||||
|
# Visual Studio / Rider / VSCode
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.csproj
|
||||||
|
*.unityproj
|
||||||
|
*.sln
|
||||||
|
*.suo
|
||||||
|
*.tmp
|
||||||
|
*.user
|
||||||
|
*.userprefs
|
||||||
|
*.pidb
|
||||||
|
*.booproj
|
||||||
|
*.svd
|
||||||
|
*.pdb
|
||||||
|
*.mdb
|
||||||
|
*.opendb
|
||||||
|
*.VC.db
|
||||||
|
|
||||||
|
# Secrets (explicit blacklist)
|
||||||
|
Doc/server_connect.md
|
||||||
|
**/server_connect.md
|
||||||
|
|
||||||
|
# Mac / Win artifacts
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
18
Doc/Bag-System-Guide.md
Normal file
18
Doc/Bag-System-Guide.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Bag System Guide
|
||||||
|
|
||||||
|
## 模块目标
|
||||||
|
|
||||||
|
- 统一 Item 存储(货币、碎片、装备、材料都走 itemId)
|
||||||
|
- 提供增减、查询、批量发奖/扣费能力
|
||||||
|
|
||||||
|
## 核心接口
|
||||||
|
|
||||||
|
- `AddItems(List<RewardEntry>)`
|
||||||
|
- `RemoveItems(List<CostEntry>)`
|
||||||
|
- `GetItemCount(itemId)`
|
||||||
|
- `HasItems(List<CostEntry>)`
|
||||||
|
|
||||||
|
## 关键约束
|
||||||
|
|
||||||
|
- 任何资源变动都必须走 Bag 接口
|
||||||
|
- 禁止在 Player 上新增独立货币字段
|
||||||
226
Doc/ET-Packages-Audit.md
Normal file
226
Doc/ET-Packages-Audit.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
# ET 框架包能力清单(38 个)
|
||||||
|
|
||||||
|
> **目的**:避免重复造轮子。每次设计新功能前先查这份清单,确认是否有现成包可用。
|
||||||
|
> **更新时间**:2026-05-27
|
||||||
|
> **范围**:`My project/Packages/` 下所有 `cn.etetet.*` + `com.etetet.*` 包
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 速查表
|
||||||
|
|
||||||
|
| 类别 | 包 | 状态 | 关键 API/类 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **核心运行时** | cn.etetet.core | 自带可用 | `Log` / `MongoHelper` / `World` / `Entry` / `Fiber` / `TimerComponent` |
|
||||||
|
| | cn.etetet.loader | 自带可用 | `CodeLoader` / `Init` / `NLogger` / `BuildHelper` |
|
||||||
|
| | cn.etetet.sourcegenerator | 自带可用 | `[EntitySystemOf]` / `[ComponentOf]` 等属性 |
|
||||||
|
| | cn.etetet.memorypack | 自带可用 | MemoryPack 序列化 |
|
||||||
|
| | cn.etetet.mathematics | 自带可用 | float3/quaternion 等 |
|
||||||
|
| | com.etetet.init | 自带可用 | MongoDB.Driver 原生 DLL + 包依赖解析 |
|
||||||
|
| **UI 框架** | cn.etetet.yiuiframework | 自带可用 | `YIUIMgrComponent` / `PanelInfo` / `YIUILoadHelper` |
|
||||||
|
| | cn.etetet.yiui | 需扩展 | 项目级业务 UI,目前仅 `CommonHeader` |
|
||||||
|
| | cn.etetet.yiuiinvoke | 自带可用 | `[YIUIInvoke]` 跨模块解耦调用 |
|
||||||
|
| | **cn.etetet.yiuigm** | **完全现成** | **`[GM]` 属性 + `IGMCommand` 接口 + GMPanel 面板** |
|
||||||
|
| | cn.etetet.yiuitips | 自带可用 | `TipsHelper` 弹窗 |
|
||||||
|
| | cn.etetet.yiuireddot | 自带可用 | 红点树 + DAG |
|
||||||
|
| | cn.etetet.yiuiloopscrollrectasync | 自带可用 | 异步无限滚动列表 |
|
||||||
|
| | cn.etetet.yiui3ddisplay | 自带可用 | UI 内嵌 3D 模型 |
|
||||||
|
| | cn.etetet.yiuieffect | 自带可用 | UI 特效(粒子/灰度) |
|
||||||
|
| | cn.etetet.yiuiyooassets | 自带可用 | YIUI ↔ YooAssets 桥接 |
|
||||||
|
| **业务样板** | cn.etetet.login | 需扩展 | `C2R_LoginHandler` / `C2G_LoginGateHandler` / `Player`(仅 Account 字段) |
|
||||||
|
| | cn.etetet.statesync | 仅参考 | MMO Demo(Unit 同步/跨 Map) |
|
||||||
|
| | cn.etetet.yiuistatesync | 仅参考 | YIUI Login/Lobby/Main Panel 集成样板 |
|
||||||
|
| **MMO 战斗(与本次无关)** | cn.etetet.unit | 不复用 | MMO 单位 Entity |
|
||||||
|
| | cn.etetet.numeric | 可借鉴 | KV 数值 + Buff 五段公式 + Watcher |
|
||||||
|
| | cn.etetet.move | 不复用 | MMO 寻路点移动 |
|
||||||
|
| | cn.etetet.ai | 不复用 | 行为机 AI |
|
||||||
|
| | cn.etetet.aoi | 不复用 | 九宫格视野同步 |
|
||||||
|
| | cn.etetet.recast | 不复用 | 3D 寻路 |
|
||||||
|
| | cn.etetet.ui | 已废弃 | ET 原生 UI(被 YIUI 替代) |
|
||||||
|
| | cn.etetet.demores | 不复用 | StateSync Demo 资源 |
|
||||||
|
| **网络** | cn.etetet.router | 自带可用 | KCP 软路由 |
|
||||||
|
| | cn.etetet.netinner | 自带可用 | 进程间内网消息 |
|
||||||
|
| | cn.etetet.http | 自带可用 | 轻量 HTTP Server |
|
||||||
|
| | cn.etetet.actorlocation | 可选 | 分布式 Actor Location |
|
||||||
|
| **协议** | **cn.etetet.proto** | **自带可用** | **`ET/Proto/Proto2CS` 工具链 + 自动 Opcode + MemoryPack** |
|
||||||
|
| **配置** | **cn.etetet.excel** | 不够用 | Excel→C#+JSON+BSON,**不支持外键/嵌套 struct** |
|
||||||
|
| | cn.etetet.startconfig | 自带可用 | 服务器拓扑配置(Process/Zone/Scene/Machine) |
|
||||||
|
| **资源/热更** | cn.etetet.yooassets | 自带可用 | YooAsset 资源管理(打包/热更/下载) |
|
||||||
|
| | cn.etetet.hybridclr | 自带可用 | HybridCLR C# 全平台热更 |
|
||||||
|
| | cn.etetet.referencecollector | 自带可用 | Prefab 引用收集器 |
|
||||||
|
| **工具/运维** | **cn.etetet.console** | **完全现成** | **`[ConsoleHandler]` 属性 + `IConsoleHandler` 接口 + stdin 命令循环** |
|
||||||
|
| | cn.etetet.watcher | 可选 | 多进程保活 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键能力(GM/日志/DB/配置/协议)
|
||||||
|
|
||||||
|
### GM 命令(**完全现成,双轨**)
|
||||||
|
|
||||||
|
| 维度 | `cn.etetet.console` | `cn.etetet.yiuigm` |
|
||||||
|
|---|---|---|
|
||||||
|
| 运行端 | 服务端 stdin REPL | 客户端 YIUI 面板 |
|
||||||
|
| 扩展方式 | `[ConsoleHandler("AddItem")]` + `IConsoleHandler.Run(fiber, contex, content)` | `[GM(EGMType.Test, 1, "发放道具", "...")]` + `IGMCommand.GetParams + Run(scene, paramVo)` |
|
||||||
|
| 参数类型 | 字符串自由切分 | Enum / String / Bool / Float / Int / Long 6 种 |
|
||||||
|
| 内置命令 | `R` 热更 DLL、`C ConfigName` 热更 Reload 配置、`Robot` 压测 | 示例 `GM_Test` |
|
||||||
|
| 业务 GM 接入 | 新增 `[ConsoleHandler]` 类 | 新增 `[GM]` 类 |
|
||||||
|
|
||||||
|
**结论**:**不需要新建 GM 框架**。每个新业务命令只需写一个 `[GM]` 类(客户端)或 `[ConsoleHandler]` 类(服务端)。
|
||||||
|
|
||||||
|
### 日志(自带 Log 类,需扩展)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 已有:
|
||||||
|
Log.Debug(msg); Log.Info(msg); Log.Warning(msg); Log.Error(msg);
|
||||||
|
Log.Trace(msg); Log.Console(msg);
|
||||||
|
|
||||||
|
// 已有:Fiber 级 ILog 接口可替换底层(默认 NLog 服务端 + UnityLogger 客户端)
|
||||||
|
```
|
||||||
|
|
||||||
|
**缺失能力**(要在 ET Log 基础上扩展):
|
||||||
|
- jsonl 审计日志(无)
|
||||||
|
- 模块标签前缀(无,需约定 `[Hero]`/`[Bag]`/`[Gacha]` 写法)
|
||||||
|
- 按天分目录(NLog 支持,需配置)
|
||||||
|
- 慢操作告警(无,需自加 AOP/手动埋点)
|
||||||
|
- reqId 全链路透传(无,需在 Handler 基类透传)
|
||||||
|
- 敏感字段脱敏(无)
|
||||||
|
- 客户端日志桥接到服务端(无)
|
||||||
|
|
||||||
|
**结论**:**扩展非重写**。新建 `cn.etetet.audit` 或在 `cn.etetet.logging` 加扩展方法 `Log.Audit(module, action, payload)`,底层调 NLog 输出 jsonl 到 `Logs/Audit/{module}/{date}.jsonl`。
|
||||||
|
|
||||||
|
### DBComponent / 玩家持久化(基础设施现成,业务层缺)
|
||||||
|
|
||||||
|
| 已有 | 缺失 |
|
||||||
|
|---|---|
|
||||||
|
| `MongoHelper.ToJson/FromJson/Clone/CloneBytes`(BSON 序列化) | `IDBComponent.Save<T>/Query<T>/Update<T>/Delete<T>` 业务封装 |
|
||||||
|
| `com.etetet.init/Plugins/MongoDB/*` 原生 DLL | 连接池/重试/慢查询 |
|
||||||
|
| `MongoRegister` 注册类型映射 | Entity 与 MongoDB Collection 的映射约定 |
|
||||||
|
|
||||||
|
**结论**:**新建业务包 `cn.etetet.db`** 包装这层。**不需要重写 BSON**。
|
||||||
|
|
||||||
|
### 协议生成(完全现成)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 在业务包下建 Proto/*.proto
|
||||||
|
2. Unity 菜单 ET/Proto/Proto2CS
|
||||||
|
3. 自动扫描所有 cn.etetet.* 包的 Proto/ 目录
|
||||||
|
4. 生成带 [MemoryPackable] + Opcode 的 C# 到 cn.etetet.proto/CodeMode/Model/{Client,Server,ClientServer}
|
||||||
|
```
|
||||||
|
|
||||||
|
**现有协议组占用**:1000-1099 Login / 1100-1199 Router / 11001-11099 StateSync C2C / 20100-20199 ActorLocation / 21001-21099 StateSync M2C / 21100-21199 StateSync M2M。
|
||||||
|
|
||||||
|
**ROK 占用建议**:3000-3099 Hero / 3100-3199 Bag/Equip / 3200-3299 Gacha。
|
||||||
|
|
||||||
|
### YIUI 框架(**深度复用,UI 几乎零造轮**)
|
||||||
|
|
||||||
|
**核心认知**:
|
||||||
|
- 标准面板入口:`scene.YIUIRoot().OpenPanelAsync<T>()` / `OpenPanelParamAsync<T>(...)` / `OpenPanelWaitAsync<T>(...)`(模态等待)
|
||||||
|
- 数据驱动:Prefab 挂 `UIDataBind*` 组件,代码改 `u_DataXxx.SetValue(...)` 自动刷新 UI
|
||||||
|
- 事件驱动:Prefab 挂 `UIEventBind*` + `[YIUIInvoke(const)]` 静态方法接线,**无需 Button.onClick.AddListener**
|
||||||
|
- 跨模块解耦:`[YIUIInvokeSystem("Key")]` + `YIUIInvokeHandler<T>` + `YIUIInvokeSystem.Instance.Invoke(...)`
|
||||||
|
- CDETable = **C**omponent + **D**ata + **E**vent 三张表,YIUI 自动化工具一键生成 Component/System
|
||||||
|
|
||||||
|
**15 条「YIUI 已经做了的事」速查**:
|
||||||
|
|
||||||
|
| # | 不要自己做 | 用 YIUI 什么 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 面板打开/关闭/栈/Home 管理 | `scene.YIUIRoot().Open/ClosePanelAsync` |
|
||||||
|
| 2 | 按钮点击接线 | `UIEventBind*` + `[YIUIInvoke]` |
|
||||||
|
| 3 | Item/面板字段刷新样板 | `u_Data*` + `UIDataBind*` |
|
||||||
|
| 4 | 图标/贴图异步加载 | `UIDataBindImage`(内部走 YooAsset) |
|
||||||
|
| 5 | Prefab 从 AB 加载 | 打开 Panel 即可(`cn.etetet.yiuiyooassets` 桥接) |
|
||||||
|
| 6 | 确认框 / 飘字 / 模态等待 | `TipsHelper.OpenWait/OpenSync` + `TipsMessageView/TipsTextView` |
|
||||||
|
| 7 | 红点树与父子传播 | `RedDotMgr.Inst.SetCount(key, count)` + `RedDotBind`,**只写叶子,父节点自动汇总** |
|
||||||
|
| 8 | 长列表虚拟化与池化 | `YIUILoopScrollChild.SetDataRefresh(IList)` + `ReRenderer()` |
|
||||||
|
| 9 | GM/模块间零引用调用 | `YIUIInvokeSystem` + invoke 契约包 |
|
||||||
|
| 10 | 顶栏/关闭按钮 | 现成 `CommonHeader` + `YIUICloseCommon` |
|
||||||
|
| 11 | 英雄详情 3D 模型 | `YIUI3DDisplayChild.ShowAsync(resName)`,自带 RT/Camera/RawImage 池 |
|
||||||
|
| 12 | 按钮灰显/抽卡 UI 特效 | `UIDataBindGray` / `UIEffect` / `UIParticle` |
|
||||||
|
| 13 | 异步操作全屏挡点击 | `scene.YIUIMgr().BanLayerOptionForever()` |
|
||||||
|
| 14 | UI 倒计时文本 | `CountDownMgr`(在 framework) |
|
||||||
|
| 15 | 列表数据变长度不变 | `Loop.ReRenderer()` 增量刷新 |
|
||||||
|
|
||||||
|
**仍需自己做(YIUI 未覆盖)**:
|
||||||
|
|
||||||
|
| 项 | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| `BagItemTooltip` 道具浮层 | tips 包无现成 Tooltip View,但容器可用 `TipsHelper.Open` 承载 |
|
||||||
|
| `BagItemUICommon` 通用道具组件 | 业务复合组件,用 YIUI 生成 + 上述绑定拼装 |
|
||||||
|
| 英雄列表三段分组 | Loop 只负责虚拟化,分组逻辑在 IList 数据层或 Header Item |
|
||||||
|
| 天赋树可视化 | 无图编辑器组件,需自绘 |
|
||||||
|
| `ErrorMessageHelper` 文案表 | 自建(**展示**用 `TipsTextView`) |
|
||||||
|
| 业务 invoke Handler | 框架有机制,业务 Handler 需自写 |
|
||||||
|
|
||||||
|
**易踩坑(必读)**:
|
||||||
|
|
||||||
|
1. **没有 `PanelHelper`**:标准写法是 `scene.YIUIRoot().OpenPanelAsync<T>()`,与 `yiuistatesync` 登录/大厅一致,不要自建 Helper
|
||||||
|
2. **CDETable 正式名是 `UIBindCDETable`**(C+D+E = Component + Data + Event)
|
||||||
|
3. **红点只能在叶子节点 `SetCount`**,给有子节点的 Key 直接 Set 会报错
|
||||||
|
4. **`cn.etetet.yiui` 几乎是空包**,ROK 业务 Panel 应建在 `cn.etetet.hero/bag/gacha` 内部,不期待现成业务 UI
|
||||||
|
5. **客户端数据 → UI 优先走 `UIDataBind`,不要默认走 ET EventSystem**:UIData 是"局部数据驱动",EventSystem 是"跨面板业务广播",两者职责不同
|
||||||
|
|
||||||
|
### 配置(ExcelExporter 不够用,引入 Luban)
|
||||||
|
|
||||||
|
`cn.etetet.excel` 能力:
|
||||||
|
- 支持类型:基本类型 + `int[]` / `string[]` / `int[][]`
|
||||||
|
- 不支持:**外键校验、嵌套 struct、复杂引用类型**(未知类型 `throw 不支持`)
|
||||||
|
- 输出:C# Category + JSON + BSON bytes
|
||||||
|
|
||||||
|
**对 ROK Hero/Bag 缺口**:
|
||||||
|
- `List<RewardEntry>` 通用奖励结构 → ExcelExporter 无法表达 bean 数组
|
||||||
|
- `List<AttrEntry>` 装备属性配对 → 难表达
|
||||||
|
- 卡池权重表多列关联 → 无外键校验
|
||||||
|
- 天赋树前置依赖 → 无引用校验
|
||||||
|
|
||||||
|
**结论**:**新业务表用 Luban**(不写自定义模板,原生 API + Scene Component 包装)。**老配置(StartConfig 等)保持 ExcelExporter**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ROK Hero/Bag 移植技术选型(基于本清单)
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 直接复用
|
||||||
|
- 核心运行时(core/loader/sourcegenerator/memorypack)
|
||||||
|
- 协议工具链(proto + Proto2CS)
|
||||||
|
- UI 全套(yiuiframework + 各 yiui 子包)
|
||||||
|
- GM 双轨(yiuigm + console)
|
||||||
|
- 资源热更(yooassets + hybridclr)
|
||||||
|
- 登录链路(login,扩展 Player 字段)
|
||||||
|
|
||||||
|
🔧 扩展使用
|
||||||
|
- Log → Audit 扩展(新建 cn.etetet.logging 或 cn.etetet.audit)
|
||||||
|
- login Player → 挂 HeroComponent / BagComponent / GachaComponent
|
||||||
|
- excel → 只维护 StartConfig 等 ET 基础设施表
|
||||||
|
|
||||||
|
📦 新增业务包
|
||||||
|
- cn.etetet.db # DBComponent 业务封装(基于 MongoHelper + MongoDB.Driver)
|
||||||
|
- cn.etetet.hero # 英雄系统
|
||||||
|
- cn.etetet.bag # 背包/装备系统
|
||||||
|
- cn.etetet.gacha # 抽卡系统
|
||||||
|
- cn.etetet.config # Luban Tables + ConfigComponent
|
||||||
|
|
||||||
|
❌ 不复用(与本次无关)
|
||||||
|
- unit / move / ai / aoi / recast / numeric(OctoberStudio 走自己的局内战斗)
|
||||||
|
- statesync / yiuistatesync(MMO Demo,仅参考代码风格)
|
||||||
|
- ui(旧 UI,已被 YIUI 替代)
|
||||||
|
- demores(Demo 美术资源)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计原则(基于这份清单的检查清单)
|
||||||
|
|
||||||
|
每次新功能落地前的 3 问:
|
||||||
|
|
||||||
|
1. **现有 ET 包是否有?** → 查上表
|
||||||
|
2. **如果有但能力不全,是扩展还是重写?** → **优先扩展**,禁止重写已有内核
|
||||||
|
3. **如果没有,应该建独立业务包还是塞进现有包?** → 业务功能建新包 `cn.etetet.xxx`,**禁止塞进** `core` / `login` 等基础包
|
||||||
|
|
||||||
|
每次重大决策都需要在 `design.md` 的 D 决策表中明确"复用了哪些已有包 + 扩展点 + 新增点"。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修订记录
|
||||||
|
|
||||||
|
| 日期 | 版本 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-05-27 | v1.0 | 初版,38 包全量调研 |
|
||||||
39
Doc/GM-Commands.md
Normal file
39
Doc/GM-Commands.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# GM Commands Guide
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
本项目 GM 体系采用双轨入口,并强制复用现有 ET 包能力,不额外新建 GM 框架。
|
||||||
|
|
||||||
|
## 双轨入口约定
|
||||||
|
|
||||||
|
- 服务端命令入口:`cn.etetet.console`
|
||||||
|
- 使用 `[ConsoleHandler("CommandName")]` 注册
|
||||||
|
- 适合批量操作、压测、运维值守
|
||||||
|
- 客户端调试入口:`cn.etetet.yiuigm`
|
||||||
|
- 使用 `[GM(...)]` + `IGMCommand` 注册
|
||||||
|
- 仅用于 Debug 开发测试
|
||||||
|
|
||||||
|
## 共享实现约定
|
||||||
|
|
||||||
|
- GM 业务逻辑必须下沉到共享静态帮助类 `GMHelper`
|
||||||
|
- Console 与 Panel 只负责参数解析与转发,禁止各自复制业务逻辑
|
||||||
|
- GMHelper 内部必须调用正式业务系统接口(如 `BagComponentSystem.AddItems`),禁止直接改字段绕过校验
|
||||||
|
|
||||||
|
## 发布与安全
|
||||||
|
|
||||||
|
- 客户端 `[GM]` 类必须使用 `#if ENABLE_GM` 条件编译包裹
|
||||||
|
- Release 构建不加载 GM 入口
|
||||||
|
- 所有 GM 操作应写审计日志(模块 `GM`)
|
||||||
|
|
||||||
|
## 建议命名规范
|
||||||
|
|
||||||
|
- `GM_*`:客户端 panel 命令实现
|
||||||
|
- `C_*`:服务端 console 命令实现
|
||||||
|
- `GMHelper.*`:共享业务函数
|
||||||
|
|
||||||
|
## 待接入命令清单(第一批)
|
||||||
|
|
||||||
|
- 资源:`AddItem` / `RemoveItem` / `ShowBag`
|
||||||
|
- 英雄:`AddHero` / `SetHeroLevel` / `ShowHeroes`
|
||||||
|
- 抽卡:`Gacha` / `ShowGachaStats` / `SimulateGacha`
|
||||||
|
- 运维:`FlushDirty` / `SetLogLevel`
|
||||||
23
Doc/Hero-System-Guide.md
Normal file
23
Doc/Hero-System-Guide.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Hero System Guide
|
||||||
|
|
||||||
|
## 模块目标
|
||||||
|
|
||||||
|
- 提供英雄创建、升级、升星、技能、觉醒、天赋核心流程
|
||||||
|
- 服务端权威,客户端仅缓存与展示
|
||||||
|
|
||||||
|
## 核心实体
|
||||||
|
|
||||||
|
- `Player`
|
||||||
|
- `HeroComponent`
|
||||||
|
- `Hero`(子实体)
|
||||||
|
|
||||||
|
## 关键接口
|
||||||
|
|
||||||
|
- `C2G_SummonHero`
|
||||||
|
- `C2G_HeroAddExp`
|
||||||
|
- `C2G_HeroSkillUp`
|
||||||
|
|
||||||
|
## 校验策略
|
||||||
|
|
||||||
|
- 所有消耗先走 `BagComponentSystem.HasItems/RemoveItems`
|
||||||
|
- 所有失败返回统一错误码并记 L3
|
||||||
19
Doc/Item-Design-Rule.md
Normal file
19
Doc/Item-Design-Rule.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 通用 Item 设计约定
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- 全游戏可获得资源必须用 `itemId` 表示
|
||||||
|
- 禁止在 `Player` 上新增 `Gold/Diamond/Food/Wood` 等独立货币字段
|
||||||
|
- 发奖统一走 `BagComponentSystem.AddItems`
|
||||||
|
- 扣费统一走 `BagComponentSystem.RemoveItems`
|
||||||
|
|
||||||
|
## 预留货币段
|
||||||
|
|
||||||
|
- `1` = 金币
|
||||||
|
- `2` = 钻石
|
||||||
|
- `3` = PvP 币
|
||||||
|
|
||||||
|
## 统一结构
|
||||||
|
|
||||||
|
- `RewardEntry { itemId, count }`
|
||||||
|
- `CostEntry { itemId, count }`
|
||||||
25
Doc/Logging-Guide.md
Normal file
25
Doc/Logging-Guide.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logging Guide
|
||||||
|
|
||||||
|
## 模块标签前缀约定
|
||||||
|
|
||||||
|
- `[Hero]`
|
||||||
|
- `[Bag]`
|
||||||
|
- `[Gacha]`
|
||||||
|
- `[Equip]`
|
||||||
|
- `[DB]`
|
||||||
|
- `[Login]`
|
||||||
|
- `[GM]`
|
||||||
|
- `[Net]`
|
||||||
|
|
||||||
|
## 层级
|
||||||
|
|
||||||
|
- L1:业务日志
|
||||||
|
- L2:审计日志(jsonl)
|
||||||
|
- L3:错误日志
|
||||||
|
|
||||||
|
## 常用方法
|
||||||
|
|
||||||
|
- `LogExtensions.Module(tag, level, msg)`
|
||||||
|
- `LogExtensions.Audit(module, action, payload)`
|
||||||
|
- `LogExtensions.Slow(module, action, durationMs, ctx)`
|
||||||
|
- `LogExtensions.AuditGM(operator, action, targetPlayer, args, success)`
|
||||||
25
Doc/Luban-Guide.md
Normal file
25
Doc/Luban-Guide.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Luban 使用说明(最小链路)
|
||||||
|
|
||||||
|
## 目录约定
|
||||||
|
|
||||||
|
- Schema:`Config/Defines`
|
||||||
|
- Data:`Config/Datas`
|
||||||
|
- 生成 C#:`My project/Packages/cn.etetet.config/Scripts/Model/Share/cfg`
|
||||||
|
- 生成 Json:`My project/Bundles/Config`
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
`powershell -ExecutionPolicy Bypass -File "tools/luban/gen.ps1"`
|
||||||
|
|
||||||
|
## 运行时接入
|
||||||
|
|
||||||
|
- 场景挂载:`ConfigComponent`
|
||||||
|
- 读取入口:`scene.Cfg()`
|
||||||
|
- 示例:`scene.Cfg().TbItem.Get(1)`
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 当前 `cfg.Tables` 为占位实现,后续应由 Luban 正式生成结果覆盖。
|
||||||
|
- 新增表一律先补 `bean.xml`,再补数据文件。
|
||||||
848
Doc/ROK-Bag-Spec.md
Normal file
848
Doc/ROK-Bag-Spec.md
Normal file
@ -0,0 +1,848 @@
|
|||||||
|
# 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<Int64, ItemInfoEntity> Items; // itemIndex → 道具实体
|
||||||
|
private List<ItemInfoEntity> m_itemList; // 顺序列表
|
||||||
|
private Dictionary<Int64, int> m_itemNumMap; // itemId → 累计数量(懒计算,-1 表示需重算)
|
||||||
|
private Dictionary<long, ItemInfo> m_materialItemInfos; // 装备材料分组
|
||||||
|
private Dictionary<long, ItemInfo> m_equipItemInfos; // 装备本体分组
|
||||||
|
private Dictionary<int, Dictionary<long, long>> m_reddotRecord; // type → (itemIndex → 新增数量)
|
||||||
|
private Dictionary<int, long> 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\<int\> | 获得途径列表,关联 `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<int, IItemUseHandler>` 注册机制 | 比 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)
|
||||||
18
Doc/ROK-Config-Source.md
Normal file
18
Doc/ROK-Config-Source.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# ROK 配置源说明
|
||||||
|
|
||||||
|
## 数据源真相
|
||||||
|
|
||||||
|
- ROK 服务端配置源:`Configs.data`(Lua 大表,tabtoy 生成)
|
||||||
|
- ROK 客户端配置源:加密 SQLite + 大量 `*Define.cs` DTO
|
||||||
|
- 原始工程无可直接复用的 Excel 源文件
|
||||||
|
|
||||||
|
## 迁移策略
|
||||||
|
|
||||||
|
1. 从 `*Define.cs` 派生 Luban `bean.xml`
|
||||||
|
2. 从 `Configs.data` 解析 JSON
|
||||||
|
3. JSON 转 Luban `xlsx`
|
||||||
|
4. 使用 Luban 生成 C# + Json
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
本项目采用“脚本迁移 + 自动生成”,不做手工录入。
|
||||||
509
Doc/ROK-Equip-Spec.md
Normal file
509
Doc/ROK-Equip-Spec.md
Normal file
@ -0,0 +1,509 @@
|
|||||||
|
# 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\<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 协议
|
||||||
|
|
||||||
|
```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<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<AttrEntry>" | 实际是 `att[] + attAddEx[]` 两个 List 平行 | bean schema 按平行 List 设计 |
|
||||||
|
| "套装效果 = 4 件套" | 实际是 2/4/6/8 件**逐级叠加** | bean schema 拆 4 段 |
|
||||||
1441
Doc/ROK-Hero-Spec.md
Normal file
1441
Doc/ROK-Hero-Spec.md
Normal file
File diff suppressed because it is too large
Load Diff
575
Doc/ROK-HeroAcquire-Spec.md
Normal file
575
Doc/ROK-HeroAcquire-Spec.md
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
# ROK 英雄获取机制业务规则(1:1 迁移依据)
|
||||||
|
|
||||||
|
> 来源:`E:\Game\gmd\ROK` 服务端 Lua + 客户端 C# Proxy/View + sproto 协议
|
||||||
|
> 用途:澄清 ROK 是否有抽卡 / 玩家究竟如何获得英雄,作为 Survivors 工程移植 / 设计英雄包的依据
|
||||||
|
> 调研日期:2026-05-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 核心结论(一句话)
|
||||||
|
|
||||||
|
**ROK 不是传统抽卡游戏。** 它走的是 **「随机宝箱产出碎片 → 玩家主动用碎片兑换指定英雄」** 的两段式获取链,而不是「英雄卡池 + 权重 + 单抽十连保底」。
|
||||||
|
|
||||||
|
完整链路(一张图):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ROK 英雄获取完整链路 │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
【产出端 - 随机】 【中间产物】 【兑换端 - 确定性】
|
||||||
|
|
||||||
|
酒馆白银宝箱 ──┐ ┌── 英雄招募碎片 A
|
||||||
|
(Build_Tavern │ │ (HeroDefine.getItem)
|
||||||
|
silverBox) │ │
|
||||||
|
│ │ ┌──────────────┐
|
||||||
|
酒馆黄金宝箱 ──┤───→ 服务端 ItemPackage 随机 ────→├──→│ Captain UI │
|
||||||
|
(Build_Tavern │ (silverBoxItemPackage) │ │ 点击"召唤" │
|
||||||
|
goldBox) │ (goldBoxItemPackage) │ └──────┬───────┘
|
||||||
|
│ │ │
|
||||||
|
任务/活动/邮件─┤ 产出可以是: │ ▼
|
||||||
|
/充值/商店 ─┤ • ITEM (碎片,最常见) │ Hero_SummonHero RPC
|
||||||
|
奖励 ─┤ • HERO (直接给英雄,罕见) │ (heroId, 扣 getItemNum 个
|
||||||
|
│ • 资源/士兵/... │ getItem 碎片,确定性获得)
|
||||||
|
特殊:新手引导─┘ │
|
||||||
|
完成"攻击野蛮人" │
|
||||||
|
→ 直接给 guideHero │
|
||||||
|
│
|
||||||
|
──────────────────────────────────────────────── │
|
||||||
|
关键:碎片产出端"似抽卡",但英雄本体的"获取"环节是 │
|
||||||
|
确定性兑换,没有随机、没有保底、没有十连。 │
|
||||||
|
──────────────────────────────────────────────── │
|
||||||
|
│
|
||||||
|
雕像兑换 (Hero_ExchangeHeroItem 605) │
|
||||||
|
把"通用英雄雕像"道具转换为某英雄的招募碎片,──────┘
|
||||||
|
不是直接兑换英雄。
|
||||||
|
```
|
||||||
|
|
||||||
|
**对 Survivors `cn.etetet.gacha` 包的建议(详见第 7 节)**:
|
||||||
|
- ROK 没有"抽卡",因此 1:1 迁移角度建议**改名为 `cn.etetet.summon`**(英雄碎片兑换召唤)+ **`cn.etetet.tavern`**(酒馆开箱产出碎片);
|
||||||
|
- 用户在前序对话已明确表态"还需要抽卡的逻辑用来验证产出",因此 **`cn.etetet.gacha` 作为 Survivors 独立扩展功能保留**,但应明确定位它是「在 ROK 基础之上新增」而非「移植 ROK 已有」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 调研方法
|
||||||
|
|
||||||
|
### 1.1 走读文件
|
||||||
|
|
||||||
|
| 路径 | 关键发现 |
|
||||||
|
|------|---------|
|
||||||
|
| `E:\Game\gmd\ROK\Server\common\protocol\Protocol.sproto` | Hero 模块协议号段 601-700;Tavern 在 Build 模块 407 |
|
||||||
|
| `E:\Game\gmd\ROK\Server\common\protocol\Common.sproto` | `.Heros` / `.RewardInfo` 结构定义 |
|
||||||
|
| `E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\HeroLogic.lua` | `addHero` 唯一入口;无 summon/recruit/exchange/draw/gacha/lottery/pool 关键字(除 `summonTime` 字段名) |
|
||||||
|
| `E:\Game\gmd\ROK\Server\server\game_server\logic\service\proxy\Hero.lua` | `response.SummonHero` / `response.ExchangeHeroItem` 等英雄相关 RPC handler |
|
||||||
|
| `E:\Game\gmd\ROK\Server\server\game_server\logic\service\proxy\Build.lua` | `response.Tavern` 酒馆开箱 RPC handler |
|
||||||
|
| `E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\BuildingLogic.lua` | `openSilver` / `openGold` 宝箱开启逻辑 |
|
||||||
|
| `E:\Game\gmd\ROK\Server\server\game_server\logic\lualib\ItemLogic.lua` | `getItemPackage` 通用奖励包(含 HERO 类型节点) |
|
||||||
|
| `E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\Proxy\HeroProxy.cs` | 客户端 `SummonHero(id)` 入口 |
|
||||||
|
| `E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\View_Mediator\Captain\*` | 英雄列表 UI / 召唤按钮 |
|
||||||
|
| `E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\MVC\View_Mediator\Tavern\*` | 酒馆双宝箱 UI |
|
||||||
|
| `E:\Game\gmd\ROK\Client\Assets\Scripts\Hotfix\Config\HeroConfig.cs` | HeroDefine schema |
|
||||||
|
| `E:\Game\gmd\ROK\Server\common\errorcode\ErrorCode.lua` | HERO 相关错误码 3000-3027 |
|
||||||
|
|
||||||
|
### 1.2 关键字 grep 结果
|
||||||
|
|
||||||
|
在 `HeroLogic.lua` 里 grep **`summon|recruit|exchange|draw|gacha|lottery|pool|fragment`**(不区分大小写):
|
||||||
|
|
||||||
|
```
|
||||||
|
HeroLogic.lua:42: MSM.ActivityRoleMgr[_rid].req.setActivitySchedule( _rid, Enum.ActivityActionType.RECRUIT_HERO_COUNT, 1 )
|
||||||
|
HeroLogic.lua:60: defHeroAttr.summonTime = os.time()
|
||||||
|
HeroLogic.lua:97: summonTime = heroInfo.summonTime,
|
||||||
|
```
|
||||||
|
|
||||||
|
只有三处命中,且全部都是「记录英雄被获取的时间戳」和「活动事件统计计数」,**不是抽卡/卡池/概率逻辑**。
|
||||||
|
|
||||||
|
在客户端 `MVC/View_Mediator/` 列目录:
|
||||||
|
|
||||||
|
```
|
||||||
|
Activity/ Alliance/ Bag/ Captain/ Chat/ Equip/ Hero/(仅 RareView) ...
|
||||||
|
Tavern/ ← 唯一长得像"抽卡"的目录
|
||||||
|
```
|
||||||
|
|
||||||
|
`Hero/` 目录只有 `UI_Hero_RareMediator.cs` / `UI_Hero_RareView.cs`(稀有度展示组件);**真正的英雄列表与召唤入口在 `Captain/` 目录**(统帅=Captain=Hero)。
|
||||||
|
|
||||||
|
在 ROK 全工程 grep **`fragmentItemId|recruitLimit`**:**0 命中**。这两个字段名是 Survivors openspec proposal 自己拟的,不来自 ROK。
|
||||||
|
|
||||||
|
### 1.3 阳性 / 阴性结论
|
||||||
|
|
||||||
|
| 关键问题 | 结论 | 证据 |
|
||||||
|
|----------|------|------|
|
||||||
|
| ROK 有传统抽卡卡池(heroId 池 + 权重 + 保底)吗? | **没有** | 服务端无任何 `HeroPool` / `gacha` / `lottery` / `weight` 配置或逻辑;`Hero_SummonHero` RPC 内部是 1:1 道具兑换,无 `Random` 调用 |
|
||||||
|
| ROK 有"招募"概念吗? | **有,但名字叫"召唤 SummonHero"** | `Hero_SummonHero 601` 协议;客户端按钮 `OnSummonClick`;语言包文案使用"招募/Recruit" |
|
||||||
|
| ROK 有"碎片兑换"概念吗? | **有** | `HeroDefine.getItem` + `getItemNum` 就是"招募所需碎片 itemId + 数量";玩家凑齐后调用 `Hero_SummonHero` 扣碎片得英雄 |
|
||||||
|
| ROK 有其他英雄获取途径吗? | **有** | 新手引导直给 `guideHero`、文明初始英雄 `civilization.initialHero`、各种奖励包里直接含 HERO 类型节点(罕见)、酒馆宝箱开出英雄(罕见,主要开出的是碎片) |
|
||||||
|
| `fragmentItemId` / `recruitLimit` 在 ROK 存在吗? | **不存在** | grep 全工程 0 命中。等价字段是 `getItem`;不存在 `recruitLimit`(同一英雄一辈子只能拥有一次,已拥有再 summon 会报 `HERO_ALREADY_EXIST` 错误码) |
|
||||||
|
| ROK 的「酒馆开箱」算不算抽卡? | **算"准抽卡"** | `Build_Tavern 407` 协议,白银/黄金两种宝箱,免费次数+CD+付费三种开法,奖励池为 `silverBoxItemPackage` / `goldBoxItemPackage`(通用 ItemPackage),可输出碎片/英雄/资源/士兵 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 英雄获取的所有途径
|
||||||
|
|
||||||
|
按"玩家获得英雄实例所占比例"排序(推测):
|
||||||
|
|
||||||
|
### 2.1 ⟨途径 1⟩ 碎片兑换召唤(主路径)
|
||||||
|
|
||||||
|
- **名称**:英雄召唤 / Summon Hero / 招募
|
||||||
|
- **玩家入口**:Captain UI(统帅大厅)→ 选中一个未拥有的英雄卡 → 点击「召唤」按钮(`UI_Item_CaptainData_SubView.OnSummonClick`,文件 `View_Mediator/Common/Logic/UI_Item_CaptainData_SubView.cs:61-65`)
|
||||||
|
- **RPC**:`Hero_SummonHero (601)`,参数 `{ heroId: integer }`
|
||||||
|
- 客户端发起:`HeroProxy.cs:567-572` `SummonHero(int id)`
|
||||||
|
- **服务端核心逻辑**(`server/game_server/logic/service/proxy/Hero.lua:18-58`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function response.SummonHero( msg )
|
||||||
|
local rid, heroId = msg.rid, msg.heroId
|
||||||
|
|
||||||
|
-- 1. 参数检查 + 配置检查
|
||||||
|
if not heroId then return nil, ErrorCode.HERO_ARG_ERROR end
|
||||||
|
local sHero = CFG.s_Hero:Get( heroId )
|
||||||
|
if not sHero or table.empty( sHero ) then return nil, ErrorCode.CFG_ERROR end
|
||||||
|
|
||||||
|
-- 2. 王国开服天数 >= sHero.getLimit
|
||||||
|
local openDays = Common.getSelfNodeOpenDays()
|
||||||
|
if openDays < ( sHero.getLimit or 0 ) then
|
||||||
|
return nil, ErrorCode.HERO_OPEN_DAYS_NOT_ENOUGH -- 3000
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. 必须没拥有过该英雄(同英雄只能召唤一次)
|
||||||
|
local heroInfo = HeroLogic:getHero( rid, heroId )
|
||||||
|
if heroInfo and not table.empty( heroInfo ) then
|
||||||
|
return nil, ErrorCode.HERO_ALREADY_EXIST -- 3002
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4. 碎片道具数量足够
|
||||||
|
if not ItemLogic:checkItemEnough( rid, sHero.getItem, sHero.getItemNum ) then
|
||||||
|
return nil, ErrorCode.HERO_SUMMON_ITEM_NOT_ENOUGH -- 3001
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5. 扣道具 → 加英雄(确定性,无随机)
|
||||||
|
ItemLogic:delItemById( rid, sHero.getItem, sHero.getItemNum, nil,
|
||||||
|
Enum.LogType.SUMMON_HERO_COST_ITEM )
|
||||||
|
HeroLogic:addHero( rid, heroId )
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **消耗 & 产出**:
|
||||||
|
- 消耗:`HeroDefine.getItem` 物品 ×`HeroDefine.getItemNum` 个
|
||||||
|
- 产出:1 个完整英雄实例(初始 `star = sHero.initStar`, `level = 1`, `summonTime = os.time()`),并解锁该星级开放的技能(`unlockSkill`)
|
||||||
|
- **是否有概率 / 保底**:**无**。完全确定性。
|
||||||
|
- **频率限制**:每个英雄一辈子只能召唤 **1** 次(数据库以 `heroId` 为唯一键存储,再次召唤同 ID 报 `HERO_ALREADY_EXIST`)。无每日/每周限制。
|
||||||
|
- **副作用**:
|
||||||
|
- 统计 `Enum.ActivityActionType.RECRUIT_HERO_COUNT` 活动事件 +1(`HeroLogic.lua:42`)
|
||||||
|
- 触发限时礼包:`LimitTimeType.NEW_HERO`(`HeroLogic.lua:83`)
|
||||||
|
- 自动更换驻防英雄 `BuildingLogic:changeDefendHero`
|
||||||
|
- 重算战力 `RoleLogic:cacleSyncHistoryPower`
|
||||||
|
|
||||||
|
### 2.2 ⟨途径 2⟩ 酒馆开箱产出(最主要的"碎片来源")
|
||||||
|
|
||||||
|
- **名称**:酒馆召唤 / Tavern Summon / 开宝箱
|
||||||
|
- **玩家入口**:城市建筑「酒馆 Tavern」点击进入,出现两种宝箱:
|
||||||
|
- **白银宝箱 (woodbox)**:每日免费次数(按酒馆等级 `s_BuildingTavern.silverBoxCnt`)+ CD(`silverBoxCD`),免费用完后用 `silverBoxOpenItem` 道具或钻石购买
|
||||||
|
- **黄金宝箱 (goldbox)**:免费次数 1 次 + CD(`s_BuildingTavern.goldBoxCD`),其余消耗 `goldBoxOpenItem` 或钻石
|
||||||
|
- **RPC**:`Build_Tavern (407)`,参数 `{ type, free, count, useDenar }`
|
||||||
|
- `type`:`1`=白银 / `2`=黄金(`Enum.BoxType.SILVER/GOLD`)
|
||||||
|
- `count`:批量开启数量(一般 1 或 10)
|
||||||
|
- **服务端核心逻辑**(`server/game_server/logic/service/proxy/Build.lua:297-361` + `BuildingLogic.lua:openSilver/openGold`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- 简化伪码
|
||||||
|
function response.Tavern( msg )
|
||||||
|
local rid, type, free, count, useDenar = msg.rid, msg.type, msg.free, msg.count, msg.useDenar
|
||||||
|
|
||||||
|
if type == Enum.BoxType.SILVER then
|
||||||
|
if free then
|
||||||
|
-- 验免费次数 + 验 CD
|
||||||
|
if roleInfo.silverFreeCount <= 0 then return ErrorCode.BUILD_SILVER_FREE_NOT_ENOHGH end
|
||||||
|
if roleInfo.openNextSilverTime > os.time() then return ErrorCode.BUILD_SILVER_FREE_TIME_ERROR end
|
||||||
|
else
|
||||||
|
-- 验道具或钻石
|
||||||
|
if not useDenar then check item silverBoxOpenItem×silverBoxOpenItemNum*count
|
||||||
|
else check denar shopPrice*count
|
||||||
|
end
|
||||||
|
return BuildingLogic:openSilver( rid, free, count, useDenar )
|
||||||
|
elseif type == Enum.BoxType.GOLD then
|
||||||
|
...同上,配置项换成 goldBox*
|
||||||
|
return BuildingLogic:openGold( rid, free, count, useDenar )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function BuildingLogic:openSilver( _rid, _free, _count, _useDenar )
|
||||||
|
-- 扣免费次数 / 扣道具 / 扣钻石
|
||||||
|
...
|
||||||
|
-- 调用通用包随机
|
||||||
|
local groupId = CFG.s_Config:Get("silverBoxItemPackage")
|
||||||
|
local rewardInfo = ItemLogic:getItemPackage( _rid, groupId, ..., _count, true )
|
||||||
|
-- 任务进度
|
||||||
|
TaskLogic:addTaskStatisticsSum( _rid, TaskType.TAVERN_BOX, TaskTavernBoxType.SILVER, _count )
|
||||||
|
return { rewardInfo = rewardInfo, count = _count, type = Enum.BoxType.SILVER }
|
||||||
|
end
|
||||||
|
|
||||||
|
function BuildingLogic:openGold( _rid, _free, _count, __useDenar )
|
||||||
|
...同上
|
||||||
|
local groupId = CFG.s_Config:Get("goldBoxItemPackage")
|
||||||
|
-- 黄金箱首抽保底:第一次开走 goldBoxFirstReward 包
|
||||||
|
local firstOpenGold = RoleLogic:getRole( _rid, Enum.Role.firstOpenGold )
|
||||||
|
if not firstOpenGold then
|
||||||
|
groupId = CFG.s_Config:Get("goldBoxFirstReward")
|
||||||
|
RoleLogic:setRole( _rid, Enum.Role.firstOpenGold, true )
|
||||||
|
end
|
||||||
|
local rewardInfo = ItemLogic:getItemPackage( _rid, groupId, ..., _count, true )
|
||||||
|
return { rewardInfo = rewardInfo, count = _count, type = Enum.BoxType.GOLD }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **消耗 & 产出**:
|
||||||
|
- 消耗:免费次数 OR 银箱钥匙/金箱钥匙道具 OR 钻石(`s_Item.shopPrice`)
|
||||||
|
- 产出:`RewardInfo`(资源/道具/英雄实例/士兵的混合包),结构见 `Common.sproto:381-398`:
|
||||||
|
|
||||||
|
```sproto
|
||||||
|
.RewardInfo {
|
||||||
|
food/wood/stone/gold/denar 0-4 : integer
|
||||||
|
items 5 : *RewardItem # 道具数组(碎片落这里)
|
||||||
|
soldiers 6 : *SoldierInfo
|
||||||
|
groupId 7 : integer
|
||||||
|
actionForce 8 : integer
|
||||||
|
guildGifts 9 : *GuildGift
|
||||||
|
heros 10 : *Heros # 英雄数组(罕见情况直接给英雄)
|
||||||
|
...
|
||||||
|
}
|
||||||
|
.Heros { heroId 0:integer; num 1:integer; isNew 2:integer }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **是否有概率 / 保底**:**有**
|
||||||
|
- **概率**:`silverBoxItemPackage` / `goldBoxItemPackage` 配置组,每个节点有 `type/typeData/number` 三元组,由 `ItemLogic:getItemPackage` 走通用包逻辑(`ItemLogic.lua:531-`)随机产出
|
||||||
|
- **保底**:黄金箱**首次必出固定礼包** `goldBoxFirstReward`(`BuildingLogic.lua:1084-1088`,记录在 `Enum.Role.firstOpenGold` 玩家标志位);银箱无保底
|
||||||
|
- **频率限制**:
|
||||||
|
- 银箱:每日免费次数受酒馆等级限制(配置 `s_BuildingTavern.silverBoxCnt`),有冷却 `silverBoxCD`;付费无限
|
||||||
|
- 金箱:免费次数固定 1,冷却 `s_BuildingTavern.goldBoxCD`;付费无限
|
||||||
|
- **特殊行为**:
|
||||||
|
- 服务端返回的奖励里 `heros[].isNew` 标志位区分"新英雄(1)"和"已拥有英雄重复给(0)",客户端用于展示动画
|
||||||
|
- 当 ItemPackage 抽到 HERO 类型节点且玩家已拥有该英雄时,`HeroLogic:addHero` 自动把该英雄转化为对应的 `sHero.getItem` 碎片道具(`HeroLogic.lua:39-52`)—— 即"重复英雄自动转碎片"
|
||||||
|
- 任务追踪:`Enum.TaskType.TAVERN_BOX`(按银/金分类统计)
|
||||||
|
|
||||||
|
### 2.3 ⟨途径 3⟩ 雕像兑换(碎片之间的转换)
|
||||||
|
|
||||||
|
- **名称**:雕像兑换 / Exchange Hero Item
|
||||||
|
- **本质**:**不是"获得英雄",而是"道具A换成道具B"**——把通用「英雄雕像」道具换成某英雄专属的招募碎片,用于绕过抽不到某英雄碎片的尴尬。
|
||||||
|
- **RPC**:`Hero_ExchangeHeroItem (605)`,参数 `{ heroId, itemNum }`
|
||||||
|
- **服务端逻辑**(`Hero.lua:108-135`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
function response.ExchangeHeroItem( msg )
|
||||||
|
local rid, heroId, itemNum = msg.rid, msg.heroId, msg.itemNum
|
||||||
|
local sHero = CFG.s_Hero:Get( heroId )
|
||||||
|
|
||||||
|
-- sHero.exchange = 通用雕像 itemId;0 = 该英雄不支持兑换
|
||||||
|
if Enum.Exchange.NO == sHero.exchange then return nil, ErrorCode.HERO_NOT_EXCHANGE end
|
||||||
|
|
||||||
|
-- 注:此处源代码逻辑略奇怪("heroInfo 不存在但 not empty"),实际有效约束是下面这一条:
|
||||||
|
-- 已拥有该英雄 + 技能已满,禁止再兑换更多碎片(防浪费)
|
||||||
|
if table.size(heroInfo.skills) >= 4 and HeroLogic:checkHeroSkillFull( rid, heroId ) then
|
||||||
|
return nil, ErrorCode.HERO_EXCHANGE_SKILL_MAX
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 验 itemNum 个通用雕像存在
|
||||||
|
if not ItemLogic:checkItemEnough( rid, sHero.exchange, itemNum ) then
|
||||||
|
return nil, ErrorCode.HERO_EXCHANGE_ITEM_NOT_ENOUGH
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1:1 兑换:扣 itemNum 个通用雕像,加 itemNum 个该英雄招募碎片
|
||||||
|
ItemLogic:delItemById( rid, sHero.exchange, itemNum, nil, Enum.LogType.HERO_EXCHANGE_COST_ITEM )
|
||||||
|
ItemLogic:addItem( { rid = rid, itemId = sHero.getItem, itemNum = itemNum,
|
||||||
|
eventType = Enum.LogType.HERO_EXCHANGE_GAIN_ITEM } )
|
||||||
|
return { result = true }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **消耗 & 产出**:`sHero.exchange` 通用雕像 ×N → `sHero.getItem` 该英雄碎片 ×N(1:1)
|
||||||
|
- **是否有概率 / 保底**:无,确定性 1:1
|
||||||
|
- **频率限制**:玩家手动触发,无限制
|
||||||
|
- **使用场景**:玩家长期开箱攒了一堆「通用英雄雕像」但缺某英雄的特定碎片时,定向兑换补差
|
||||||
|
|
||||||
|
### 2.4 ⟨途径 4⟩ 新手引导直给(一次性)
|
||||||
|
|
||||||
|
- **名称**:完成新手"攻击野蛮人"引导奖励英雄
|
||||||
|
- **服务端逻辑**(`Role.lua:1056-1062`):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local guideHeroStage = CFG.s_Config:Get( "guideHeroStage" ) or 7
|
||||||
|
if not RoleLogic:checkGuideFinish( oldNoviceGuideStep, guideHeroStage )
|
||||||
|
and RoleLogic:checkGuideFinish( noviceGuideStep, guideHeroStage ) then
|
||||||
|
local guideHero = CFG.s_Config:Get( "guideHero" ) or 0
|
||||||
|
if guideHero > 0 then
|
||||||
|
HeroLogic:addHero( rid, guideHero )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
- **频率**:玩家全生命周期内 1 次
|
||||||
|
- **特殊表现**:客户端 `HeroCmd.cs:33-36` 会检测 `heroId == civilization.initialHero`,触发首获英雄剧情引导
|
||||||
|
|
||||||
|
### 2.5 ⟨途径 5⟩ 文明初始英雄(角色创建时)
|
||||||
|
|
||||||
|
- 玩家选定文明(civilization)后,`CivilizationDefine.initialHero` 字段定义该文明的开局赠送英雄
|
||||||
|
- 客户端 `HeroCmd.cs:30-37` 据此识别"首次得到的初始英雄",跳过普通的召唤动画走剧情
|
||||||
|
|
||||||
|
### 2.6 ⟨途径 6⟩ 通用奖励包内嵌(任务/活动/邮件/充值/商店)
|
||||||
|
|
||||||
|
- ROK 服务端不存在"任务直接给英雄"的专属代码(`TaskLogic` 不调 `HeroLogic:addHero`、`EmailLogic` 不调、`GuildShopLogic` 不调、`ActivityLogic` 仅"统计英雄等级数"不发放)
|
||||||
|
- 但**所有这些系统的奖励都走通用 `ItemLogic:getItemPackage` → `ItemLogic:addItem`**,而通用包支持 `Enum.ItemPackageType.HERO` 节点(`ItemLogic.lua:389-410`)。所以理论上:
|
||||||
|
- 任务奖励可以配置 `HERO` 节点 → 通关后直给某英雄
|
||||||
|
- 邮件附件可以挂 `HERO` 节点 → 客服补偿/活动派发可直给某英雄
|
||||||
|
- 充值礼包、商店物品同理
|
||||||
|
- 当配置成 `HERO` 类型时,重复获得已拥有英雄会自动转化为 `sHero.getItem` 碎片(见 §2.2 末尾)
|
||||||
|
|
||||||
|
### 2.7 ⟨途径 7⟩ PMLogic / GM 命令(仅运营 / 测试)
|
||||||
|
|
||||||
|
- `PMLogic.lua:481-486` `PMLogic:addHero(rid, heroId)` GM 后台命令
|
||||||
|
- `PMLogic.lua:1480-1495` `PMLogic:allLevelUp` 一键解锁所有英雄
|
||||||
|
- 玩家正式渠道不走
|
||||||
|
|
||||||
|
### 2.8 ⟨途径 8⟩ 公会复活 / 增援回调(功能恢复用,非常规获取)
|
||||||
|
|
||||||
|
- `PMLogic.lua:1197/1200/1271/1274`:在公会援军 / 阵亡复活相关回调里,针对**已死亡数据被清的成员**重新补回 mainHero / deputyHero 实例
|
||||||
|
- 实质是数据修复,不是玩家主动获取
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 碎片机制详解
|
||||||
|
|
||||||
|
### 3.1 碎片 itemId 与英雄 ID 的关联
|
||||||
|
|
||||||
|
ROK 通过 `HeroDefine.getItem`(招募所需道具 itemId)字段在配置层绑定。**没有**"英雄 ID 自动派生碎片 ID"的规则,纯查表,多对一也允许(多个英雄可以共享同一种碎片,由配表决定)。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// HeroConfig.cs (客户端 C# 镜像) ── 字段名权威源
|
||||||
|
public class HeroDefine {
|
||||||
|
public int ID; // 英雄ID
|
||||||
|
public int initStar; // 初始星级
|
||||||
|
public int star; // 星级上限
|
||||||
|
public int rare; // 稀有度 1=白 2=绿 3=蓝 4=紫 5=橙
|
||||||
|
public int score; // 基础战力
|
||||||
|
public int getItem; // ★ 招募所需道具(碎片) itemId
|
||||||
|
public int getItemNum; // ★ 招募所需道具数量
|
||||||
|
public int exchange; // 通用雕像兑换 itemId,0=不可
|
||||||
|
public int getLimit; // 王国开服天数解锁门槛
|
||||||
|
public List<int> skill; // 技能 ID 列表
|
||||||
|
public List<int> talent; // 天赋 ID 列表
|
||||||
|
public int listDisplay; // 1=不显示在英雄列表
|
||||||
|
// (...其他模型/语音字段省略)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 兑换公式
|
||||||
|
|
||||||
|
```
|
||||||
|
凑齐 sHero.getItemNum 个 sHero.getItem
|
||||||
|
→ 调用 Hero_SummonHero(heroId)
|
||||||
|
→ 扣道具
|
||||||
|
→ 永久拥有 1 个英雄(star = sHero.initStar, level = 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
无升星批量兑换、无折扣、无随机加成。**升星走单独的 `Hero_HeroStarUp (606)` 协议**,消耗的是 `s_HeroStarExp` 配置的星级道具,跟招募碎片是两套独立资源。
|
||||||
|
|
||||||
|
### 3.3 关于 `recruitLimit`(Survivors proposal 里的字段)
|
||||||
|
|
||||||
|
**ROK 不存在该字段**。等价机制:
|
||||||
|
- 同一英雄一辈子只能 `addHero` 进表 1 次(数据库以 `heroId` 为主键)
|
||||||
|
- 第二次起 `Hero_SummonHero` 返回 `HERO_ALREADY_EXIST` 错误码
|
||||||
|
- 如需让玩家持续获得"该英雄相关收益",ROK 的做法是 **重复获得的碎片可以继续累积在背包**:
|
||||||
|
- 多余碎片可用于升级技能(`HeroLogic.lua:204` `HERO_LEVEL_UP_COST_ITEM`)
|
||||||
|
- 多余碎片可参与雕像兑换流通
|
||||||
|
|
||||||
|
如果 Survivors 想保留 `recruitLimit` 概念,等价含义只能是「多少个碎片兑一次」(即 `getItemNum`),或者扩展为「同英雄可重复招募 N 次用于升星」(ROK 不是这么做的)。
|
||||||
|
|
||||||
|
### 3.4 多余碎片的处理
|
||||||
|
|
||||||
|
**保留在背包**,不自动转化。具体路径:
|
||||||
|
- 已拥有英雄 → 路径 2/6 又掉到该英雄(HERO 类型节点)→ `HeroLogic:addHero` 检测到已存在 → 自动转 `sHero.getItem` 道具(`HeroLogic.lua:43-52`)
|
||||||
|
- 已拥有英雄 → 路径 2/6 又掉到该英雄碎片(ITEM 类型节点)→ 正常进背包累加
|
||||||
|
- 玩家技能升级或雕像兑换时主动消耗多余碎片
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 抽卡 / 概率机制
|
||||||
|
|
||||||
|
### 4.1 "传统抽卡"(卡池+权重+保底+十连)—— **不存在**
|
||||||
|
|
||||||
|
排查依据:
|
||||||
|
1. `HeroLogic.lua` grep `summon|recruit|exchange|draw|gacha|lottery|pool` 全部 3 处命中均为字段名/计数器,无任何 Random 调用
|
||||||
|
2. `Hero_SummonHero` RPC 服务端实现(`Hero.lua:18-58`)从头到尾**没有任何 `Random.*` 函数调用**,完全是确定性查表 + 扣道具 + addHero
|
||||||
|
3. ROK 客户端 `View_Mediator/` 没有 `Summon/Gacha/Lottery/Draw` 目录;唯一相关的是 `Captain/CaptainSummon*` 但那是"获得英雄后的展示动画",不是抽卡入口
|
||||||
|
4. 没有 `s_HeroPool` / `s_GachaPool` / `s_Lottery` / `s_DrawWeight` 之类的配置表(在 `Server/common/service/data/config/` 下全表扫过 Hero 相关只有 7 张表,全都是属性/技能/天赋/星级 表)
|
||||||
|
|
||||||
|
### 4.2 "酒馆开箱"(已确认存在)
|
||||||
|
|
||||||
|
如 §2.2 所述:
|
||||||
|
|
||||||
|
| 项 | 银箱 | 金箱 |
|
||||||
|
|----|------|------|
|
||||||
|
| **奖励池配置** | `silverBoxItemPackage` | `goldBoxItemPackage`(首抽 `goldBoxFirstReward`) |
|
||||||
|
| **池构成** | ItemPackage 通用 group,节点类型包含 ITEM / CURRENCY / HERO / SOLDIER / SUB_ITEM_TYPE 等 | 同 |
|
||||||
|
| **权重** | 由 ItemPackage 节点的 `number` 字段及 group 内 weight 推断(实现在 `ItemLogic:getItemPackage`) | 同 |
|
||||||
|
| **保底** | 无 | 有 —— 玩家首次开金箱必走 `goldBoxFirstReward` 固定包,标志位 `Enum.Role.firstOpenGold` |
|
||||||
|
| **开法** | 免费次数 + 钥匙道具 + 钻石 | 同 |
|
||||||
|
| **每日免费次数** | `s_BuildingTavern[酒馆等级].silverBoxCnt` | `s_BuildingTavern[酒馆等级].goldBoxCnt`(实际固定为 1) |
|
||||||
|
| **CD** | `s_Config.silverBoxCD` | `s_BuildingTavern[酒馆等级].goldBoxCD` |
|
||||||
|
| **批量开** | 支持,count 字段传次数 | 同 |
|
||||||
|
| **客户端预览** | `TavernRankDefine` group=1 列表展示"可能开出的稀有奖励" | group=2 同 |
|
||||||
|
|
||||||
|
**这就是 ROK 最接近"抽卡"的机制**。但产出端**主要是英雄碎片道具**(玩家攒齐再去 `Hero_SummonHero`),偶尔直接产出英雄实例。
|
||||||
|
|
||||||
|
### 4.3 抽卡相关错误码
|
||||||
|
|
||||||
|
```
|
||||||
|
ErrorCode.lua:
|
||||||
|
BUILD_SILVER_FREE_NOT_ENOHGH 银箱免费次数不足
|
||||||
|
BUILD_SILVER_FREE_TIME_ERROR 银箱免费 CD 未到
|
||||||
|
BUILD_GOLD_FREE_NOT_ENOHGH 金箱免费次数不足
|
||||||
|
ITEM_NOT_ENOUGH 钥匙不足
|
||||||
|
ROLE_DENAR_NOT_ENOUGH 钻石不足
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 协议清单(与英雄获取相关)
|
||||||
|
|
||||||
|
| 协议号 | 名称 | 方向 | 用途 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `Build_Tavern` 407 | 酒馆召唤 / 开宝箱 | C2S | 开白银/黄金宝箱,**产出碎片或英雄的主路径** |
|
||||||
|
| `Hero_SummonHero` 601 | 召唤统帅 | C2S | 用碎片兑换具体英雄,**主获取路径** |
|
||||||
|
| `Hero_HeroInfo` 30201 | 统帅信息推送 | S2C | 服务端在新英雄获得 / 数值变更后下发,客户端 `HeroProxy.UpdateHeroInfo` 处理 |
|
||||||
|
| `Hero_ExchangeHeroItem` 605 | 雕像兑换 | C2S | 通用雕像 → 该英雄碎片(不是直接兑英雄) |
|
||||||
|
| `Hero_AddHeroExp` 602 | 加经验 | C2S | 不是获取,但同包 |
|
||||||
|
| `Hero_HeroSkillLevelUp` 603 | 技能升级 | C2S | 同上 |
|
||||||
|
| `Hero_HeroAwake` 604 | 觉醒 | C2S | 同上 |
|
||||||
|
| `Hero_HeroStarUp` 606 | 升星 | C2S | 同上 |
|
||||||
|
| `Hero_TalentUp/ChangeTalentIndex/ResetTalent/ModifyTalentName` 607-610 | 天赋系列 | C2S | 同上 |
|
||||||
|
| `Hero_HeroWearEquip / TakeOffEquip` 611-612 | 装备 | C2S | 同上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 错误码清单(与英雄获取相关)
|
||||||
|
|
||||||
|
```
|
||||||
|
HERO_OPEN_DAYS_NOT_ENOUGH 3000 召唤统帅,所在王国不满足该统帅解锁天数条件
|
||||||
|
HERO_SUMMON_ITEM_NOT_ENOUGH 3001 召唤统帅所需道具不足
|
||||||
|
HERO_ALREADY_EXIST 3002 统帅已存在
|
||||||
|
HERO_NOT_EXIST 3003 统帅不存在
|
||||||
|
HERO_NOT_EXCHANGE 3008 该统帅无法使用通用雕像兑换
|
||||||
|
HERO_EXCHANGE_SKILL_MAX 3009 统帅技能已满,无法兑换碎片
|
||||||
|
HERO_EXCHANGE_ITEM_NOT_ENOUGH 3010 通用雕像不足
|
||||||
|
|
||||||
|
# 酒馆宝箱
|
||||||
|
BUILD_SILVER_FREE_NOT_ENOHGH ... 银箱免费次数不足
|
||||||
|
BUILD_SILVER_FREE_TIME_ERROR ... 银箱免费 CD 未到
|
||||||
|
BUILD_GOLD_FREE_NOT_ENOHGH ... 金箱免费次数不足
|
||||||
|
ITEM_NOT_ENOUGH ... 钥匙道具不足
|
||||||
|
ROLE_DENAR_NOT_ENOUGH ... 钻石不足
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 与 Survivors 实现的对照点
|
||||||
|
|
||||||
|
### 7.1 之前 openspec proposal 中的 `cn.etetet.gacha` 包
|
||||||
|
|
||||||
|
**结论:名字与实际职责不匹配**。ROK 里"抽卡"实际是「酒馆开箱」+「碎片兑换」两段式,不是传统单一抽卡。建议拆分调整:
|
||||||
|
|
||||||
|
| Survivors 包 | 对应 ROK 模块 | 职责 |
|
||||||
|
|--------------|---------------|------|
|
||||||
|
| `cn.etetet.hero` | `HeroLogic.lua` + `Hero.lua` proxy | 英雄实例存储、属性、技能、星级、天赋、装备等已规划 |
|
||||||
|
| **`cn.etetet.summon`**(建议新名) | `Hero.lua: SummonHero/ExchangeHeroItem` | **碎片兑换召唤 + 雕像兑换**,对应 RPC `Hero_SummonHero` `Hero_ExchangeHeroItem` |
|
||||||
|
| **`cn.etetet.tavern`**(建议新名,或合并到 cn.etetet.building) | `BuildingLogic.lua: openSilver/openGold` + `Build.lua: Tavern` | **酒馆宝箱开启**(碎片产出端),双宝箱+免费次数+CD+保底 |
|
||||||
|
| `cn.etetet.gacha`(用户要求保留) | **ROK 没有对应** | Survivors 独立扩展:传统卡池抽卡(用于"验证产出") |
|
||||||
|
|
||||||
|
### 7.2 三种方案对比
|
||||||
|
|
||||||
|
**方案 A:严格 1:1 移植**
|
||||||
|
|
||||||
|
1. 第一阶段先实现 `cn.etetet.summon`(碎片兑换召唤)—— 与 ROK 完全一致:每个英雄 `getItem`+`getItemNum`,玩家凑齐手动召唤
|
||||||
|
2. 第二阶段实现 `cn.etetet.tavern`(酒馆双宝箱)—— 产出端的"准抽卡"
|
||||||
|
3. **不做** `cn.etetet.gacha`(传统抽卡),因为 ROK 没有
|
||||||
|
4. 优点:与原版美术 / 数值 / 设计完全对齐;
|
||||||
|
5. 缺点:与用户已表达的"还需要抽卡验证产出"诉求冲突
|
||||||
|
|
||||||
|
**方案 B:1:1 + 独立扩展抽卡**(**推荐**)
|
||||||
|
|
||||||
|
1. P0-P1 实现 `cn.etetet.summon` + `cn.etetet.tavern`:严格 1:1 ROK 的招募 + 酒馆开箱
|
||||||
|
2. P3+ 新增 `cn.etetet.gacha`:作为 Survivors 独立扩展功能,与 ROK 解耦
|
||||||
|
- 可设计成"特殊活动卡池":限时上线,权重+保底+十连
|
||||||
|
- 抽到的可以是碎片(继续走 `cn.etetet.summon` 流程兑换)或直接英雄
|
||||||
|
3. 优点:保留 ROK 设计的同时满足"测试抽卡逻辑"诉求;新功能不污染原 1:1 路径
|
||||||
|
4. 缺点:工作量略大;需要清晰区分"碎片兑换召唤"与"抽卡"在 UI/UX 上的不同入口
|
||||||
|
|
||||||
|
**方案 C:用抽卡替代碎片兑换**(不推荐)
|
||||||
|
|
||||||
|
1. 直接做 `cn.etetet.gacha`,跳过 ROK 的碎片兑换
|
||||||
|
2. 优点:开发简单
|
||||||
|
3. 缺点:偏离 ROK 设计哲学;ROK 的"碎片累积+主动召唤"是其养成长尾的核心环路,丢掉这一层会让英雄获取节奏完全不一样
|
||||||
|
|
||||||
|
### 7.3 命名重构建议
|
||||||
|
|
||||||
|
如果用户接受 `cn.etetet.summon` + `cn.etetet.tavern` + 独立的 `cn.etetet.gacha` 拆分:
|
||||||
|
|
||||||
|
```text
|
||||||
|
原 openspec proposal 涉及调整:
|
||||||
|
- 把"cn.etetet.gacha 实现 ROK 的抽卡"修改为"cn.etetet.gacha 是 Survivors 独立扩展"
|
||||||
|
- 新增 cn.etetet.summon 包(实现 Hero_SummonHero + Hero_ExchangeHeroItem 对应 RPC)
|
||||||
|
- 在 cn.etetet.building(如已规划)下新增 Tavern 子模块,或独立成 cn.etetet.tavern
|
||||||
|
- HeroDefine 字段:
|
||||||
|
fragmentItemId → 改名为 getItem (与 ROK 一致)
|
||||||
|
fragmentNum → 改名为 getItemNum (与 ROK 一致)
|
||||||
|
recruitLimit → 删除 (ROK 不存在);或解释为"同英雄重复获得碎片继续累积"
|
||||||
|
openDaysLimit → 改名为 getLimit (与 ROK 一致)
|
||||||
|
exchangeItemId → 新增 exchange (通用雕像 itemId, 0=不可)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 给用户的建议
|
||||||
|
|
||||||
|
### 推荐方案:**B (1:1 + 独立扩展抽卡)**
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 严格 1:1 还原 ROK 的「碎片兑换 + 酒馆开箱」是 Survivors 这个工程立项的核心承诺(前序对话明确"严格 1:1");这两个机制是 ROK 中英雄获取的真实主流程
|
||||||
|
2. 抽卡用户也明确要求保留("还需要抽卡的逻辑用来验证产出"),但应明确**抽卡是 Survivors 在 ROK 基础上的扩展**,不污染 1:1 路径
|
||||||
|
3. 三个包职责清晰、可独立开发、便于后期裁剪:
|
||||||
|
- `cn.etetet.summon` —— 必做(P0)
|
||||||
|
- `cn.etetet.tavern` —— 必做(P1,因为没酒馆就没碎片来源)
|
||||||
|
- `cn.etetet.gacha` —— 可延后(P3+,作为运营活动手段)
|
||||||
|
|
||||||
|
### 落地步骤建议
|
||||||
|
|
||||||
|
1. **立刻:** 更新 openspec proposal `port-rok-hero-bag-system`,把 "cn.etetet.gacha 1:1" 改为 "cn.etetet.summon + cn.etetet.tavern 1:1",新增 `cn.etetet.gacha` 作为 P3 扩展项
|
||||||
|
2. **HeroDefine 配置:** 字段命名跟 ROK 对齐(`getItem` / `getItemNum` / `getLimit` / `exchange`),不要造 `fragmentItemId` / `recruitLimit` 等新字段
|
||||||
|
3. **P0:** 实现 `Hero_SummonHero` 等价 RPC(C2G_SummonHero),扣 `getItem` × `getItemNum` → 加英雄实例,错误码对齐 `HERO_*` 段
|
||||||
|
4. **P1:** 实现 Tavern 双宝箱(C2G_TavernOpen),银 / 金 + 免费次数 + CD + 保底(金箱首抽)+ 通用 ItemPackage 引用
|
||||||
|
5. **P3+:** 独立设计 `cn.etetet.gacha` 卡池系统(权重表、保底、十连、抽卡券),可与酒馆解耦
|
||||||
|
|
||||||
|
### 风险提示
|
||||||
|
|
||||||
|
- ROK 的"碎片 → 召唤"链严重依赖通用 ItemPackage 系统(`getItemPackage`)。如果 Survivors 还没规划"通用奖励包"基础设施,需要同步规划,否则酒馆 / 任务奖励 / 商店奖励都没法落地
|
||||||
|
- ROK 的 `civilization.initialHero` / `guideHero` 等"开局直给英雄"路径与新手引导耦合,移植时需保留以保证新玩家体验
|
||||||
|
- 雕像兑换(`Hero_ExchangeHeroItem`)这条"碎片之间互换"路径在 ROK 中是中后期玩法,可放 P2 实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:关键源码位置索引(便于后续 1:1 比对)
|
||||||
|
|
||||||
|
| 主题 | 文件 | 行号 |
|
||||||
|
|------|------|------|
|
||||||
|
| 英雄数据新增 | `HeroLogic.lua` | 38-84 (`addHero`) |
|
||||||
|
| 召唤 RPC | `service/proxy/Hero.lua` | 17-58 (`SummonHero`) |
|
||||||
|
| 雕像兑换 RPC | `service/proxy/Hero.lua` | 108-135 (`ExchangeHeroItem`) |
|
||||||
|
| 酒馆 RPC | `service/proxy/Build.lua` | 296-361 (`response.Tavern`) |
|
||||||
|
| 银箱开启 | `BuildingLogic.lua` | 981-1031 (`openSilver`) |
|
||||||
|
| 金箱开启 | `BuildingLogic.lua` | 1049-1106 (`openGold`)(含首抽保底) |
|
||||||
|
| 通用奖励包随机产出 | `ItemLogic.lua` | 261-436 (`getItemPackage` 内 HERO/ITEM/SUB_ITEM_TYPE 分支) |
|
||||||
|
| 奖励包 → 加英雄 | `ItemLogic.lua` | 493-505 (heros 字段处理) |
|
||||||
|
| 协议 sproto | `protocol/Protocol.sproto` | 862-874 (Build_Tavern), 970-974 (Hero_SummonHero), 1013-1021 (Hero_ExchangeHeroItem) |
|
||||||
|
| 公共数据结构 | `protocol/Common.sproto` | 373-398 (`.Heros` `.RewardInfo`) |
|
||||||
|
| HeroDefine 字段 | `Client/.../Config/HeroConfig.cs` | 全文件 |
|
||||||
|
| 客户端发起召唤 | `Client/.../Proxy/HeroProxy.cs` | 567-572 (`SummonHero`) |
|
||||||
|
| 召唤按钮事件 | `Client/.../View_Mediator/Common/Logic/UI_Item_CaptainData_SubView.cs` | 61-65 (`OnSummonClick`) |
|
||||||
|
| 酒馆 UI | `Client/.../View_Mediator/Tavern/TavernSummonMediator.cs` | 全文件 |
|
||||||
|
| 错误码 | `Server/common/errorcode/ErrorCode.lua` | 149-176 (HERO_*) |
|
||||||
21
Doc/ROK-Luban-Field-Mapping.md
Normal file
21
Doc/ROK-Luban-Field-Mapping.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# ROK 字段到 Luban 字段映射(首版)
|
||||||
|
|
||||||
|
## HeroDefine -> Hero.bean.xml
|
||||||
|
|
||||||
|
- `id` -> `id`
|
||||||
|
- `name` -> `name`
|
||||||
|
- `rare` -> `rare`
|
||||||
|
- `skill_ids` -> `skillIds`
|
||||||
|
- `talent_ids` -> `talentIds`
|
||||||
|
|
||||||
|
## ItemDefine -> Item.bean.xml
|
||||||
|
|
||||||
|
- `id` -> `id`
|
||||||
|
- `name` -> `name`
|
||||||
|
- `type` -> `itemType`
|
||||||
|
- `icon` -> `icon`
|
||||||
|
- `desc` -> `desc`
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 本文档为迁移首版映射,后续以实际导出脚本和策划表校验结果迭代。
|
||||||
376
Doc/ROK-Spec-Index.md
Normal file
376
Doc/ROK-Spec-Index.md
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
# ROK 业务规则总索引(1:1 迁移依据)
|
||||||
|
|
||||||
|
> 来源:`E:\Game\gmd\ROK` 服务端 Lua + 客户端 C# + sproto 协议
|
||||||
|
> 用途:移植到 Survivors 工程前的业务规则全摊牌,**实现时禁止凭空发挥**
|
||||||
|
> 调研日期:2026-05-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 四份文档清单
|
||||||
|
|
||||||
|
| # | 文档 | 大小 | 摘要 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | [ROK-Hero-Spec.md](./ROK-Hero-Spec.md) | 70K / 1362 行 | 英雄系统完整业务(招募 / 升级 / 升星 / 技能 / 觉醒 / 天赋 / 装备 / 战力 / 排序 / 同步部队) |
|
||||||
|
| 2 | [ROK-Bag-Spec.md](./ROK-Bag-Spec.md) | 48K / ~770 行 | 背包系统(道具 6 大类 / itemId 编码 / 增删使用 / 礼包 / 资源货币机制) |
|
||||||
|
| 3 | [ROK-HeroAcquire-Spec.md](./ROK-HeroAcquire-Spec.md) | 34K / 456 行 | 英雄获取链路(**ROK 无抽卡**,是宝箱+碎片+主动召唤) |
|
||||||
|
| 4 | [ROK-Equip-Spec.md](./ROK-Equip-Spec.md) | 25K | 装备系统(8 槽位 / 锻造双段 RPC / 分解 / 套装 / 材料合成) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 四个核心架构结论(必读)
|
||||||
|
|
||||||
|
### 结论 1:**ROK 没有抽卡,是"宝箱产出碎片 + 主动碎片召唤"两段式**
|
||||||
|
|
||||||
|
```
|
||||||
|
酒馆宝箱(白银 / 黄金 / 免费 + 付费)
|
||||||
|
│ Build_Tavern 407 → 服务端 ItemPackage 随机产出
|
||||||
|
▼
|
||||||
|
碎片 / 资源 / 士兵 / 极少数英雄本体
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Captain UI → 点击"召唤" → Hero_SummonHero 601
|
||||||
|
│ 扣 HeroDefine.getItem × HeroDefine.getItemNum 个碎片
|
||||||
|
▼
|
||||||
|
英雄入手(每个 heroId 一辈子只能拥有 1 次)
|
||||||
|
```
|
||||||
|
|
||||||
|
**对 openspec 的影响**:
|
||||||
|
- ⚠️ 原 `cn.etetet.gacha` 包**不对应 ROK 任何业务**(ROK 没抽卡)
|
||||||
|
- ✅ 用户明确表示"想要抽卡用来验证产出",所以 `cn.etetet.gacha` **作为 Survivors 独立扩展功能保留**,但定位明确为"在 ROK 基础上**新增**",不是"移植"
|
||||||
|
- ✅ 1:1 严格迁移的入口实际是 `cn.etetet.summon`(碎片召唤)+ `cn.etetet.tavern`(酒馆宝箱),二者比抽卡更接近 ROK
|
||||||
|
|
||||||
|
### 结论 2:**资源货币(金/木/石/粮/宝石/VIP/行动力)不进背包,是 Role 字段**
|
||||||
|
|
||||||
|
ROK 把 **可堆叠的玩家货币** 和 **进背包的道具** 分得很清楚:
|
||||||
|
|
||||||
|
| 类别 | 存储方式 | 操作 API | 示例 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **货币** | Role Entity 字段 | `RoleLogic.addGold/Wood/Stone/Grain/Gem/Vip/Vitality` | 金币、木料、粮食、宝石、行动力、VIP |
|
||||||
|
| **资源类道具袋** | Item 入背包 | `ItemLogic.addItem` | 1xxxxxxxx 段的"100 金币袋",使用时调 `ItemChangeResource` 兑换为货币 |
|
||||||
|
| **装备 / 材料 / 经验书 / 其他** | Item 入背包 | `ItemLogic.addItem` | 4xxxxxxxx + 5xxxxxxxx 段 |
|
||||||
|
| **头像框 HEAD** | 不进背包 | `RoleLogic.unlockRoleHead` | 直接挂玩家解锁列表 |
|
||||||
|
|
||||||
|
**对 openspec 的影响**:
|
||||||
|
- ⚠️ 之前 `bag-system/spec.md` 写的 **"通用 Item 设计原则,全部货币也用 itemId 管理"** **与 ROK 不一致**
|
||||||
|
- ✅ 1:1 迁移应该跟随 ROK:金币 / 钻石 在 PlayerComponent 上做整型字段,而不是 BagItem
|
||||||
|
- ✅ 但"统一奖励 / 消耗结构 RewardEntry { itemId, count }" 这个**抽象层可以保留**——服务端发奖时用 RewardEntry 表达,内部分发到 Role 字段或 BagComponent
|
||||||
|
|
||||||
|
### 结论 3:**装备就是带 `heroId` 的 Item,不是独立 Entity**
|
||||||
|
|
||||||
|
| 维度 | 真相 |
|
||||||
|
|---|---|
|
||||||
|
| 装备的存储 | 背包 ItemUnit,`count=1`,`heroId>0` 表示已穿戴 |
|
||||||
|
| 英雄持有装备 | Hero Entity 上 8 个 int 字段,存 `itemIndex`(引用背包) |
|
||||||
|
| 装备槽位数 | **8 个**(head / breastPlate / weapon / gloves / pants / accessories1 / accessories2 / shoes) |
|
||||||
|
| 装备 subType 数 | **6 种**(HELMET / BREASTPLATE / ARMS / GLOVES / PANTS / ACCESSORIES / SHOES)— accessories 两槽共享同一 subType |
|
||||||
|
| 锻造 RPC | **双段**:CheckMakeEquip (50% 专属概率) → MakeEquipment (真消耗) |
|
||||||
|
| 锻造是否随机 | **必中**(材料够 + 金币够 = 必出),随机的是"专属属性" |
|
||||||
|
| 分解 | 已穿戴可直接分解(前提英雄在城内),返还固定,**无随机** |
|
||||||
|
|
||||||
|
**对 openspec 的影响**:
|
||||||
|
- ⚠️ 之前规划的"独立 `cn.etetet.equip` 包"过度设计;改为**装备子系统放在 `cn.etetet.bag` + `cn.etetet.hero`** 协作实现
|
||||||
|
- ⚠️ 之前 spec 说"8 部位"但没说映射,实际是 8 槽 / 6 subType,accessories 双槽共享,需修正
|
||||||
|
- ⚠️ 之前漏写 CheckMakeEquip 预检 RPC,需补充
|
||||||
|
|
||||||
|
### 结论 4:**英雄字段比 openspec 现有 spec 写的多得多**
|
||||||
|
|
||||||
|
英雄 Entity 上的字段(部分):
|
||||||
|
|
||||||
|
| 之前 spec 提到 | ROK 实际还有 |
|
||||||
|
|---|---|
|
||||||
|
| heroId / level / exp / star / starExp / skills / talents | + summonTime / soldierKillNum / savageKillNum / talentPoint(遗留) / talentTrees(3 套天赋页 map) / talentIndex |
|
||||||
|
| | + 8 个装备字段 |
|
||||||
|
|
||||||
|
英雄招募的真实字段名:
|
||||||
|
|
||||||
|
| openspec 用过的名字 | ROK 真名 |
|
||||||
|
|---|---|
|
||||||
|
| `fragmentItemId` | `getItem` |
|
||||||
|
| `recruitLimit` | **不存在**(每个 heroId 一辈子只能拥 1 次,重复 → `HERO_ALREADY_EXIST` 错误码) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 与 openspec 现有 spec 的差异清单(必须修正)
|
||||||
|
|
||||||
|
以下 specs 文件需要根据本调研结果调整:
|
||||||
|
|
||||||
|
### 2.1 `openspec/changes/port-rok-hero-bag-system/specs/hero-system/spec.md`
|
||||||
|
|
||||||
|
| 需修正项 | 当前写法 | ROK 真相 | 建议改法 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 招募字段名 | `fragmentItemId` / `recruitLimit` | `getItem` / `getItemNum`,无 recruitLimit | 字段名跟 ROK |
|
||||||
|
| 招募上限 | "上限"概念 | 每个 heroId 拥 1 次(重复报错) | 改成"重复招募校验" |
|
||||||
|
| 战力公式 | 通用描述 | 完整公式见 [Hero-Spec §4.10] | 引用 Doc/ROK-Hero-Spec.md |
|
||||||
|
| 天赋字段 | `TalentIndex` 单值 | `talentTrees` map(1/2/3 三套)+ `talentIndex` | 加 3 套天赋页结构 |
|
||||||
|
| 觉醒触发 | 通用描述 | 前 4 技能满级自动解锁第 5 + 独立 RPC | 双触发机制 |
|
||||||
|
| 部队同步 | 未提 | 任意英雄改动 → 更新 `RoleArmy` 战斗属性 | 加同步规则 |
|
||||||
|
|
||||||
|
### 2.2 `openspec/changes/port-rok-hero-bag-system/specs/bag-system/spec.md`
|
||||||
|
|
||||||
|
| 需修正项 | 当前写法 | ROK 真相 | 建议改法 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **通用 Item 设计** | "所有货币都走 itemId" | ROK 金/木/石/粮/宝石/VIP 是 Role 字段 | **改成"统一奖励结构 RewardEntry,内部分发到 Role 字段 / BagComponent"** |
|
||||||
|
| 道具脚本 | "ItemScript 注册机制" | ROK 没有脚本表,是 if/elseif 硬编码 | Survivors 可以做得更好(用 IItemUseHandler 注册),但要意识到 ROK 是硬编码 |
|
||||||
|
| 新道具标记 | "服务端 + 客户端分工" | ROK 全在客户端 PlayerPrefs | 跟 ROK:客户端 SaveManager 存 |
|
||||||
|
| BagItemType | "6 大类" | ROK Tab 只显 5 个(HEAD 类不入背包) | 改成 5 Tab |
|
||||||
|
| 头像框 | 未提 | HEAD 类不进背包,直走 unlockRoleHead | 加单独说明 |
|
||||||
|
| 资源阈值告警 | 未提 | `s_GameWarning.num` 大额变动通知运营 | 可保留作为 P3 |
|
||||||
|
|
||||||
|
### 2.3 `openspec/changes/port-rok-hero-bag-system/specs/equipment-system/spec.md`
|
||||||
|
|
||||||
|
| 需修正项 | 当前写法 | ROK 真相 | 建议改法 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 部位数 | "8 部位 + 4 个 EquipItemType" | 8 槽 / 6 subType(accessories 双槽共享) | 修正映射 |
|
||||||
|
| 锻造 | 单 RPC | 双 RPC(CheckMakeEquip + MakeEquipment) | 加预检 RPC |
|
||||||
|
| 专属属性 | 未提 | 50% 概率,独立 exclusive 字段 | 补充 |
|
||||||
|
| 套装 | 未提 | 2/4/6/8 件**逐级叠加**加成 | 补充 EquipComposeDefine |
|
||||||
|
| 千分比数值 | 未提 | 服务端 attAddEx 用千分比整数 | 数据类型澄清 |
|
||||||
|
|
||||||
|
### 2.4 `openspec/changes/port-rok-hero-bag-system/specs/gacha-system/spec.md`
|
||||||
|
|
||||||
|
| 需修正项 | 当前写法 | ROK 真相 | 建议改法 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **整体定位** | "1:1 移植 ROK 抽卡" | ROK 无抽卡 | **改为"独立扩展功能(非 ROK 移植)"** |
|
||||||
|
| 类比 | 借鉴 ROK 卡池 | ROK 没卡池 | 类比常见 SLG 抽卡(COK / RoK 实际游戏)或参考 gmd 姐妹工程 |
|
||||||
|
| 优先级 | P1 | 推迟到 P3 或更后 | **降级**:先做 ROK 现有的"召唤 + 酒馆",抽卡可作为 P4+ 扩展功能 |
|
||||||
|
|
||||||
|
### 2.5 `tasks.md`
|
||||||
|
|
||||||
|
需要追加的任务:
|
||||||
|
|
||||||
|
| 新增章节 | 任务 |
|
||||||
|
|---|---|
|
||||||
|
| 2.0 → 改为 2.1 | 抽卡降级到 P3+ |
|
||||||
|
| **新 2.0** | 「英雄碎片召唤 + 酒馆宝箱」(对应 ROK `Hero_SummonHero` + `Build_Tavern`) — **真正的 P1** |
|
||||||
|
| 4.1 → 替换 | 升星:星级 1-N,溢出 → 星级+1,**带幸运双倍机制** |
|
||||||
|
| 3.4 → 修正 | `C2G_CheckMakeEquip` + `C2G_MakeEquipment` 双 RPC |
|
||||||
|
| 1.11 → 修正 | 通用 Item:保留 `RewardEntry`,但**金币/钻石/资源走 Role 字段** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 推荐实现路径(在 ROK 真相基础上)
|
||||||
|
|
||||||
|
### 3.1 包结构调整
|
||||||
|
|
||||||
|
**之前 openspec 规划**:
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.hero # 英雄
|
||||||
|
cn.etetet.bag # 背包 + 装备
|
||||||
|
cn.etetet.gacha # 抽卡(ROK 无对应)
|
||||||
|
```
|
||||||
|
|
||||||
|
**根据调研推荐**:
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.hero # 英雄养成(招募 / 升级 / 升星 / 技能 / 觉醒 / 天赋 / 战力)
|
||||||
|
cn.etetet.bag # 背包 + 装备(装备是带 heroId 的 Item,不独立分包)
|
||||||
|
# 锻造放 bag/Forge/ 子目录
|
||||||
|
cn.etetet.summon # 碎片召唤 RPC(薄薄一层,主要在 hero 内复用)— 可与 cn.etetet.hero 合并
|
||||||
|
cn.etetet.tavern # 酒馆宝箱(ROK 主要碎片来源)— P2/P3 视优先级
|
||||||
|
cn.etetet.gacha # 抽卡(独立扩展,非 ROK 移植)— P3+ 或后期
|
||||||
|
```
|
||||||
|
|
||||||
|
实际可以**合并**:把 summon 并到 hero 里、tavern 单独包(因为它是独立的 UI 入口 + Build_Tavern 协议)。
|
||||||
|
|
||||||
|
### 3.2 阶段重排(基于 ROK 真相)
|
||||||
|
|
||||||
|
| 阶段 | 目标 | 端到端验证 |
|
||||||
|
|---|---|---|
|
||||||
|
| **P0** | 基础设施(DB / 日志 / GM / 配置 / Luban / 协议) | Login Demo 通 |
|
||||||
|
| **P1** | 英雄招募(碎片召唤 RPC) + 经验升级 + 通用 Item 增删 + 货币变动 | GM `AddItem 碎片 30` → `SummonHero heroId` → 英雄出现,升级 |
|
||||||
|
| **P2** | 升星 + 技能升级 + 装备穿戴 + 战力计算(套装) | GM 完整养成链路 |
|
||||||
|
| **P3** | 觉醒 + 天赋树 + 锻造(双段 RPC)+ 分解 | GM 全养成 + 装备产出闭环 |
|
||||||
|
| **P3+** | 酒馆宝箱(ROK 主要碎片来源) + 雕像兑换 | UI 之前用 GM 验证酒馆 |
|
||||||
|
| **P4** | UI 集中实现(YIUI Panel) | 全功能可视化 |
|
||||||
|
| **P4+ / 可选** | **抽卡扩展**(非 ROK 移植,独立设计) | 用户验证产出 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 实现时的"防走样"检查清单
|
||||||
|
|
||||||
|
每写一个业务函数前,**对照本索引文档 + 对应模块 spec**:
|
||||||
|
|
||||||
|
- [ ] 数据字段名是否跟 ROK(如 `getItem` 不要写成 `fragmentItemId`)
|
||||||
|
- [ ] RPC 个数是否对(如锻造 2 个不是 1 个)
|
||||||
|
- [ ] 公式是否照 ROK(如战力公式参考 ROK-Hero-Spec §4.10)
|
||||||
|
- [ ] 错误码段位是否对(3000-3027 Hero,6029-6031 BuildEquip)
|
||||||
|
- [ ] 货币走 Role 字段 vs Item,分得清吗
|
||||||
|
- [ ] 装备槽位映射对吗(8 槽 / 6 subType,accessories 双槽)
|
||||||
|
- [ ] 配置表字段全部翻译到 Luban 了吗(不要漏列)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 给用户的建议
|
||||||
|
|
||||||
|
### 5.1 必须先做
|
||||||
|
|
||||||
|
1. **修正 openspec specs**:按本索引 §2 的差异表逐项修正 `hero-system/spec.md` / `bag-system/spec.md` / `equipment-system/spec.md` / `gacha-system/spec.md`
|
||||||
|
2. **调整 proposal.md**:抽卡定位明确为"独立扩展,非 ROK 移植"
|
||||||
|
3. **调整 tasks.md**:阶段重排(按本索引 §3.2),新增"召唤 + 酒馆"任务
|
||||||
|
4. **调整 design.md D2 包组织**:把 `cn.etetet.equip` 合并到 `cn.etetet.bag`
|
||||||
|
|
||||||
|
### 5.2 可以推迟(先不调整)
|
||||||
|
|
||||||
|
- ROK 的`雕像兑换` / `天赋页改名` 等次要功能可放最后
|
||||||
|
- `酒馆宝箱` 可在 P3 才补(先用 GM `AddItem 碎片 N` 模拟产出)
|
||||||
|
- 抽卡扩展先不做,P1-P3 用 ROK 原版机制(召唤)验证产出
|
||||||
|
|
||||||
|
### 5.3 决策点请确认
|
||||||
|
|
||||||
|
| 决策点 | 选项 A | 选项 B | 推荐 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 货币是否进 Item | A. 跟 ROK:金/钻是 Player 字段 | B. 统一 Item:金/钻也是 itemId | **A**(与 ROK 一致,便于 1:1 迁移) |
|
||||||
|
| 装备分包 | A. 跟 ROK:装备并入 Bag | B. 独立 `cn.etetet.equip` 包 | **A**(与 ROK 一致) |
|
||||||
|
| 抽卡优先级 | A. P1 实现(用户要求) | B. P4+ 扩展(避免干扰 1:1 迁移) | **B**(先把 ROK 跑通,抽卡作为后续创新点) |
|
||||||
|
| 召唤 + 酒馆放哪 | A. 合并到 hero 包 | B. 独立 `cn.etetet.tavern` 包 | **A 合并到 hero,酒馆 UI 时再独立** |
|
||||||
|
|
||||||
|
确认这些决策后,我可以动手按结论修正 openspec 全部文档。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 用户最新决策与统一设计哲学(2026-05-27 确认)
|
||||||
|
|
||||||
|
> 这一节是 ROK 调研之后用户拍板的设计原则。**与 ROK 部分实现细节不一致**,但作为 Survivors 工程的最终设计哲学,后续 openspec 修改必须以此为准。
|
||||||
|
|
||||||
|
### 6.1 决策汇总
|
||||||
|
|
||||||
|
| # | 议题 | 决定 | 与 ROK 关系 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D1 | 货币管理 | **通用 Item**:金/钻/木/粮/碎片/英雄道具/装备/材料**全部** itemId 管理 | ✗ 与 ROK 不一致(ROK 把货币存 Role 字段) |
|
||||||
|
| D2 | 装备分包 | **并入 `cn.etetet.bag`**(装备是带 heroId 的 Item) | ✓ 与 ROK 一致 |
|
||||||
|
| D3 | 抽卡优先级 | **推迟 P4+**,先把 ROK 召唤+酒馆跑通 | — |
|
||||||
|
| D4 | "抽卡 / 召唤" 重新定义 | **抽卡 = 酒馆开盲盒(ItemPackage 机制);召唤 = 英雄碎片 Item → 英雄 Entity 转化** | ✓ 本质与 ROK 一致,抽象更统一 |
|
||||||
|
|
||||||
|
### 6.2 核心抽象:**"一切皆 Item,开盲盒即抽卡"**
|
||||||
|
|
||||||
|
#### 抽象 1:通用 Item
|
||||||
|
|
||||||
|
| 范畴 | itemId 示例段位 | 例子 |
|
||||||
|
|---|---|---|
|
||||||
|
| 货币类 Item | 1xxxxx (跟 ROK itemId 编码) | itemId=1 金币 / itemId=2 钻石 / itemId=3 木料 / itemId=4 粮食 |
|
||||||
|
| 资源道具袋 | 1xxxxxxx | 100 金币袋 / 1000 钻石袋(使用时扣道具+加货币 Item) |
|
||||||
|
| 加速 / 增益 / 杂项 | 2xxx / 3xxx / 5xxx | 训练加速卷、迁城卡、经验书 |
|
||||||
|
| 装备 / 材料 / 图纸 | 4xxx | 装备本体(`count=1`+`heroId>=0`)、材料、图纸 |
|
||||||
|
| **英雄碎片** | 类似 ROK `getItem` 编码 | 招募关羽碎片 / 招募张飞碎片 |
|
||||||
|
| **英雄 Item**(新增抽象) | 新增段位 | "直接获得 1 名关羽"道具,使用 → 转化为 Hero Entity |
|
||||||
|
|
||||||
|
**统一接口**(服务端 `BagComponentSystem`):
|
||||||
|
```csharp
|
||||||
|
// 任何业务发奖、扣资源都走这两个接口
|
||||||
|
AddItems(List<RewardEntry> rewards)
|
||||||
|
RemoveItems(List<CostEntry> costs)
|
||||||
|
HasItems(List<CostEntry> costs)
|
||||||
|
GetItemCount(itemId)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 抽象 2:ItemPackage(开盲盒 / 抽卡 / 礼包统一模型)
|
||||||
|
|
||||||
|
```
|
||||||
|
输入:消耗 CostEntry 列表 + 一个 packageId
|
||||||
|
内部:按 PackageConfig.weights 加权随机
|
||||||
|
输出:RewardEntry 列表(itemId, count)→ 调用 BagComponentSystem.AddItems
|
||||||
|
```
|
||||||
|
|
||||||
|
**ItemPackage 的具体业务化身**:
|
||||||
|
|
||||||
|
| 业务 | 实现 = 一次 ItemPackage 调用 |
|
||||||
|
|---|---|
|
||||||
|
| 酒馆白银宝箱(ROK Build_Tavern silverBox) | 消耗:免费次数 or 银币 / 输出:silverBoxItemPackage 加权产出 |
|
||||||
|
| 酒馆黄金宝箱(ROK Build_Tavern goldBox) | 消耗:钻石 / 输出:goldBoxItemPackage 加权产出 |
|
||||||
|
| 邮件附件领取 | 消耗:0 / 输出:邮件配置的 RewardEntry 列表(无随机) |
|
||||||
|
| 任务奖励 | 同上 |
|
||||||
|
| 礼包道具使用(gift box) | 消耗:1 个礼包 Item / 输出:固定 / 加权 RewardEntry |
|
||||||
|
| **未来"抽卡"扩展** | 消耗:抽卡券 or 钻石 / 输出:英雄碎片 / 英雄 Item / 资源等 |
|
||||||
|
|
||||||
|
> **关键洞察**:上面 6 种业务**底层是同一个函数** `ItemPackageSystem.Open(rid, packageId)`,只是 UI 入口和配置不同。
|
||||||
|
|
||||||
|
#### 抽象 3:Item → Entity 转化("召唤"机制)
|
||||||
|
|
||||||
|
```
|
||||||
|
玩家持有英雄碎片 Item(如关羽碎片 ×30)或英雄 Item(直接获得 1 名关羽)
|
||||||
|
│ 调用 UseItem 协议
|
||||||
|
▼
|
||||||
|
ItemUseHandler 分发(按 ItemDefine.useType / useScript)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
对于"英雄碎片"类型 Item:
|
||||||
|
- 校验数量足够(>= getItemNum)
|
||||||
|
- 校验该 heroId 未被拥有
|
||||||
|
- HeroComponentSystem.CreateHero(heroId)
|
||||||
|
- BagComponentSystem.RemoveItems(碎片 × getItemNum)
|
||||||
|
对于"英雄 Item"类型(直接给英雄):
|
||||||
|
- 校验该 heroId 未被拥有
|
||||||
|
- HeroComponentSystem.CreateHero(heroId)
|
||||||
|
- BagComponentSystem.RemoveItems(英雄 Item × 1)
|
||||||
|
对于"装备"类型 Item:不可使用(穿戴走另外的 RPC)
|
||||||
|
对于"经验书"类型 Item:调 HeroComponentSystem.AddExp
|
||||||
|
对于"金币袋"类型 Item:扣道具 + AddItem 金币 itemId
|
||||||
|
... 等
|
||||||
|
```
|
||||||
|
|
||||||
|
> **关键洞察**:ROK 的"召唤" 在 Survivors 不需要独立 RPC,本质就是 **UseItem 协议** 的一个分支。
|
||||||
|
|
||||||
|
### 6.3 包结构最终方案(基于 D1-D4 决策)
|
||||||
|
|
||||||
|
```
|
||||||
|
cn.etetet.bag # 核心:Item + RewardEntry + ItemPackage + Forge
|
||||||
|
├── Scripts/Model/Server/
|
||||||
|
│ ├── BagComponent.cs # 玩家背包
|
||||||
|
│ ├── ItemUnit.cs # 通用 Item 单元(含 heroId / exclusive 字段供装备用)
|
||||||
|
│ ├── ItemPackageConfig.cs # ItemPackage 配置
|
||||||
|
│ └── CurrencyItemIds.cs # 货币 itemId 常量(1=金币 2=钻石 3=木料 ...)
|
||||||
|
├── Scripts/Hotfix/Server/
|
||||||
|
│ ├── BagComponentSystem.cs # AddItems / RemoveItems / HasItems / GetItemCount
|
||||||
|
│ ├── ItemPackageSystem.cs # Open(packageId) 加权随机 → RewardEntry
|
||||||
|
│ ├── ItemUseHandler.cs # UseItem 分发(按 useType)
|
||||||
|
│ ├── Forge/ # 装备锻造子目录
|
||||||
|
│ └── UseHandlers/ # 各类道具使用 Handler(IItemUseHandler)
|
||||||
|
│ ├── HeroFragmentUseHandler.cs # 英雄碎片 → 召唤
|
||||||
|
│ ├── HeroItemUseHandler.cs # 英雄 Item → 直接获得
|
||||||
|
│ ├── ExpBookUseHandler.cs # 经验书 → 英雄加经验
|
||||||
|
│ └── CurrencyBagUseHandler.cs # 金币袋 → 加货币
|
||||||
|
|
||||||
|
cn.etetet.hero # 英雄养成(升级 / 升星 / 技能 / 觉醒 / 天赋 / 装备穿戴 / 战力)
|
||||||
|
├── 没有"招募"RPC(招募通过 UseItem 触发,统一入口)
|
||||||
|
|
||||||
|
cn.etetet.tavern # 酒馆(ItemPackage 的具体业务应用 + UI 入口)
|
||||||
|
├── Scripts/Hotfix/Server/
|
||||||
|
│ └── TavernSystem.cs # OpenSilverBox / OpenGoldBox(内部调 ItemPackageSystem.Open)
|
||||||
|
|
||||||
|
(已删除:cn.etetet.gacha / cn.etetet.equip / cn.etetet.summon — 通通不需要)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 设计哲学相比 ROK 的"升级点"
|
||||||
|
|
||||||
|
| 维度 | ROK 原版 | Survivors 升级 | 收益 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 货币管理 | Role 字段 + Item 双轨制 | 全部 Item(itemId 段位区分) | 接口统一,避免每种货币都写一遍 add/remove |
|
||||||
|
| 道具使用 | ItemLogic 巨型 if/elseif | IItemUseHandler 注册机制 | 新增道具类型加一个 Handler 类,零修改老代码 |
|
||||||
|
| 抽卡 / 宝箱 / 礼包 | 分散在 ItemLogic / Tavern / 各业务 | 统一 ItemPackage 机制 | 一处实现,N 处复用 |
|
||||||
|
| 召唤 | 独立 Hero_SummonHero 协议 | UseItem 分支 | 协议数量减少,玩家行为路径统一 |
|
||||||
|
|
||||||
|
### 6.5 与 openspec 现有 spec 的对应调整(待用户读完文档后执行)
|
||||||
|
|
||||||
|
| openspec 文档 | 主要修改 |
|
||||||
|
|---|---|
|
||||||
|
| `proposal.md` | 抽卡定位 → 推迟 P4+;新增 ItemPackage 抽象说明;包结构按 6.3 重写 |
|
||||||
|
| `design.md` D2 | 包列表按 6.3;删 gacha/equip/summon |
|
||||||
|
| `design.md` 新增 D14 | ItemPackage 设计;Item→Entity 转化机制 |
|
||||||
|
| `specs/bag-system/spec.md` | 通用 Item 覆盖货币;新增 ItemPackage 章节;新增 ItemUseHandler 注册机制;装备并入此 spec(不再分文件) |
|
||||||
|
| `specs/hero-system/spec.md` | 删"招募 RPC",改为"通过 UseItem 触发";字段名跟 ROK;战力公式引用 Doc/ROK-Hero-Spec.md |
|
||||||
|
| `specs/equipment-system/spec.md` | **合并进 `specs/bag-system/spec.md` 作为子章节**("装备子系统");删独立文件 |
|
||||||
|
| `specs/gacha-system/spec.md` | **删除**,被 ItemPackage 抽象替代;后续做扩展抽卡时再起新 change |
|
||||||
|
| `tasks.md` | P0/P1 阶段重排;新增 ItemPackage 任务;新增 ItemUseHandler 注册任务;删独立 gacha/equip 任务 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 调研未覆盖的次要功能(按需补充)
|
||||||
|
|
||||||
|
如有需要再单独调研:
|
||||||
|
|
||||||
|
- `Build_Tavern` 协议详细(酒馆开宝箱完整流程) — 需要时去看 `proxy/Build.lua` `response.Tavern`
|
||||||
|
- 雕像兑换 `Hero_ExchangeHeroItem` — `proxy/Hero.lua`
|
||||||
|
- 行动力减免与技能关联 — `HeroLogic.lua` + 战斗服务器
|
||||||
|
- 限时礼包触发 `RechargeLogic.triggerLimitPackage` — 与英雄关联但非英雄本身
|
||||||
|
- 红点系统在客户端 — `BagProxy` PlayerPrefs 部分
|
||||||
19
Doc/Server-Setup.md
Normal file
19
Doc/Server-Setup.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Server Setup
|
||||||
|
|
||||||
|
## 1. 编译
|
||||||
|
|
||||||
|
- `powershell -ExecutionPolicy Bypass -File tools/build-server.ps1`
|
||||||
|
|
||||||
|
## 2. 启动 Mongo(可选)
|
||||||
|
|
||||||
|
- 本地测试可用 memory 模式(`StartZoneConfig.DBConnection=memory`)
|
||||||
|
- Mongo 模式示例:`mongodb://127.0.0.1`
|
||||||
|
|
||||||
|
## 3. 启动服务
|
||||||
|
|
||||||
|
- Unity 中通过 Loader/ServerTools 启动,或按既有 ET 服务端流程启动
|
||||||
|
|
||||||
|
## 4. 常见问题
|
||||||
|
|
||||||
|
- 若 `ET.sln` 构建失败,优先检查缺失包/生成代码是否齐全
|
||||||
|
- 若登录失败,先看 Gate 日志与 L3 错误日志
|
||||||
16
Packages/cn.xt.bag/Ignore.ET.Bag.asmdef
Normal file
16
Packages/cn.xt.bag/Ignore.ET.Bag.asmdef
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "Ignore.ET.Bag",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [
|
||||||
|
"IGNORE"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
7
Packages/cn.xt.bag/Ignore.ET.Bag.asmdef.meta
Normal file
7
Packages/cn.xt.bag/Ignore.ET.Bag.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3241baae0a5b2f64b8b5b7550a29a238
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Proto.meta
Normal file
8
Packages/cn.xt.bag/Proto.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc1c586b2b7645d4884be6c841a6c749
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
87
Packages/cn.xt.bag/Proto/BagOuter_C_3100.proto
Normal file
87
Packages/cn.xt.bag/Proto/BagOuter_C_3100.proto
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package ET;
|
||||||
|
|
||||||
|
message ItemInfo
|
||||||
|
{
|
||||||
|
int64 itemIndex = 1;
|
||||||
|
int32 itemId = 2;
|
||||||
|
int64 count = 3;
|
||||||
|
int64 heroId = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EquipSlot
|
||||||
|
{
|
||||||
|
int32 slotIndex = 1;
|
||||||
|
int64 itemIndex = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseType R2C_UseItem
|
||||||
|
message C2G_UseItem // ISessionRequest
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
string reqId = 2;
|
||||||
|
int64 itemIndex = 3;
|
||||||
|
int32 count = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message R2C_UseItem // ISessionResponse
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
int32 Error = 2;
|
||||||
|
string Message = 3;
|
||||||
|
ItemInfo item = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseType R2C_HeroWearEquip
|
||||||
|
message C2G_HeroWearEquip // ISessionRequest
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
string reqId = 2;
|
||||||
|
int64 heroId = 3;
|
||||||
|
int64 itemIndex = 4;
|
||||||
|
int32 slotIndex = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message R2C_HeroWearEquip // ISessionResponse
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
int32 Error = 2;
|
||||||
|
string Message = 3;
|
||||||
|
int64 heroId = 4;
|
||||||
|
int64 itemIndex = 5;
|
||||||
|
int32 slotIndex = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseType R2C_TakeOffEquip
|
||||||
|
message C2G_TakeOffEquip // ISessionRequest
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
string reqId = 2;
|
||||||
|
int64 heroId = 3;
|
||||||
|
int32 slotIndex = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message R2C_TakeOffEquip // ISessionResponse
|
||||||
|
{
|
||||||
|
int32 RpcId = 1;
|
||||||
|
int32 Error = 2;
|
||||||
|
string Message = 3;
|
||||||
|
int64 heroId = 4;
|
||||||
|
int64 itemIndex = 5;
|
||||||
|
int32 slotIndex = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Item_ItemInfo // IMessage
|
||||||
|
{
|
||||||
|
ItemInfo item = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Item_ItemInfoList // IMessage
|
||||||
|
{
|
||||||
|
repeated ItemInfo items = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Item_RemoveItem // IMessage
|
||||||
|
{
|
||||||
|
int64 itemIndex = 1;
|
||||||
|
}
|
||||||
7
Packages/cn.xt.bag/Proto/BagOuter_C_3100.proto.meta
Normal file
7
Packages/cn.xt.bag/Proto/BagOuter_C_3100.proto.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 61af3e5634c96be48a9a899ab5ecac39
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts.meta
Normal file
8
Packages/cn.xt.bag/Scripts.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 75f400aa3a023c2418b2055e8e7d1788
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Hotfix.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Hotfix.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e1995c2e7bd5f5047a5a4071d92f93e6
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Hotfix/Client.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Hotfix/Client.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6bd0a469661c562428f97a8f3c739d87
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Hotfix" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9be6c6e10d671074f97616f40c2ebf0d
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace ET.Client
|
||||||
|
{
|
||||||
|
[MessageHandler(SceneType.Main)]
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public class Item_ItemInfoHandler : MessageHandler<Scene, Item_ItemInfo>
|
||||||
|
{
|
||||||
|
protected override async ETTask Run(Scene scene, Item_ItemInfo message)
|
||||||
|
{
|
||||||
|
BagComponent comp = scene.GetComponent<BagComponent>() ?? scene.AddComponent<BagComponent>();
|
||||||
|
comp.ItemMap[message.item.itemIndex] = message.item;
|
||||||
|
comp.NewItemSet.Add(message.item.itemIndex);
|
||||||
|
EventSystem.Instance.Publish(scene, new ItemChangedEvent { ItemIndex = message.item.itemIndex });
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5860a8d6e966d844682e26d1414d30a1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
namespace ET.Client
|
||||||
|
{
|
||||||
|
[MessageHandler(SceneType.Main)]
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public class Item_ItemInfoListHandler : MessageHandler<Scene, Item_ItemInfoList>
|
||||||
|
{
|
||||||
|
protected override async ETTask Run(Scene scene, Item_ItemInfoList message)
|
||||||
|
{
|
||||||
|
BagComponent comp = scene.GetComponent<BagComponent>() ?? scene.AddComponent<BagComponent>();
|
||||||
|
comp.ItemMap.Clear();
|
||||||
|
comp.NewItemSet.Clear();
|
||||||
|
foreach (ItemInfo item in message.items)
|
||||||
|
{
|
||||||
|
comp.ItemMap[item.itemIndex] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventSystem.Instance.Publish(scene, new BagUpdatedEvent { Count = message.items.Count });
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4d780f0b2ef6a3e4ca5c7d481ed155f7
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace ET.Client
|
||||||
|
{
|
||||||
|
[MessageHandler(SceneType.Main)]
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public class Item_RemoveItemHandler : MessageHandler<Scene, Item_RemoveItem>
|
||||||
|
{
|
||||||
|
protected override async ETTask Run(Scene scene, Item_RemoveItem message)
|
||||||
|
{
|
||||||
|
BagComponent comp = scene.GetComponent<BagComponent>() ?? scene.AddComponent<BagComponent>();
|
||||||
|
comp.ItemMap.Remove(message.itemIndex);
|
||||||
|
comp.NewItemSet.Remove(message.itemIndex);
|
||||||
|
EventSystem.Instance.Publish(scene, new ItemChangedEvent { ItemIndex = message.itemIndex });
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d74a1095280d0ab48984fe73d9766a79
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Hotfix/Server.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Hotfix/Server.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 73d4efcd0d7572c4eb4e54e519825d47
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Hotfix" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e853a52d3eddc454c944b92c500f7e21
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
208
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagComponentSystem.cs
Normal file
208
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagComponentSystem.cs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[FriendOf(typeof(ItemUnit))]
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public static class BagComponentSystem
|
||||||
|
{
|
||||||
|
// 约定:>=10000 的道具按装备类处理(独立实例,不堆叠)
|
||||||
|
private static bool IsEquipItem(int itemId)
|
||||||
|
{
|
||||||
|
return itemId >= 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemUnit AddItem(this BagComponent self, int itemId, long count)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsEquipItem(itemId))
|
||||||
|
{
|
||||||
|
ItemUnit stack = self.ItemMap.Values.Select(x => (ItemUnit)x).FirstOrDefault(x => x.ItemId == itemId && x.HeroId == 0);
|
||||||
|
if (stack != null)
|
||||||
|
{
|
||||||
|
stack.Count += count;
|
||||||
|
BagNoticeHelper.NotifyItemChange(self.GetParent<Player>(), stack);
|
||||||
|
if (count > 1000)
|
||||||
|
{
|
||||||
|
LogExtensions.Audit("Bag", "LargeResourceAdd", $"{{\"playerId\":{self.GetParent<Player>()?.Id ?? 0},\"itemId\":{itemId},\"count\":{count}}}");
|
||||||
|
}
|
||||||
|
return stack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long itemIndex = self.NextItemIndex++;
|
||||||
|
ItemUnit unit = self.AddChildWithId<ItemUnit, long, int, long, long>(itemIndex, itemIndex, itemId, count, 0);
|
||||||
|
self.ItemMap[itemIndex] = unit;
|
||||||
|
BagNoticeHelper.NotifyItemChange(self.GetParent<Player>(), unit);
|
||||||
|
if (count > 1000)
|
||||||
|
{
|
||||||
|
LogExtensions.Audit("Bag", "LargeResourceAdd", $"{{\"playerId\":{self.GetParent<Player>()?.Id ?? 0},\"itemId\":{itemId},\"count\":{count}}}");
|
||||||
|
}
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RemoveItem(this BagComponent self, int itemId, long count)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.GetItemCount(itemId) < count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long remain = count;
|
||||||
|
List<ItemUnit> units = self.ItemMap.Values
|
||||||
|
.Select(x => (ItemUnit)x)
|
||||||
|
.Where(x => x.ItemId == itemId && x.HeroId == 0)
|
||||||
|
.OrderBy(x => x.ItemIndex)
|
||||||
|
.ToList();
|
||||||
|
foreach (ItemUnit unit in units)
|
||||||
|
{
|
||||||
|
if (remain <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
long cost = remain > unit.Count ? unit.Count : remain;
|
||||||
|
unit.Count -= cost;
|
||||||
|
remain -= cost;
|
||||||
|
if (unit.Count <= 0)
|
||||||
|
{
|
||||||
|
self.RemoveItemByIndex(unit.ItemIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BagNoticeHelper.NotifyItemChange(self.GetParent<Player>(), unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return remain == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RemoveItemByIndex(this BagComponent self, long itemIndex)
|
||||||
|
{
|
||||||
|
if (!self.ItemMap.TryGetValue(itemIndex, out EntityRef<ItemUnit> unitRef))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
self.ItemMap.Remove(itemIndex);
|
||||||
|
BagNoticeHelper.NotifyItemRemove(self.GetParent<Player>(), itemIndex);
|
||||||
|
unit.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SetHeroIdOnEquip(this BagComponent self, long itemIndex, long heroId)
|
||||||
|
{
|
||||||
|
if (!self.ItemMap.TryGetValue(itemIndex, out EntityRef<ItemUnit> unitRef))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
unit.HeroId = heroId;
|
||||||
|
BagNoticeHelper.NotifyItemChange(self.GetParent<Player>(), unit);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddItems(this BagComponent self, List<RewardEntry> rewards)
|
||||||
|
{
|
||||||
|
if (rewards == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (RewardEntry reward in rewards)
|
||||||
|
{
|
||||||
|
self.AddItem(reward.ItemId, reward.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.GetParent<Player>()?.SetDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RemoveItems(this BagComponent self, List<CostEntry> costs)
|
||||||
|
{
|
||||||
|
if (!self.HasItems(costs))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (CostEntry cost in costs)
|
||||||
|
{
|
||||||
|
if (!self.RemoveItem(cost.ItemId, cost.Count))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.GetParent<Player>()?.SetDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long GetItemCount(this BagComponent self, int itemId)
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
foreach (EntityRef<ItemUnit> unitRef in self.ItemMap.Values)
|
||||||
|
{
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
if (unit.ItemId == itemId && unit.HeroId == 0)
|
||||||
|
{
|
||||||
|
total += unit.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasItems(this BagComponent self, List<CostEntry> costs)
|
||||||
|
{
|
||||||
|
if (costs == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (CostEntry cost in costs)
|
||||||
|
{
|
||||||
|
if (self.GetItemCount(cost.ItemId) < cost.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ItemUnit> GetAllItems(this BagComponent self)
|
||||||
|
{
|
||||||
|
return self.ItemMap.Values.Select(x => (ItemUnit)x).OrderBy(x => x.ItemIndex).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearBagKeepEquipped(this BagComponent self)
|
||||||
|
{
|
||||||
|
List<long> removed = new();
|
||||||
|
foreach (EntityRef<ItemUnit> unitRef in self.ItemMap.Values)
|
||||||
|
{
|
||||||
|
ItemUnit item = unitRef;
|
||||||
|
if (item.HeroId == 0)
|
||||||
|
{
|
||||||
|
removed.Add(item.ItemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (long itemIndex in removed)
|
||||||
|
{
|
||||||
|
self.RemoveItemByIndex(itemIndex);
|
||||||
|
}
|
||||||
|
self.GetParent<Player>()?.SetDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5bc749596ed1d84ea4cb0f6df2bdc6b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
205
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagConsoleHandlers.cs
Normal file
205
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagConsoleHandlers.cs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[FriendOf(typeof(PlayerComponent))]
|
||||||
|
public static class BagConsoleHelper
|
||||||
|
{
|
||||||
|
public static Player ResolvePlayer(Scene root, string account)
|
||||||
|
{
|
||||||
|
PlayerComponent playerComp = root.GetComponent<PlayerComponent>();
|
||||||
|
if (playerComp == null || playerComp.dictionary.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(account))
|
||||||
|
{
|
||||||
|
return playerComp.GetByAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (EntityRef<Player> value in playerComp.dictionary.Values)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] SplitArgs(string content)
|
||||||
|
{
|
||||||
|
string[] raw = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
return raw.Select(x => x.Trim()).Where(x => x.Length > 0).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleHandler("AddItem")]
|
||||||
|
public class C_AddItem : IConsoleHandler
|
||||||
|
{
|
||||||
|
// AddItem [account] itemId count
|
||||||
|
public async ETTask Run(Fiber fiber, ModeContex contex, string content)
|
||||||
|
{
|
||||||
|
string[] args = BagConsoleHelper.SplitArgs(content);
|
||||||
|
if (args.Length < 2 || args.Length > 3)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] AddItem 参数错误: AddItem [account] itemId count");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string account = args.Length == 3 ? args[0] : string.Empty;
|
||||||
|
int shift = args.Length == 3 ? 1 : 0;
|
||||||
|
if (!int.TryParse(args[0 + shift], out int itemId) || !long.TryParse(args[1 + shift], out long count))
|
||||||
|
{
|
||||||
|
Log.Console("[GM] AddItem 参数错误: itemId/count 非法");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = BagConsoleHelper.ResolvePlayer(fiber.Root, account);
|
||||||
|
BagComponent bag = player?.GetComponent<BagComponent>() ?? player?.AddComponent<BagComponent>();
|
||||||
|
if (player == null || bag == null)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] AddItem 失败: 无在线玩家");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemUnit item = bag.AddItem(itemId, count);
|
||||||
|
player.SetDirty();
|
||||||
|
LogExtensions.AuditGM("AddItem", $"{{\"account\":\"{player.Account}\",\"itemId\":{itemId},\"count\":{count},\"itemIndex\":{item?.ItemIndex ?? 0}}}");
|
||||||
|
Log.Console($"[GM] AddItem success account={player.Account} itemId={itemId} count={count}");
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleHandler("RemoveItem")]
|
||||||
|
public class C_RemoveItem : IConsoleHandler
|
||||||
|
{
|
||||||
|
// RemoveItem [account] itemId count
|
||||||
|
public async ETTask Run(Fiber fiber, ModeContex contex, string content)
|
||||||
|
{
|
||||||
|
string[] args = BagConsoleHelper.SplitArgs(content);
|
||||||
|
if (args.Length < 2 || args.Length > 3)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] RemoveItem 参数错误: RemoveItem [account] itemId count");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string account = args.Length == 3 ? args[0] : string.Empty;
|
||||||
|
int shift = args.Length == 3 ? 1 : 0;
|
||||||
|
if (!int.TryParse(args[0 + shift], out int itemId) || !long.TryParse(args[1 + shift], out long count))
|
||||||
|
{
|
||||||
|
Log.Console("[GM] RemoveItem 参数错误: itemId/count 非法");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = BagConsoleHelper.ResolvePlayer(fiber.Root, account);
|
||||||
|
BagComponent bag = player?.GetComponent<BagComponent>();
|
||||||
|
if (player == null || bag == null || !bag.RemoveItem(itemId, count))
|
||||||
|
{
|
||||||
|
Log.Console("[GM] RemoveItem 失败: 数量不足或玩家不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.SetDirty();
|
||||||
|
LogExtensions.AuditGM("RemoveItem", $"{{\"account\":\"{player.Account}\",\"itemId\":{itemId},\"count\":{count}}}");
|
||||||
|
Log.Console($"[GM] RemoveItem success account={player.Account} itemId={itemId} count={count}");
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleHandler("ShowBag")]
|
||||||
|
public class C_ShowBag : IConsoleHandler
|
||||||
|
{
|
||||||
|
// ShowBag [account]
|
||||||
|
public async ETTask Run(Fiber fiber, ModeContex contex, string content)
|
||||||
|
{
|
||||||
|
Player player = BagConsoleHelper.ResolvePlayer(fiber.Root, content.Trim());
|
||||||
|
BagComponent bag = player?.GetComponent<BagComponent>();
|
||||||
|
if (bag == null)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] ShowBag: 背包不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new();
|
||||||
|
var items = bag.GetAllItems();
|
||||||
|
sb.Append($"[GM] ShowBag account={player.Account} count={items.Count}");
|
||||||
|
foreach (ItemUnit item in items)
|
||||||
|
{
|
||||||
|
sb.Append($" | idx={item.ItemIndex},itemId={item.ItemId},count={item.Count},heroId={item.HeroId}");
|
||||||
|
}
|
||||||
|
Log.Console(sb.ToString());
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleHandler("BagClear")]
|
||||||
|
public class C_BagClear : IConsoleHandler
|
||||||
|
{
|
||||||
|
// BagClear [account]
|
||||||
|
public async ETTask Run(Fiber fiber, ModeContex contex, string content)
|
||||||
|
{
|
||||||
|
Player player = BagConsoleHelper.ResolvePlayer(fiber.Root, content.Trim());
|
||||||
|
BagComponent bag = player?.GetComponent<BagComponent>();
|
||||||
|
if (bag == null)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] BagClear: 背包不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bag.ClearBagKeepEquipped();
|
||||||
|
LogExtensions.AuditGM("BagClear", $"{{\"account\":\"{player.Account}\"}}");
|
||||||
|
Log.Console($"[GM] BagClear success account={player.Account}");
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConsoleHandler("UseItem")]
|
||||||
|
public class C_UseItem : IConsoleHandler
|
||||||
|
{
|
||||||
|
// UseItem [account] itemIndex [count]
|
||||||
|
public async ETTask Run(Fiber fiber, ModeContex contex, string content)
|
||||||
|
{
|
||||||
|
string[] args = BagConsoleHelper.SplitArgs(content);
|
||||||
|
if (args.Length < 1 || args.Length > 3)
|
||||||
|
{
|
||||||
|
Log.Console("[GM] UseItem 参数错误: UseItem [account] itemIndex [count]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shift = 0;
|
||||||
|
string account = string.Empty;
|
||||||
|
if (args.Length >= 2 && !long.TryParse(args[0], out _))
|
||||||
|
{
|
||||||
|
account = args[0];
|
||||||
|
shift = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!long.TryParse(args[shift], out long itemIndex))
|
||||||
|
{
|
||||||
|
Log.Console("[GM] UseItem 参数错误: itemIndex 非法");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 1;
|
||||||
|
if (shift + 1 < args.Length)
|
||||||
|
{
|
||||||
|
int.TryParse(args[shift + 1], out count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = BagConsoleHelper.ResolvePlayer(fiber.Root, account);
|
||||||
|
string reason = string.Empty;
|
||||||
|
ItemUnit item = null;
|
||||||
|
if (player == null || !BagService.TryUseItem(player, itemIndex, count, out reason, out item))
|
||||||
|
{
|
||||||
|
Log.Console($"[GM] UseItem 失败: {reason}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogExtensions.AuditGM("UseItem", $"{{\"account\":\"{player.Account}\",\"itemIndex\":{itemIndex},\"count\":{count},\"left\":{item?.Count ?? 0}}}");
|
||||||
|
Log.Console($"[GM] UseItem success account={player.Account} itemIndex={itemIndex} left={item?.Count ?? 0}");
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fd852d5111cbaa34c8d9eee141387ad4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
20
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagMapper.cs
Normal file
20
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagMapper.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
public static class BagMapper
|
||||||
|
{
|
||||||
|
public static ItemInfo ToProto(ItemUnit unit)
|
||||||
|
{
|
||||||
|
if (unit == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemInfo info = ItemInfo.Create();
|
||||||
|
info.itemIndex = unit.ItemIndex;
|
||||||
|
info.itemId = unit.ItemId;
|
||||||
|
info.count = unit.Count;
|
||||||
|
info.heroId = unit.HeroId;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagMapper.cs.meta
Normal file
11
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagMapper.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3424a2ac52396794593ed008ca6d8061
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
48
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagNoticeHelper.cs
Normal file
48
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagNoticeHelper.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
public static class BagNoticeHelper
|
||||||
|
{
|
||||||
|
public static void NotifyItemChange(Player player, ItemUnit unit)
|
||||||
|
{
|
||||||
|
Session session = player?.GetComponent<PlayerSessionComponent>()?.Session;
|
||||||
|
if (session == null || unit == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item_ItemInfo notice = Item_ItemInfo.Create();
|
||||||
|
notice.item = BagMapper.ToProto(unit);
|
||||||
|
session.Send(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyItemRemove(Player player, long itemIndex)
|
||||||
|
{
|
||||||
|
Session session = player?.GetComponent<PlayerSessionComponent>()?.Session;
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item_RemoveItem notice = Item_RemoveItem.Create();
|
||||||
|
notice.itemIndex = itemIndex;
|
||||||
|
session.Send(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NotifyItemList(Session session, BagComponent bag)
|
||||||
|
{
|
||||||
|
if (session == null || bag == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item_ItemInfoList list = Item_ItemInfoList.Create();
|
||||||
|
foreach (EntityRef<ItemUnit> unitRef in bag.ItemMap.Values)
|
||||||
|
{
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
list.items.Add(BagMapper.ToProto(unit));
|
||||||
|
}
|
||||||
|
session.Send(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6e2ac59cf6b717149b67aa0c5f39fda5
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
60
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagService.cs
Normal file
60
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagService.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[FriendOf(typeof(BagComponent))]
|
||||||
|
[FriendOf(typeof(ItemUnit))]
|
||||||
|
public static class BagService
|
||||||
|
{
|
||||||
|
private static bool IsEquipItem(int itemId)
|
||||||
|
{
|
||||||
|
return itemId >= 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryUseItem(Player player, long itemIndex, int count, out string reason, out ItemUnit item)
|
||||||
|
{
|
||||||
|
reason = string.Empty;
|
||||||
|
item = null;
|
||||||
|
|
||||||
|
BagComponent bag = player?.GetComponent<BagComponent>();
|
||||||
|
if (bag == null || !bag.ItemMap.TryGetValue(itemIndex, out EntityRef<ItemUnit> unitRef))
|
||||||
|
{
|
||||||
|
reason = "item_not_found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemUnit unit = unitRef;
|
||||||
|
item = unit;
|
||||||
|
if (count <= 0 || unit.Count < count)
|
||||||
|
{
|
||||||
|
reason = "count_invalid";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit.HeroId > 0)
|
||||||
|
{
|
||||||
|
reason = "equip_in_use";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEquipItem(unit.ItemId))
|
||||||
|
{
|
||||||
|
reason = "equip_can_not_use";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unit.Count -= count;
|
||||||
|
if (unit.Count <= 0)
|
||||||
|
{
|
||||||
|
bag.RemoveItemByIndex(unit.ItemIndex);
|
||||||
|
item = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BagNoticeHelper.NotifyItemChange(player, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.SetDirty();
|
||||||
|
LogExtensions.Audit("Bag", "UseItem", $"{{\"playerId\":{player.Id},\"itemIndex\":{itemIndex},\"itemId\":{unit.ItemId},\"count\":{count}}}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagService.cs.meta
Normal file
11
Packages/cn.xt.bag/Scripts/Hotfix/Server/BagService.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0ceda0fb4d1ad2641ad48a6d2c7f5a51
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[MessageSessionHandler(SceneType.Gate)]
|
||||||
|
public class C2G_UseItemHandler : RpcMessageHandler<C2G_UseItem, R2C_UseItem>
|
||||||
|
{
|
||||||
|
protected override async ETTask OnRun(Session session, C2G_UseItem request, R2C_UseItem response)
|
||||||
|
{
|
||||||
|
Player player = session.GetComponent<SessionPlayerComponent>()?.Player;
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
response.Error = ErrorCode.ERR_RpcFail;
|
||||||
|
response.Message = "player_not_found";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BagService.TryUseItem(player, request.itemIndex, request.count <= 0 ? 1 : request.count, out string reason, out ItemUnit item))
|
||||||
|
{
|
||||||
|
response.Error = ErrorCode.ERR_BAG_ITEM_NOT_ENOUGH;
|
||||||
|
response.Message = reason;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.item = BagMapper.ToProto(item);
|
||||||
|
await ETTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8891d333af566674ea212d9fae952a52
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
public static class InitialResourceHelper
|
||||||
|
{
|
||||||
|
public static void EnsureInit(Player player)
|
||||||
|
{
|
||||||
|
BagComponent bag = player.GetComponent<BagComponent>() ?? player.AddComponent<BagComponent>();
|
||||||
|
if (bag.GetAllItems().Count > 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bag.AddItems(new List<RewardEntry>
|
||||||
|
{
|
||||||
|
new() { ItemId = 1, Count = 10000 }, // 金币
|
||||||
|
new() { ItemId = 2, Count = 1000 }, // 钻石
|
||||||
|
});
|
||||||
|
|
||||||
|
LogExtensions.Audit("Bag", "InitialResourceGrant", $"{{\"playerId\":{player.Id}}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 623edf3200a689d44a13e189d99aa832
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Packages/cn.xt.bag/Scripts/Hotfix/Server/ItemUnitSystem.cs
Normal file
16
Packages/cn.xt.bag/Scripts/Hotfix/Server/ItemUnitSystem.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[EntitySystemOf(typeof(ItemUnit))]
|
||||||
|
[FriendOf(typeof(ItemUnit))]
|
||||||
|
public static partial class ItemUnitSystem
|
||||||
|
{
|
||||||
|
[EntitySystem]
|
||||||
|
private static void Awake(this ItemUnit self, long itemIndex, int itemId, long count, long heroId)
|
||||||
|
{
|
||||||
|
self.ItemIndex = itemIndex;
|
||||||
|
self.ItemId = itemId;
|
||||||
|
self.Count = count;
|
||||||
|
self.HeroId = heroId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cc19d0391ad570549a010e81b4332a57
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Model.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Model.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6678345fff2abea47b1f0dc370d57958
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Model/Client.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Model/Client.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5ec239f9554725241a664c2138da19df
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Model" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9b4313e1576e0b54f91e291c8e1f46b2
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
21
Packages/cn.xt.bag/Scripts/Model/Client/BagModels.cs
Normal file
21
Packages/cn.xt.bag/Scripts/Model/Client/BagModels.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ET.Client
|
||||||
|
{
|
||||||
|
[ComponentOf(typeof(Scene))]
|
||||||
|
public class BagComponent : Entity, IAwake, IDestroy
|
||||||
|
{
|
||||||
|
public Dictionary<long, ItemInfo> ItemMap = new();
|
||||||
|
public HashSet<long> NewItemSet = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ItemChangedEvent
|
||||||
|
{
|
||||||
|
public long ItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BagUpdatedEvent
|
||||||
|
{
|
||||||
|
public int Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Packages/cn.xt.bag/Scripts/Model/Client/BagModels.cs.meta
Normal file
11
Packages/cn.xt.bag/Scripts/Model/Client/BagModels.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2e52aeef56aa75a40808a996562f586c
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.bag/Scripts/Model/Server.meta
Normal file
8
Packages/cn.xt.bag/Scripts/Model/Server.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2b527b81222f98d4688182d27d5cb22d
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Model" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 628be4dc2891baa45a10076767b1651e
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
14
Packages/cn.xt.bag/Scripts/Model/Server/BagComponent.cs
Normal file
14
Packages/cn.xt.bag/Scripts/Model/Server/BagComponent.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[ComponentOf(typeof(Player))]
|
||||||
|
public class BagComponent : Entity, IAwake, IDestroy
|
||||||
|
{
|
||||||
|
[BsonElement]
|
||||||
|
public long NextItemIndex = 1;
|
||||||
|
[BsonElement]
|
||||||
|
public Dictionary<long, EntityRef<ItemUnit>> ItemMap = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Packages/cn.xt.bag/Scripts/Model/Server/BagComponent.cs.meta
Normal file
11
Packages/cn.xt.bag/Scripts/Model/Server/BagComponent.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 80db836d71d05ef4e95870ba5dfd0435
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
14
Packages/cn.xt.bag/Scripts/Model/Server/CommonEntries.cs
Normal file
14
Packages/cn.xt.bag/Scripts/Model/Server/CommonEntries.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
public struct RewardEntry
|
||||||
|
{
|
||||||
|
public int ItemId;
|
||||||
|
public long Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct CostEntry
|
||||||
|
{
|
||||||
|
public int ItemId;
|
||||||
|
public long Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d1eb783733a4ba24f800172359315927
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
17
Packages/cn.xt.bag/Scripts/Model/Server/ItemUnit.cs
Normal file
17
Packages/cn.xt.bag/Scripts/Model/Server/ItemUnit.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace ET.Server
|
||||||
|
{
|
||||||
|
[ChildOf(typeof(BagComponent))]
|
||||||
|
public class ItemUnit : Entity, IAwake<long, int, long, long>
|
||||||
|
{
|
||||||
|
[BsonElement]
|
||||||
|
public long ItemIndex { get; set; }
|
||||||
|
[BsonElement]
|
||||||
|
public int ItemId { get; set; }
|
||||||
|
[BsonElement]
|
||||||
|
public long Count { get; set; }
|
||||||
|
[BsonElement]
|
||||||
|
public long HeroId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Packages/cn.xt.bag/Scripts/Model/Server/ItemUnit.cs.meta
Normal file
11
Packages/cn.xt.bag/Scripts/Model/Server/ItemUnit.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff433e1ca41884446a40586231631e25
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
24
Packages/cn.xt.bag/package.json
Normal file
24
Packages/cn.xt.bag/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "cn.xt.bag",
|
||||||
|
"displayName": "ET.Bag",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"unity": "2022.3",
|
||||||
|
"description": "Bag core module for universal item storage.",
|
||||||
|
"author": {
|
||||||
|
"name": "ET-Packages"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ET-Packages/cn.etetet.bag"
|
||||||
|
},
|
||||||
|
"relatedPackages": {},
|
||||||
|
"dependencies": {
|
||||||
|
"cn.etetet.core": "3.0.3",
|
||||||
|
"cn.etetet.login": "3.0.0",
|
||||||
|
"cn.etetet.console": "3.0.0",
|
||||||
|
"cn.etetet.logging": "0.0.1"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.pkg.github.com/@ET-Packages"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Packages/cn.xt.bag/package.json.meta
Normal file
7
Packages/cn.xt.bag/package.json.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3546971ab74002240a68d08da4f16950
|
||||||
|
PackageManifestImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Packages/cn.xt.baginvoke/Ignore.ET.BagInvoke.asmdef
Normal file
16
Packages/cn.xt.baginvoke/Ignore.ET.BagInvoke.asmdef
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "Ignore.ET.BagInvoke",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [
|
||||||
|
"IGNORE"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
7
Packages/cn.xt.baginvoke/Ignore.ET.BagInvoke.asmdef.meta
Normal file
7
Packages/cn.xt.baginvoke/Ignore.ET.BagInvoke.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f4992bd58d61d64ebfea5afd1144a40
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.baginvoke/Scripts.meta
Normal file
8
Packages/cn.xt.baginvoke/Scripts.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6a1f8ef658890ba4d8d92ef7e3d845a2
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.baginvoke/Scripts/Model.meta
Normal file
8
Packages/cn.xt.baginvoke/Scripts/Model.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5581b331342b0cb419a08c9b2ff2f2ed
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.baginvoke/Scripts/Model/Share.meta
Normal file
8
Packages/cn.xt.baginvoke/Scripts/Model/Share.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f1e25def94fb8446ac1890e0d9caf6f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Model" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6f9c66ba90104c343bbbad1d30becb4b
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
public static class BagInvokeContract
|
||||||
|
{
|
||||||
|
public const string AddItems = "BagInvokeContract.AddItems";
|
||||||
|
public const string RemoveItems = "BagInvokeContract.RemoveItems";
|
||||||
|
public const string ShowBagSnapshot = "BagInvokeContract.ShowBagSnapshot";
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BagInvokeAddItems
|
||||||
|
{
|
||||||
|
public long PlayerId;
|
||||||
|
public int ItemId;
|
||||||
|
public long Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct BagInvokeRemoveItems
|
||||||
|
{
|
||||||
|
public long PlayerId;
|
||||||
|
public int ItemId;
|
||||||
|
public long Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ee50386f2d01d3c4690d3d5f17802353
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
22
Packages/cn.xt.baginvoke/package.json
Normal file
22
Packages/cn.xt.baginvoke/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "cn.xt.baginvoke",
|
||||||
|
"displayName": "ET.Bag.Invoke",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"unity": "2022.3",
|
||||||
|
"description": "Cross-package invoke contracts for bag module.",
|
||||||
|
"author": {
|
||||||
|
"name": "ET-Packages"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ET-Packages/cn.etetet.baginvoke"
|
||||||
|
},
|
||||||
|
"relatedPackages": {},
|
||||||
|
"dependencies": {
|
||||||
|
"cn.etetet.core": "3.0.3",
|
||||||
|
"cn.etetet.yiuiinvoke": "4.0.3"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.pkg.github.com/@ET-Packages"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Packages/cn.xt.baginvoke/package.json.meta
Normal file
7
Packages/cn.xt.baginvoke/package.json.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4e566574766d2884284237a424730a2a
|
||||||
|
PackageManifestImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Packages/cn.xt.config/Ignore.ET.Config.asmdef
Normal file
16
Packages/cn.xt.config/Ignore.ET.Config.asmdef
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "Ignore.ET.Config",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [
|
||||||
|
"IGNORE"
|
||||||
|
],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
7
Packages/cn.xt.config/Ignore.ET.Config.asmdef.meta
Normal file
7
Packages/cn.xt.config/Ignore.ET.Config.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 739d9581d60071d44b285ab4627db193
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.config/Scripts.meta
Normal file
8
Packages/cn.xt.config/Scripts.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e94cb38ad20d5f8438b54b522de40e25
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.config/Scripts/Hotfix.meta
Normal file
8
Packages/cn.xt.config/Scripts/Hotfix.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d33da66b61ad2cf44915a78dd5e7acef
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.config/Scripts/Hotfix/Share.meta
Normal file
8
Packages/cn.xt.config/Scripts/Hotfix/Share.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 119ed73d241d0494a97a762bf5ea10cb
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Hotfix" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 40342f8734113244381a678ba5912304
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
[EntitySystemOf(typeof(ConfigComponent))]
|
||||||
|
[FriendOf(typeof(ConfigComponent))]
|
||||||
|
public static partial class ConfigComponentSystem
|
||||||
|
{
|
||||||
|
[EntitySystem]
|
||||||
|
private static void Awake(this ConfigComponent self)
|
||||||
|
{
|
||||||
|
self.Tables = new cfg.Tables();
|
||||||
|
}
|
||||||
|
|
||||||
|
[EntitySystem]
|
||||||
|
private static void Destroy(this ConfigComponent self)
|
||||||
|
{
|
||||||
|
self.Tables = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9c022c196d3fc834d9f07cda14d7f020
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
public static class SceneConfigExtensions
|
||||||
|
{
|
||||||
|
public static cfg.Tables Cfg(this Scene self)
|
||||||
|
{
|
||||||
|
return self.GetComponent<ConfigComponent>()?.Tables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 137c4cb0950076e45aa385a93f07189a
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.config/Scripts/Model.meta
Normal file
8
Packages/cn.xt.config/Scripts/Model.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7731b355a79a2a049a3ecf58426116af
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/cn.xt.config/Scripts/Model/Share.meta
Normal file
8
Packages/cn.xt.config/Scripts/Model/Share.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6e08b9da9a727c94c96963e972d892be
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -0,0 +1 @@
|
|||||||
|
{ "reference": "ET.Model" }
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dfcd8b086e500114d98a78c0dedce2d7
|
||||||
|
AssemblyDefinitionReferenceImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
10
Packages/cn.xt.config/Scripts/Model/Share/ConfigComponent.cs
Normal file
10
Packages/cn.xt.config/Scripts/Model/Share/ConfigComponent.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using cfg;
|
||||||
|
|
||||||
|
namespace ET
|
||||||
|
{
|
||||||
|
[ComponentOf(typeof(Scene))]
|
||||||
|
public class ConfigComponent : Entity, IAwake, IDestroy
|
||||||
|
{
|
||||||
|
public Tables Tables { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4469784ae5a28204896eeea8aca490dd
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user