From 376b0fcac501b531391153b0300cf1adbfcbadd8 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Fri, 10 Jul 2026 15:30:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A3=85=E5=A4=87=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.md | 4 +- docs/dev/hero/hero-equipment.md | 204 ++++++++++++ docs/dev/template.md | 66 +++- packages-user/data-base/src/hero/attribute.ts | 124 ++++--- .../data-base/src/hero/equipStore.ts | 287 ++++++++++++++++ packages-user/data-base/src/hero/equipment.ts | 313 ++++++++++++++++++ packages-user/data-base/src/hero/index.ts | 2 + packages-user/data-base/src/hero/items.ts | 32 +- packages-user/data-base/src/hero/modifier.ts | 40 +++ packages-user/data-base/src/hero/state.ts | 9 +- packages-user/data-base/src/hero/types.ts | 251 +++++++++++++- .../data-common/src/store/tileStore.ts | 24 +- packages-user/data-common/src/store/types.ts | 16 +- packages/common/src/logger.json | 8 +- prompt.md | 211 +++++++----- 15 files changed, 1426 insertions(+), 165 deletions(-) create mode 100644 docs/dev/hero/hero-equipment.md create mode 100644 packages-user/data-base/src/hero/equipStore.ts create mode 100644 packages-user/data-base/src/hero/equipment.ts create mode 100644 packages-user/data-base/src/hero/modifier.ts diff --git a/dev.md b/dev.md index d89c11e..54daffd 100644 --- a/dev.md +++ b/dev.md @@ -104,7 +104,9 @@ - 只进行必要的非空判断,非必要时直接使用非空断言 `!`。 - 除非参数要求传入函数等情况,不建议在函数内定义局部函数。 - 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。 -- 一般不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的。 +- 极度不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的,一般是必须要提供运算符操作方法且需要监听 `get` 和 `set` 时才允许使用。 +- 方法实现中,未使用的后置参数直接不填,不得写以下划线开头的参数名。 +- 使用对象解构语法时,除非明确后续可能新增其他变量,否则不允许出现单个的对象解构,如 `const { value } = obj`,必须写成 `const value = obj.value`。 ## 双端分离 diff --git a/docs/dev/hero/hero-equipment.md b/docs/dev/hero/hero-equipment.md new file mode 100644 index 0000000..572ab20 --- /dev/null +++ b/docs/dev/hero/hero-equipment.md @@ -0,0 +1,204 @@ +# 需求综述 + +为勇士添加装备系统,实现装备的装上、卸下及属性加成。核心工作分为四层:实例存储层(`IHeroEquipsStore`,不可堆叠,以唯一 `uid` 管理每件装备)、装备状态层(`IEquipmentState`,纯数据,持有修饰器实例并负责存档读档)、属性加成层(通用 `ValueModifier` 与 `PercentageModifier`)、装备系统层(`IHeroEquipment`,操作装上/卸下,管理槽位)。 + +# 需求理解 + +## 明确需求 + +1. 独立 uid 管理。装备道具不沿用 `count` 合并计数,每件实例以自增 `uid` 唯一标识 +2. 通用修饰器加成。属性加成通过 `ValueModifier` 和 `PercentageModifier` 实现,百分比 `priority` 高于数值 +3. 修饰器存档隔离。装备修饰器不入勇士状态存档——需在 `addModifier` 中新增 `nosave` 参数 +4. 排序与对比。提供 `IEquipmentSorter` 自定义排序和 `compareEquip` 属性对比 +5. 槽位系统。槽位名称通过 `setSlots` 动态设置,装备声明可用槽位 + +## 隐含需求 + +1. 实例与定义分离。修饰器的值可独立于定义数据变化(强化通过 `setValue`),但修饰器的存在(有哪些属性)始终由定义决定 +2. 装上/卸下必须可逆。卸下后勇士属性恢复到装上前的状态,修饰器的添加和移除必须对称 +3. 重复装上处理策略。同一装备不能重复装上同一个槽,但已在其他槽的装备需通过 `autoUnload` 或直接拒绝来处理 +4. 排序影响 UI 展示。装备实例的排序结果直接影响背包和装备栏的展示顺序,排序标准由游戏作者定义 + +# 设计前提 + +1. 装备不可堆叠。装备可能拥有不同的强化状态,同名装备的属性值可以不同,因此无法像消耗品一样合并计数——每件装备都是独立个体 +2. 固定槽位序列。槽位是勇士身上若干个固定位置按顺序排列而成,每件装备只能安装到其声明兼容的槽位中 +3. 修饰器是唯一加成载体。装备对勇士属性的影响完全通过修饰器实现,不存在修饰器之外的加成方式 + +# 核心概念定义 + +## 装备 + +装备是 `ItemCategory.Equipment` 类别的道具。与永久道具和消耗道具不同,装备不可堆叠——同名但强化状态不同的两件装备拥有不同的运行时数据,必须分立管理。每件装备的定义数据中包含可安装的槽位信息和属性加成定义(数值加成与百分比加成),实际属性影响由从定义派生的修饰器产生。 + +## 装备槽 + +装备槽是勇士身上若干固定位置的序列。每个槽位由一个字符串标识其名称(如"武器"、"头盔"),由游戏作者定义。每件装备在其定义中声明兼容的槽位列表,只有目标槽位名称在声明中时方可装上。槽位索引与槽位名称是两个维度:传入槽名时可能在多个同名槽中自动选择空槽;传入索引时精确定位。同一槽位同时最多容纳一件装备;若目标槽位已被占用,装上时自动卸载原装备。 + +## 装备装上行为 + +装备装上行为是指一件装备实例与勇士身上某个槽位之间建立的绑定关系。此关系使得装备的修饰器在该期间对勇士属性生效。此关系是临时的、可逆的——绑定可随时解除。 + +## 装备卸下行为 + +装备卸下行为是指解除装备实例与槽位之间绑定关系的操作。解除后装备实例本身不会消失,仅槽位恢复为空,装备的修饰器不再影响勇士属性。 + +## 装备修饰器 + +装备修饰器是装备对勇士属性产生影响的抽象单元。每件装备从定义数据派生出若干修饰器,每个修饰器持有一个值(加成的量)并拥有一种计算规则。修饰器分为两类:固定数值加成——直接在属性值上叠加;百分比加成——基于原始基础属性按比例计算。修饰器之间具有先后关系:百分比修饰器先于数值修饰器计算。修饰器的值与其所属装备实例绑定,可随强化等操作独立变化。 + +# 接口设计分析 + +## `IEquipmentState` + +### 设计思路 + +由于装备修饰器的值会因强化而变化,独立于装备的定义数据,因此需要一个与装备实例一一对应的运行时状态对象,用于持有全部修饰器并在构造时从定义数据派生创建。修饰器本身不应直接序列化,而是保存其上游数据(即当前的 `value` 和 `percentage` 映射),读档时通过 `setValue` 恢复。这样设计也使得 `SaveCompression` 的差异化存储成为可能:`NoCompression` 完整存储,其他级别仅存与定义值不同的条目。 + +### 接口分析 + +- 成员 `uid: number`:预期频率**中频**。实例在存储层和装备系统中的唯一标识 +- 成员 `item: IItemRawData`:预期频率**中频**。装备定义数据引用,获取名称、槽位等信息 +- 成员 `modifiers: [SelectKey, IHeroModifier][]`:预期频率**中频**。该装备的全部修饰器,装上/卸下时遍历 + +### 预期体量 + +合计约 80 行。 + +- 构造器预期 15 行:两组循环分别从 `IItemEquipData.value` 和 `IItemEquipData.percentage` 创建 `ValueModifier` 和 `PercentageModifier`,修饰器数量通常为个位数 +- 存档逻辑预期 30 行:两个压缩分支,每个分支内遍历两种修饰器、比较值、写入 `Map` +- 读档逻辑预期 25 行:与存档对称,差异模式下还需处理键缺失时回退到定义数据的逻辑 + +## `IHeroEquipsStore` + +### 设计思路 + +由于装备不可堆叠(设计前提 1),需要以 `uid` 为键的独立存储模型,因此设计 `IHeroEquipsStore` 以 `Map` 管理所有实例。作为 `IHeroItems.equipment` 成员暴露,替代原有基于 `count` 的装备分表。 + +由于游戏作者可能需要自定义装备在背包中的排序,因此提供 `useSorter` 注入 `IEquipmentSorter` 回调,由 `instances` 和 `instancesOf` 调用。 + +由于装备栏 UI 需要展示装备切换前后的属性变化,因此提供 `compareEquip`,它先卸载目标槽位当前装备的修饰器,再分别模拟装上两件候选装备,通过 `getFinalAttribute` 对比差值。 + +### 接口分析 + +- 方法 `add(item: number | string): number`:预期频率**高频**。创建装备实例分配 `uid`,返回新 `uid` +- 方法 `delete(uid: number): void`:预期频率**中频**。按 `uid` 移除实例 +- 方法 `get(uid: number): IEquipmentState | null`:预期频率**高频**。获取实例状态,与是否当前装备无关 +- 方法 `count(item: number | string): number`:预期频率**中频**。按装备类型统计实例数量 +- 方法 `compareEquip(equipA: number, equipB: number, slot: number): Readonly>`:预期频率**中频**。模拟装上两件装备后对比属性差 +- 方法 `useSorter(comparer: IEquipmentSorter | null): void`:预期频率**低频**。注入自定义排序器 +- 方法 `instancesOf(equip: number | string): IEquipmentState[]`:预期频率**中频**。按类型过滤并排序 +- 方法 `instances(): IEquipmentState[]`:预期频率**中频**。排序返回全部实例 + +### 预期体量 + +合计约 120 行。 + +- 增删查预期 20 行:`add` 中包含从类型标识解析图块数字、从物品存储获取定义数据、创建 `EquipmentState`,其余为纯 `Map` 操作 +- 计数预期 8 行:遍历所有实例,按 `item.num` 比较并计数 +- 排序预期 15 行:根据是否注册了排序器分两分支,有排序器时需构造 `IEquipmentSortHandler` 并调用回调 +- 实例列表获取预期 20 行:`instances` 收集全部实例并排序,`instancesOf` 多一层类型过滤 +- 属性对比预期 35 行:两次 `clone` 属性对象,先卸载目标槽位已有装备的修饰器,再分别遍历候选装备的 `modifiers` 调用 `addModifier`,收集受影响的键并通过 `getFinalAttribute` 比对差值 +- 存档读档预期 20 行:遍历所有实例调用各自存档读档方法 + +## `IHeroEquipment` + +### 设计思路 + +由于装上/卸下操作需要同时访问装备实例存储(获取 `IEquipmentState` 及修饰器)和勇士属性对象(执行修饰器的添加与移除),且槽位占用关系需要独立维护(`Map<槽索引, uid>`),因此需要一个持有两者引用的系统层组件。 + +由于 UI 需要在装上之前判断兼容性且不能产生副作用,因此提供 `canEquipTo` 做只读检查。装上过程涉及多层判断(槽位兼容、已装检查、替换处理),因此 `equip` 返回被替换的 `uid` 供调用方跟进。卸下时按槽索引精确定位并清理,返回卸下的 `uid`。 + +由于装备栏 UI 需要按槽位顺序渲染,因此 `getEquips` 按 `slots` 顺序输出实例数组,空槽填 `null`,通过数组下标直接对应槽位。 + +### 接口分析 + +- 成员 `slots: readonly string[]`:预期频率**低频**。当前槽位名称序列 +- 方法 `setSlots(slots: string[]): void`:预期频率**低频**。运行时设置槽位名称 +- 方法 `canEquipTo(uid: number, slot: number | string): EquipStatus`:预期频率**高频**。无副作用的兼容性检查 +- 方法 `equip(uid: number, slot: number | string, autoUnload?: boolean): number | undefined`:预期频率**高频**。装上并自动处理冲突,返回被替换 `uid` +- 方法 `unequip(slot: number): number | undefined`:预期频率**中频**。按槽卸下,返回被卸下 `uid` +- 方法 `equipped(uid: number): boolean`:预期频率**高频**。按 `uid` 查询是否已在某槽 +- 方法 `getEquips(): (IEquipmentState | null)[]`:预期频率**中频**。按槽位顺序输出,空槽为 `null` + +### 预期体量 + +合计约 140 行。 + +- 兼容性检查预期 30 行:分 `typeof slot === 'number'` 和字符串两分支,数字分支直接查 `Map`,字符串分支需遍历 `slots` 查找匹配槽名并判断空槽 +- 装上逻辑预期 50 行:同 `uid` 跨槽检查与 `autoUnload` 策略、数字/字符串两分支下的槽位查找与替换、卸载旧装备后再装新装备 +- 卸下逻辑预期 15 行:按槽索引取值、遍历修饰器移除、清空槽位 +- 装备列表获取预期 15 行:按 `slots` 数组长度遍历,对每个下标查 `Map` 获取 `uid` 再查存储获取实例 +- 查询与存档读档预期 20 行:`equipped` 遍历 `Map.values()` 比对;存档写 `Map` 和 `slots`,读档清空后按存档顺序逐个 `equip` + +## 通用修饰器 + +### 设计思路 + +由于装备的属性加成只有固定数值和基于基础属性的百分比两种形式,因此提供 `ValueModifier` 和 `PercentageModifier` 两个通用修饰器即可覆盖全部需求。二者均继承 `BaseHeroModifier`,`type` 硬编码以便读档时通过工厂重建,`priority` 由构造器参数传入——装备创建时分别传 0 和 10 以确保百分比先于数值计算。 + +### 预期体量 + +合计约 40 行。 + +- 构造器与 `type` 存储预期 15 行:接收 `priority` 与 `value`,存储 `type` 为硬编码字符串 +- `modify` 方法预期 6 行:分别实现加法(`value + this.value`)和百分比计算(`value + baseValue * this.value`) +- 无独立存档逻辑:修饰器值的变化由 `EquipmentState` 的 `value`/`percentage` 映射管理 + +--- + +# 实现思路 + +## 存档差异化存储 + +1. `saveState` 时判断 `SaveCompression`:`NoCompression` 直接存入完整的 `value` 和 `percentage` 映射 +2. 其他级别将每个修饰器的当前值与 `this.item.equip` 中的原始定义值逐一比对,仅当不同时写入映射 +3. `loadState` 反向处理:`NoCompression` 对所有修饰器 `setValue`;其他级别查询存档中的值,键缺失时回退到原始定义值 + +## `compareEquip` 属性模拟 + +1. 克隆当前属性,若目标槽位已有装备,先移除其修饰器 +2. 将装备 A 的修饰器加到副本上,通过 `getFinalAttribute` 获取各属性值 +3. 对另一副本重复步骤 1 和 2,换为装备 B +4. 收集两件装备 `modifiers` 覆盖到的所有属性键,计算差值 + +# 涉及文件 + +## `@user/data-base/hero/types.ts` + +- [ ] 新增 `IEquipmentStateSave`:装备状态存档接口,按照本文描述完成设计 +- [ ] 新增 `IHeroEquipsStoreSave`:实例仓库存档接口,按照本文描述完成设计 +- [ ] 新增 `IHeroEquipmentSave`:装备系统存档接口,按照本文描述完成设计 +- [ ] 新增 `IEquipmentState`:装备状态纯数据层接口,按照本文描述完成设计 +- [ ] 新增 `IHeroEquipsStore`:装备实例存储接口,按照本文描述完成设计 +- [ ] 新增 `IEquipmentSortHandler`:排序上下文接口,按照本文描述完成设计 +- [ ] 新增 `IEquipmentSorter`:排序回调接口,按照本文描述完成设计 +- [ ] 新增 `EquipStatus`:装备兼容判断结果枚举,按照本文描述完成设计 +- [ ] 新增 `IHeroEquipment`:装备系统层接口,按照本文描述完成设计 +- [ ] 修改 `IHeroItems`:新增装备存储成员以接入装备系统 +- [ ] 修改 `IHeroAttribute.addModifier`:新增存档隔离参数 +- [ ] 修改 `IHeroStateSave`:新增装备系统成员以接入存档流程 +- [ ] 修改 `IHeroState`:新增装备系统成员以接入读档流程 + +## `@user/data-base/hero/equipment.ts` + +- [ ] 编写 `EquipmentState` 类:按照本文描述实现装备状态层 +- [ ] 编写 `HeroEquipsStore` 类:按照本文描述实现实例存储层 +- [ ] 编写 `HeroEquipment` 类:按照本文描述实现装备系统层 + +## `@user/data-base/hero/modifier.ts` + +- [ ] 编写 `ValueModifier` 类:按照本文描述实现数值修饰器 +- [ ] 编写 `PercentageModifier` 类:按照本文描述实现百分比修饰器 + +## `@user/data-base/hero/attribute.ts` + +- [ ] 修改 `HeroAttribute` 类:按照本文描述支持修饰器存档隔离 + +## `@user/data-base/hero/items.ts` + +- [ ] 修改 `HeroItems` 类:按照本文描述传递勇士属性引用并接入装备存储 + +## `@user/data-base/hero/state.ts` + +- [ ] 修改 `HeroState` 类:按照本文描述初始化装备系统并改写存档读档流程 diff --git a/docs/dev/template.md b/docs/dev/template.md index 044f4c4..dd0446c 100644 --- a/docs/dev/template.md +++ b/docs/dev/template.md @@ -4,13 +4,53 @@ 为提供更便捷的物体移动接口,故进行本设计。本设计提供统一的移动接口 `IObjectMover`,并完成其抽象实现 `abstract class ObjectMover`,以方便具体拓展。 +# 需求理解 + +## 明确需求 + +1. 移动与物体分离。移动与物体是独立的两个系统,二者之间不应存在任何耦合,移动单方面调用物体。 +2. 移动计划。移动器需要包含移动计划及其执行两个阶段,先形成计划,再进行执行。 +3. 移动流程控制。移动时需要提供相应的接口干预移动本身。 + +## 隐含需求 + +1. 异步执行。移动自然需要动画,因此移动本身必然是异步的。 +2. 移动物体。不是所有的物体都能够移动,只有某些可以,这些应该被称为可移动物体。 + +## 未确定需求 + +1. 一个物体能不能绑定两个移动器? + +# 设计前提 + +1. 网格移动。所有移动均基于离散网格,不支持连续空间移动。 +2. 允许自定义拓展。允许自定义移动策略,不是所有的移动都由某一个类统一实现。 + +# 核心概念定义 + +> 这里是一个示例,实际情况中可以更丰富一些。 + +## 移动 + +一个物体的坐标从一点改变至另一点的行为称为移动。 + +## 可移动物体 + +所有可以被移动器移动的物体都可以被称为可移动物体,它们的核心特征是包含坐标信息,并拥有设置其坐标的接口。最常见的可移动物体是地图上的动态图块以及玩家操控的勇士。 + +## 移动器 + +一种对移动的抽象接口,可以通过调用可移动物体的坐标接口来操控可移动物体,但其提供的接口往往更加丰富,且拥有计划机制,允许进行连续移动。 + # 接口设计分析 默认场景下,移动主要发生在两种物体上:地图动态图块及玩家操控的勇士。二者的主要差别在于后者由玩家操控,需要进行移动判定,例如需要进行通行性判定,事件触发判定等,而前者通常由系统代码控制,且常用于演出,并不需要进行判定。 ## IObjectMover -### 设计意图 +### 设计思路 + +> 这部分应当分析需求,并写明接口是如何设计出来的,而不是简单地阐述接口内容。 对于玩家操控,判定往往发生在移动前,因此为统一接口,设计出 `onStepStart` 与 `onStepEnd` 两个接口,前者进行判定,后者根据判定结果进行相应的操作。为了让后者知道判定结果,将 `onStepStart` 的返回值设计为 `Promise`,`onStepEnd` 接收之并执行操作。 @@ -26,18 +66,16 @@ > 此处仅列出部分。 -- `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。 -- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。 -- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。 +- `IObjectMover.forward(count?: number)`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。 +- `IObjectMover.speed(speed: number)`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。 +- `IObjectMover.stepFace(move: FaceDirection, face: FaceDirection, count?: number)`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。 ### 预期体量 -本节应当写出预期的代码体量,并分析原因。示例如下: - 预期代码体量为 200-300 行。分析如下: -- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。 -- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。 +- 计划功能预期 100 行:`IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多。 +- 移动流程预期 200 行:`IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂。 ## IMoveController @@ -45,6 +83,8 @@ # 实现思路 +> 仅列出实现思路较为复杂的内容,例如像计划存储等属于非常直观简单的内容,不需要列出实现思路。 + ## 移动流程控制 核心难点在于如何进行移动控制,以下为核心思路。 @@ -54,15 +94,7 @@ # 涉及文件 -## 需要引用的文件 - -- `lodash-es`: 引用 `lodash` 工具库,使用某些工具函数。 -- `@motajs/common`: 引用系统基本库,使用钩子相关接口。 -- `types.ts`: 引用当前子模块的类型标注文件,使用相应的接口声明。 - -## 需要修改的文件 - -### `@user/data-common/src/common/mover.ts` +## `@user/data-common/src/common/mover.ts` - [ ] 编写 `IObjectMover` 接口:按照本文描述完成设计 - [ ] 编写 `IObjectMovable` 接口:按照本文描述完成设计 diff --git a/packages-user/data-base/src/hero/attribute.ts b/packages-user/data-base/src/hero/attribute.ts index f3691aa..00639b1 100644 --- a/packages-user/data-base/src/hero/attribute.ts +++ b/packages-user/data-base/src/hero/attribute.ts @@ -1,6 +1,11 @@ import { logger } from '@motajs/common'; import { SaveCompression } from '@user/data-common'; -import { IHeroAttribute, IHeroModifier } from './types'; +import { + IHeroAttribute, + IHeroAttributeCloneOption, + IHeroModifier +} from './types'; +import { isNil } from 'lodash-es'; export abstract class BaseHeroModifier implements IHeroModifier { abstract readonly type: string; @@ -44,10 +49,9 @@ export class HeroAttribute implements IHeroAttribute { /** 当前勇士属性修饰器 */ private readonly modifier: Map = new Map(); /** 当前每个修饰器对应的属性名称 */ - private readonly modifierName: Map< - IHeroModifier, - keyof THero - > = new Map(); + private readonly modifierName: Map = new Map(); + /** 当前标记为不存档的修饰器集合 */ + private readonly modifierNosave: Set = new Set(); /** 当前勇士最终属性 */ private readonly finalAttribute: THero; @@ -58,6 +62,8 @@ export class HeroAttribute implements IHeroAttribute { this.finalAttribute = structuredClone(attribute); } + //#region 属性计算 + /** * 判定修饰器结果是否同引用 * @param curr 当前属性值 @@ -81,6 +87,7 @@ export class HeroAttribute implements IHeroAttribute { const nextValue = modifier.modify(value, baseValue, name); // 部署之后就没必要弹这个警告了,额外判断反而可能会有一定的性能损失,直接 tree-shaking 优化掉 if (import.meta.env.DEV && this.isSameReference(value, nextValue)) { + // 对于对象属性,如果返回值和原始值的引用相同,那么应该抛出警告 const modiferName = modifier.constructor.name; logger.warn(109, modiferName, String(name)); } @@ -100,6 +107,7 @@ export class HeroAttribute implements IHeroAttribute { const nextValue = modifier.modify(value, baseValue, name); // 部署之后就没必要弹这个警告了,额外判断反而可能会有一定的性能损失,直接 tree-shaking 优化掉 if (import.meta.env.DEV && this.isSameReference(value, nextValue)) { + // 对于对象属性,如果返回值和原始值的引用相同,那么应该抛出警告 const modiferName = modifier.constructor.name; logger.warn(109, modiferName, String(name)); } @@ -116,38 +124,59 @@ export class HeroAttribute implements IHeroAttribute { return this.finalAttribute[name]; } + //#endregion + + //#region 属性操作 + set(name: K, value: THero[K]): void { this.attribute[name] = value; this.markDirty(name); } - add>( - name: K, - value: number - ): void { + add(name: SelectKey, value: number): void { (this.attribute[name] as number) += value; this.markDirty(name); } - mul>( - name: K, - value: number - ): void { + mul(name: SelectKey, value: number): void { (this.attribute[name] as number) *= value; this.markDirty(name); } - div>( - name: K, - value: number - ): void { + div(name: SelectKey, value: number): void { (this.attribute[name] as number) /= value; this.markDirty(name); } + //#endregion + + //#region 修饰器处理 + + *iterateModifiers(): IterableIterator<[PropertyKey, IHeroModifier]> { + for (const [modifier, name] of this.modifierName) { + yield [name, modifier]; + } + } + + getModifiers( + name: K + ): Iterable> { + const arr = this.modifier.get(name) as IHeroModifier[]; + return arr ?? []; + } + + getModifierIndex(modifier: IHeroModifier): number { + const name = this.modifierName.get(modifier); + if (isNil(name)) return -1; + const arr = this.modifier.get(name); + if (!arr) return -1; + return arr.indexOf(modifier); + } + addModifier( name: K, - modifier: IHeroModifier + modifier: IHeroModifier, + save: boolean = true ): void { if (modifier.owner) { const modiferName = modifier.constructor.name; @@ -160,6 +189,9 @@ export class HeroAttribute implements IHeroAttribute { modifierList.sort((left, right) => right.priority - left.priority); this.modifierName.set(modifier, name); + if (!save) { + this.modifierNosave.add(modifier); + } modifier.bindAttribute(this); this.markDirty(name); } @@ -176,10 +208,22 @@ export class HeroAttribute implements IHeroAttribute { modifier.bindAttribute(null); modifierList.splice(index, 1); this.modifierName.delete(modifier); + this.modifierNosave.delete(modifier); this.markDirty(name); } + deleteModifierByIndex( + name: K, + index: number + ): IHeroModifier | null { + const arr = this.modifier.get(name); + if (!arr) return null; + const modifier = arr.splice(index, 1); + if (modifier.length === 0) return null; + else return modifier[0] as IHeroModifier; + } + markDirty(name: keyof THero): void { this.recalculateAttribute(name); } @@ -190,17 +234,34 @@ export class HeroAttribute implements IHeroAttribute { this.markDirty(name); } - clone(cloneModifier: boolean = true): IHeroAttribute { + setModifierSaveEnabled(modifier: IHeroModifier, save: boolean): void { + if (save) { + this.modifierNosave.delete(modifier); + } else { + this.modifierNosave.add(modifier); + } + } + + getModifierSaveEnabled(modifier: IHeroModifier): boolean { + return !this.modifierNosave.has(modifier); + } + + //#endregion + + //#region 属性克隆 + + clone( + option: Readonly> = {} + ): IHeroAttribute { + const { cloneModifier = true } = option; const cloned = new HeroAttribute( structuredClone(this.attribute) ); if (!cloneModifier) return cloned; - // 拷贝修饰器 - for (const [modifier, name] of this.modifierName) { - cloned.addModifier( - name, - modifier.clone() as IHeroModifier - ); + for (const [name, modifiers] of this.modifier) { + const arr: IHeroModifier[] = modifiers.map(v => v.clone()); + cloned.modifier.set(name, arr); + cloned.recalculateAttribute(name); } return cloned; } @@ -213,16 +274,5 @@ export class HeroAttribute implements IHeroAttribute { return structuredClone(this.attribute); } - *iterateModifiers(): IterableIterator<[PropertyKey, IHeroModifier]> { - for (const [modifier, name] of this.modifierName) { - yield [name, modifier]; - } - } - - getModifiers( - name: K - ): Iterable> { - const arr = this.modifier.get(name) as IHeroModifier[]; - return arr ?? []; - } + //#endregion } diff --git a/packages-user/data-base/src/hero/equipStore.ts b/packages-user/data-base/src/hero/equipStore.ts new file mode 100644 index 0000000..aedd235 --- /dev/null +++ b/packages-user/data-base/src/hero/equipStore.ts @@ -0,0 +1,287 @@ +import { IItemRawData, SaveCompression, IDataCommon } from '@user/data-common'; +import { isNil, maxBy } from 'lodash-es'; +import { ValueModifier, PercentageModifier } from './modifier'; +import { + IEquipmentState, + IHeroModifier, + IEquipmentStateSave, + IHeroEquipsStore, + IEquipmentSorter, + IEquipmentSortHandler, + IHeroEquipsStoreSave +} from './types'; +import { logger } from '@motajs/common'; + +export class EquipmentState implements IEquipmentState { + readonly item: IItemRawData; + + /** 装备产生的所有修饰器,每个元素为 [属性名, 修饰器] */ + readonly modifiers: [SelectKey, IHeroModifier][]; + + /** 装备的数值属性 */ + private readonly value: Map, number>; + /** 装备的百分比属性 */ + private readonly percentage: Map, number>; + + constructor( + readonly uid: number, + item: IItemRawData, + buildModifier: boolean = true + ) { + this.item = item; + this.modifiers = []; + + const equip = item.equip; + this.value = new Map(equip.value); + this.percentage = new Map(equip.percentage); + + if (buildModifier) { + this.rebuildModifiers(); + } + } + + /** + * 从装备属性重新生成装备修饰器 + */ + private rebuildModifiers() { + this.modifiers.length = 0; + + for (const [name, baseVal] of this.value) { + this.modifiers.push([name, new ValueModifier(baseVal)]); + } + for (const [name, basePct] of this.percentage) { + this.modifiers.push([name, new PercentageModifier(basePct)]); + } + } + + getModifiers(): Iterable< + [keyof SelectType, IHeroModifier] + > { + return this.modifiers; + } + + /** + * NoCompression 级别保存:完整存储所有修饰器值 + * @returns 完整的装备状态存档 + */ + private saveNoCompression(): IEquipmentStateSave { + return { + uid: this.uid, + num: this.item.num, + value: new Map(this.value), + percentage: new Map(this.percentage) + }; + } + + /** + * Low / High 压缩级别保存:仅存储与原始定义存在差异的条目 + * @returns 差异化的装备状态存档 + */ + private saveDiff(): IEquipmentStateSave { + const { value, percentage } = this.item.equip; + const valueDiff = new Map, number>(); + for (const [name, equipValue] of this.value) { + const base = value.get(name); + if (base !== equipValue) { + valueDiff.set(name, equipValue); + } + } + const perDiff = new Map, number>(); + for (const [name, equipPer] of this.percentage) { + const base = percentage.get(name); + if (base !== equipPer) { + perDiff.set(name, equipPer); + } + } + return { + uid: this.uid, + num: this.item.num, + value: valueDiff, + percentage: perDiff + }; + } + + saveState(compression: SaveCompression): IEquipmentStateSave { + if (compression === SaveCompression.NoCompression) { + return this.saveNoCompression(); + } else { + return this.saveDiff(); + } + } + + /** + * NoCompression 级别读档:使用完整存档值覆盖所有修饰器 + * @param state 装备状态存档 + */ + private loadNoCompression(state: IEquipmentStateSave): void { + this.value.clear(); + this.percentage.clear(); + for (const [name, value] of state.percentage) { + this.value.set(name, value); + } + for (const [name, value] of state.percentage) { + this.percentage.set(name, value); + } + this.rebuildModifiers(); + } + + /** + * Low / High 压缩级别读档:从存档查询值,缺失则回退到原始定义 + * @param state 装备状态存档 + */ + private loadDiff(state: IEquipmentStateSave): void { + this.value.clear(); + this.percentage.clear(); + for (const [name, value] of state.percentage) { + this.value.set(name, value); + } + for (const [name, value] of state.percentage) { + this.percentage.set(name, value); + } + + // 差异内容 + for (const [name, value] of state.value) { + this.value.set(name, value); + } + for (const [name, value] of state.percentage) { + this.percentage.set(name, value); + } + + this.rebuildModifiers(); + } + + loadState( + state: IEquipmentStateSave, + compression: SaveCompression + ): void { + if (compression === SaveCompression.NoCompression) { + this.loadNoCompression(state); + } else { + this.loadDiff(state); + } + } +} + +export class HeroEquipsStore implements IHeroEquipsStore { + /** 装备实例存储表 */ + private readonly instanceMap: Map> = + new Map(); + + /** 自增 uid 计数器 */ + private nextUid: number = 0; + + /** 排序器 */ + private sorter: IEquipmentSorter | null = null; + + readonly state: IDataCommon; + + constructor(state: IDataCommon) { + this.state = state; + } + + add(item: number | string): number { + const num = this.state.tileStore.num(item); + if (isNil(num)) return -1; + + const raw = this.state.itemStore.getData(num!); + if (!raw) return -1; + + const uid = this.nextUid++; + const state = new EquipmentState(uid, raw); + this.instanceMap.set(uid, state); + return uid; + } + + delete(uid: number): void { + this.instanceMap.delete(uid); + } + + get(uid: number): IEquipmentState | null { + return this.instanceMap.get(uid) ?? null; + } + + count(item: number | string): number { + const num = this.state.tileStore.num(item); + if (isNil(num)) return 0; + + let count = 0; + for (const state of this.instanceMap.values()) { + if (state.item.num === num) count++; + } + return count; + } + + useSorter(comparer: IEquipmentSorter | null): void { + this.sorter = comparer; + } + + /** + * 按当前排序器对所有装备实例排序,无排序器时按 uid 升序 + * @param list 装备实例数组 + * @returns 排序后的数组 + */ + private sortList(list: EquipmentState[]): EquipmentState[] { + const sorter = this.sorter; + if (sorter) { + return list.sort((a, b) => { + const handler: IEquipmentSortHandler = { + equipA: a, + equipB: b, + store: this, + state: this.state + }; + const cmp = sorter.compare(handler); + if (cmp === 0) { + return a.uid - b.uid; + } else { + return cmp; + } + }); + } else { + return list.sort((a, b) => a.uid - b.uid); + } + } + + instancesOf(equip: number | string): IEquipmentState[] { + const num = this.state.tileStore.num(equip); + if (isNil(num)) return []; + return this.sortList( + [...this.instanceMap.values()].filter(v => v.item.num === num) + ); + } + + instances(): IEquipmentState[] { + return this.sortList([...this.instanceMap.values()]); + } + + saveState(compression: SaveCompression): IHeroEquipsStoreSave { + const equipments: IEquipmentStateSave[] = []; + for (const state of this.instanceMap.values()) { + equipments.push(state.saveState(compression)); + } + return { equipments }; + } + + loadState( + state: IHeroEquipsStoreSave, + compression: SaveCompression + ): void { + this.instanceMap.clear(); + for (const save of state.equipments) { + const raw = this.state.itemStore.getData(save.num); + if (!raw) { + logger.error(59, save.num.toString()); + continue; + } + const instance = new EquipmentState(save.uid, raw, false); + instance.loadState(save, compression); + this.instanceMap.set(save.uid, instance); + } + const maxUid = maxBy(state.equipments, 'uid'); + if (!maxUid) { + logger.error(58); + return; + } + this.nextUid = maxUid.uid + 1; + } +} diff --git a/packages-user/data-base/src/hero/equipment.ts b/packages-user/data-base/src/hero/equipment.ts new file mode 100644 index 0000000..e9bda0f --- /dev/null +++ b/packages-user/data-base/src/hero/equipment.ts @@ -0,0 +1,313 @@ +import { isNil } from 'lodash-es'; +import { IDataCommon } from '@user/data-common'; +import { + EquipStatus, + IEquipmentState, + IHeroAttribute, + IHeroEquipment, + IHeroEquipmentSave, + IHeroEquipsStore +} from './types'; +import { logger } from '@motajs/common'; + +export class HeroEquipment implements IHeroEquipment { + /** 当前已装备的装备,键表示装备槽索引,值表示装备至这个装备槽的装备 uid */ + private readonly equips: Map = new Map(); + + slots: string[] = []; + + readonly state: IDataCommon; + + constructor( + private readonly store: IHeroEquipsStore, + private readonly attribute: IHeroAttribute + ) { + this.state = this.store.state; + } + + setSlots(slots: string[]): void { + this.slots = slots; + } + + canEquipTo(uid: number, slot: number | string): EquipStatus { + const state = this.store.get(uid); + if (!state) return EquipStatus.CannotEquip; + const raw = state.item; + const slots = raw.equip.slots; + + // 如果装备压根不支持这个槽位 + if (!slots.includes(slot)) { + return EquipStatus.CannotEquip; + } + + if (typeof slot === 'number') { + // 如果传入的 slot 是指定索引 + if (!this.equips.has(slot)) { + return EquipStatus.CanEquip; + } else { + return EquipStatus.NeedReplace; + } + } else { + // 如果传入的 slot 是槽位名称,那么要对每个槽位都判断 + let hasEmpty = false; + let hasSlot = false; + this.slots.forEach((name, index) => { + if (slots.includes(name) && !this.equips.has(index)) { + hasEmpty = true; + } + if (name === slot) { + hasSlot = true; + } + }); + if (hasSlot) { + if (hasEmpty) { + return EquipStatus.CanEquip; + } else { + return EquipStatus.NeedReplace; + } + } else { + return EquipStatus.CannotEquip; + } + } + } + + //#region 装备行为 + + /** + * 向勇士属性施加装备的修饰器 + * @param state 装备状态实例 + */ + private loadEquipEffect(state: IEquipmentState) { + for (const [name, modifier] of state.getModifiers()) { + // @ts-expect-error 泛型无法推导 + this.attribute.addModifier(name, modifier, true); + } + } + + /** + * 从勇士属性上删除装备的修饰器 + * @param state 装备状态实例 + */ + private unloadEquipEffect(state: IEquipmentState) { + for (const [name, modifier] of state.getModifiers()) { + // @ts-expect-error 泛型无法推导 + this.attribute.deleteModifier(name, modifier); + } + } + + equip( + uid: number, + slot: number | string, + autoUnload: boolean = true + ): number | undefined { + if (this.canEquipTo(uid, slot) === EquipStatus.CannotEquip) { + return void 0; + } + + // 检查有没有同 uid 装备 + for (const [index, curr] of this.equips) { + if (curr !== uid) continue; + if (index === slot || this.slots[index] === slot) { + // 指定装备已经装备至了指定装备槽,直接忽略 + return void 0; + } else { + // 否则看 autoUnload + if (autoUnload) { + this.unequip(index); + break; + } else { + return void 0; + } + } + } + + const state = this.store.get(uid); + if (!state) { + logger.warn(146, uid.toString()); + return void 0; + } + + if (typeof slot === 'number') { + // 数字槽位,直接进行指定替换 + const curr = this.equips.get(slot); + this.unequip(slot); + this.equips.set(slot, uid); + this.loadEquipEffect(state); + return curr; + } else { + // 字符串槽位,需要判断是否包含空槽 + let first = -1; + let empty = -1; + this.slots.forEach((name, index) => { + if (name !== slot) return; + if (first === -1) first = index; + if (empty !== -1 && !this.equips.has(index)) { + empty = index; + } + }); + if (empty === -1) { + // 无空槽,替换第一个匹配的槽位 + if (first === -1) { + logger.warn(147, uid.toString()); + return void 0; + } else { + const curr = this.equips.get(first); + this.unequip(first); + this.equips.set(first, uid); + this.loadEquipEffect(state); + return curr; + } + } else { + // 此时有空槽,直接装备上就行 + this.equips.set(empty, uid); + this.loadEquipEffect(state); + return void 0; + } + } + } + + unequip(slot: number): number | undefined { + const uid = this.equips.get(slot); + if (isNil(uid)) return void 0; + + const state = this.store.get(uid); + if (!state) { + logger.warn(146, uid.toString()); + return void 0; + } + + this.unloadEquipEffect(state); + this.equips.delete(slot); + + return uid; + } + + //#endregion + + //#region 装备获取 + + equipped(uid: number): boolean { + return [...this.equips.values()].includes(uid); + } + + getEquipped(slot: number): number | undefined { + return this.equips.get(slot); + } + + getEquips(): (IEquipmentState | null)[] { + const result: (IEquipmentState | null)[] = []; + for (let i = 0; i < this.slots.length; i++) { + const uid = this.equips.get(i); + if (isNil(uid)) { + result.push(null); + } else { + const state = this.store.get(uid); + if (!state) { + logger.warn(146, uid.toString()); + result.push(null); + } else { + result.push(state); + } + } + } + return result; + } + + //#endregion + + //#region 装备对比 + + compareEquip( + equipA: number, + equipB: number, + slot: number + ): Readonly> { + const stateA = this.store.get(equipA); + const stateB = this.store.get(equipB); + if (!stateA) { + logger.warn(146, equipA.toString()); + return {} as Partial; + } + if (!stateB) { + logger.warn(146, equipB.toString()); + return {} as Partial; + } + + const clone = this.attribute.clone(); + + // 获取比对槽位的装备对应的所有修饰器,这些修饰器需要在克隆对象中删除 + const equipped = this.getEquipped(slot); + if (!isNil(equipped)) { + const state = this.store.get(equipped); + if (!state) { + logger.warn(146, equipped.toString()); + return {} as Partial; + } + for (const [name, modifier] of state.getModifiers()) { + const index = this.attribute.getModifierIndex(modifier); + clone.deleteModifierByIndex(name, index); + } + } + + // 分别将两个装备的修饰器克隆并加入到克隆属性上对比 + const attrA: Partial = {}; + const attrB: Partial = {}; + const keys = new Set>(); + + for (const [name, modifier] of stateA.getModifiers()) { + // @ts-expect-error 泛型无法推导 + clone.addModifier(name, modifier); + } + for (const [name] of stateA.getModifiers()) { + attrA[name] = clone.getFinalAttribute(name); + keys.add(name); + } + for (const [name, modifier] of stateA.getModifiers()) { + // @ts-expect-error 泛型无法推导 + clone.deleteModifier(name, modifier); + } + + for (const [name, modifier] of stateB.getModifiers()) { + // @ts-expect-error 泛型无法推导 + clone.addModifier(name, modifier); + } + for (const [name] of stateB.getModifiers()) { + attrB[name] = clone.getFinalAttribute(name); + keys.add(name); + } + + // 第二次没必要再删除了,因为这个 clone 对象不会再被使用到 + + // 然后收集键名并对比 + const diff: Partial = {}; + for (const key of keys) { + const final = this.attribute.getFinalAttribute(key); + const valueA = (attrA[key] as number) ?? final; + const valueB = (attrB[key] as number) ?? final; + diff[key] = (valueA - valueB) as THero[SelectKey]; + } + + return diff; + } + + //#endregion + + saveState(): IHeroEquipmentSave { + return { + equipped: this.equips, + slots: this.slots + }; + } + + loadState(state: IHeroEquipmentSave): void { + this.slots.length = 0; + state.slots.forEach(v => { + this.slots.push(v); + }); + // 由于装备修饰器不进存档,所以此时的勇士处于没有任何装备修饰器的状态,故可以安全清除 + this.equips.clear(); + for (const [index, uid] of state.equipped) { + this.equip(uid, index); + } + } +} diff --git a/packages-user/data-base/src/hero/index.ts b/packages-user/data-base/src/hero/index.ts index 207b7c0..ed6c03c 100644 --- a/packages-user/data-base/src/hero/index.ts +++ b/packages-user/data-base/src/hero/index.ts @@ -1,7 +1,9 @@ export * from './attribute'; +export * from './equipment'; export * from './follower'; export * from './items'; export * from './location'; +export * from './modifier'; export * from './rendering'; export * from './mover'; export * from './state'; diff --git a/packages-user/data-base/src/hero/items.ts b/packages-user/data-base/src/hero/items.ts index 683cf51..c921c0f 100644 --- a/packages-user/data-base/src/hero/items.ts +++ b/packages-user/data-base/src/hero/items.ts @@ -1,5 +1,6 @@ import { isNil } from 'lodash-es'; -import { IDataCommon, ItemCategory } from '@user/data-common'; +import { IDataCommon, ItemCategory, SaveCompression } from '@user/data-common'; +import { HeroEquipsStore } from './equipment'; import { IHeroItems, IHeroItemSave, @@ -13,10 +14,12 @@ export class HeroItems implements IHeroItems { /** 消耗道具 */ private readonly consumables: Map> = new Map(); - /** 装备道具 */ - private readonly equipments: Map> = new Map(); - constructor(readonly state: IDataCommon) {} + readonly equipment: HeroEquipsStore; + + constructor(readonly state: IDataCommon) { + this.equipment = new HeroEquipsStore(state); + } /** * 将 item 参数解析为 num,字符串 id 通过 tileStore 转换为 num @@ -37,8 +40,6 @@ export class HeroItems implements IHeroItems { return this.constants; case ItemCategory.Consumable: return this.consumables; - case ItemCategory.Equipment: - return this.equipments; default: return this.constants; } @@ -83,6 +84,13 @@ export class HeroItems implements IHeroItems { return; } + if (raw.category === ItemCategory.Equipment) { + for (let i = 0; i < count; i++) { + this.equipment.add(item); + } + return; + } + const map = this.getMap(raw.category); const existing = map.get(num); if (existing) { @@ -160,20 +168,22 @@ export class HeroItems implements IHeroItems { } } - saveState(): IHeroItemsSave { + saveState(compression: SaveCompression): IHeroItemsSave { return { constants: this.mapToSave(this.constants), consumables: this.mapToSave(this.consumables), - equipments: this.mapToSave(this.equipments) + equipStore: this.equipment.saveState(compression) }; } - loadState(state: IHeroItemsSave): void { + loadState( + state: IHeroItemsSave, + compression: SaveCompression + ): void { this.constants.clear(); this.consumables.clear(); - this.equipments.clear(); this.loadMap(this.constants, state.constants); this.loadMap(this.consumables, state.consumables); - this.loadMap(this.equipments, state.equipments); + this.equipment.loadState(state.equipStore, compression); } } diff --git a/packages-user/data-base/src/hero/modifier.ts b/packages-user/data-base/src/hero/modifier.ts new file mode 100644 index 0000000..f8eba54 --- /dev/null +++ b/packages-user/data-base/src/hero/modifier.ts @@ -0,0 +1,40 @@ +import { BaseHeroModifier } from './attribute'; +import { IHeroModifier } from './types'; + +export class ValueModifier extends BaseHeroModifier { + readonly type = '@system/value'; + + constructor( + value: number, + readonly priority: number = 0 + ) { + super(value); + } + + modify(value: number): number { + return value + this.value; + } + + clone(): IHeroModifier { + return new ValueModifier(this.value, this.priority); + } +} + +export class PercentageModifier extends BaseHeroModifier { + readonly type = '@system/percentage'; + + constructor( + value: number, + readonly priority: number = 10 + ) { + super(value); + } + + modify(value: number, baseValue: number): number { + return value + baseValue * this.value; + } + + clone(): IHeroModifier { + return new PercentageModifier(this.value, this.priority); + } +} diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index 60f5926..75e4165 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -1,4 +1,5 @@ import { HeroAttribute } from './attribute'; +import { HeroEquipment } from './equipment'; import { HeroFollowersController } from './follower'; import { HeroItems } from './items'; import { HeroLocation } from './location'; @@ -6,6 +7,7 @@ import { HeroRendering } from './rendering'; import { IHeroAttribute, IHeroFollowersController, + IHeroEquipment, IHeroItems, IHeroLocation, IHeroModifier, @@ -34,6 +36,7 @@ export class HeroState implements IHeroState { readonly rendering: IHeroRendering; readonly followers: IHeroFollowersController; readonly items: IHeroItems; + readonly equip: IHeroEquipment; constructor( state: IDataCommon, @@ -53,6 +56,7 @@ export class HeroState implements IHeroState { faceHandler ); this.items = new HeroItems(state); + this.equip = new HeroEquipment(this.items.equipment, this.attribute); } attachAttribute(attribute: IHeroAttribute): void { @@ -108,6 +112,7 @@ export class HeroState implements IHeroState { saveState(compression: SaveCompression): IHeroStateSave { const modifiers: IModifierStateSave[] = []; for (const [name, modifier] of this.attribute.iterateModifiers()) { + if (!this.attribute.getModifierSaveEnabled(modifier)) continue; modifiers.push({ name: name as keyof THero, type: modifier.type, @@ -124,7 +129,8 @@ export class HeroState implements IHeroState { rendering: this.rendering.saveState(compression), followers: followerSaves, modifiers, - items: this.items.saveState(compression) + items: this.items.saveState(compression), + equip: this.equip.saveState(compression) }; } @@ -144,6 +150,7 @@ export class HeroState implements IHeroState { this.location.loadState(state.location, compression); this.rendering.loadState(state.rendering, compression); this.items.loadState(state.items, compression); + this.equip.loadState(state.equip, compression); void this.followers.removeAllFollowers(); for (const save of state.followers) { const follower = this.followers.addFollower(save.num); diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 2e298b7..9801841 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -70,6 +70,11 @@ export interface IModifierStateSave { readonly state: unknown; } +export interface IHeroAttributeCloneOption { + /** 是否克隆属性修饰器,默认为 `true` */ + cloneModifier: boolean; +} + export interface IReadonlyHeroAttribute { /** * 获取勇士的基础属性,即未经过任何 Buff 或装备等加成的属性 @@ -97,12 +102,14 @@ export interface IReadonlyHeroAttribute { /** * 深拷贝此勇士属性对象 - * @param cloneModifier 是否同时复制修饰器,默认复制 + * @param option 深拷贝选项 */ - clone(cloneModifier?: boolean): IReadonlyHeroAttribute; + clone( + option?: Readonly> + ): IHeroAttribute; /** - * 获取此勇士属性对象的可修改副本 + * 获取此勇士属性对象的可修改副本,与 `clone()` 效果一致 */ getModifiableClone(): IHeroAttribute; @@ -123,6 +130,25 @@ export interface IReadonlyHeroAttribute { getModifiers( name: K ): Iterable>; + + /** + * 获取指定修饰器的索引位置 + * @param modifier 修饰器对象 + */ + getModifierIndex(modifier: IHeroModifier): number; + + /** + * 设置指定修饰器的存档状态 + * @param modifier 属性修饰器 + * @param save 不进入存档 + */ + setModifierSaveEnabled(modifier: IHeroModifier, save: boolean): void; + + /** + * 获取指定修饰器的存档状态 + * @param modifier 属性修饰器 + */ + getModifierSaveEnabled(modifier: IHeroModifier): boolean; } export interface IHeroAttribute extends IReadonlyHeroAttribute { @@ -138,30 +164,32 @@ export interface IHeroAttribute extends IReadonlyHeroAttribute { * @param name 属性名称 * @param value 属性增减值 */ - add>(name: K, value: number): void; + add(name: SelectKey, value: number): void; /** * 将勇士属性乘以某一个值 * @param name 属性名称 * @param value 属性乘数 */ - mul>(name: K, value: number): void; + mul(name: SelectKey, value: number): void; /** * 将勇士属性除以某一个值 * @param name 属性名称 * @param value 属性除数 */ - div>(name: K, value: number): void; + div(name: SelectKey, value: number): void; /** * 向一个属性添加属性修饰器 * @param name 属性名称 * @param modifier 属性修饰器 + * @param save 是否进入存档,默认 true */ addModifier( name: K, - modifier: IHeroModifier + modifier: IHeroModifier, + save?: boolean ): void; /** @@ -175,10 +203,15 @@ export interface IHeroAttribute extends IReadonlyHeroAttribute { ): void; /** - * 深拷贝此勇士属性对象 - * @param cloneModifier 是否同时复制修饰器,默认复制 + * 根据修饰器索引删除指定修饰器 + * @param name 属性名称 + * @param index 修饰器索引 + * @returns 被删除的修饰器 */ - clone(cloneModifier?: boolean): IHeroAttribute; + deleteModifierByIndex( + name: K, + index: number + ): IHeroModifier | null; /** * 获取勇士指定属性计算过程的可迭代对象,一般用于调试。 @@ -451,13 +484,13 @@ export interface IHeroItemSave { readonly count: number; } -export interface IHeroItemsSave { +export interface IHeroItemsSave { /** 永久道具存档 */ readonly constants: readonly IHeroItemSave[]; /** 消耗道具存档 */ readonly consumables: readonly IHeroItemSave[]; - /** 装备道具存档 */ - readonly equipments: readonly IHeroItemSave[]; + /** 装备实例仓库存档 */ + readonly equipStore: IHeroEquipsStoreSave; } export interface IHeroItemState { @@ -472,7 +505,10 @@ export interface IHeroItemState { } export interface IHeroItems - extends ISaveableContent, IDataCommonExtended { + extends ISaveableContent>, IDataCommonExtended { + /** 勇士装备实例存储 */ + readonly equipment: IHeroEquipsStore; + /** * 增加道具数量,count 可以填负数。当道具为 `Pick` 类型时会立刻执行其效果。 * @param item 道具图块数字或字符串 id @@ -509,6 +545,187 @@ export interface IHeroItems //#endregion +//#region 勇士装备 + +export interface IEquipmentStateSave { + /** 装备实例 uid */ + readonly uid: number; + /** 装备道具图块数字 */ + readonly num: number; + /** 当前数值加成 */ + readonly value: Map, number>; + /** 当前百分比加成 */ + readonly percentage: Map, number>; +} + +export interface IHeroEquipsStoreSave { + /** 所有装备实例的存档 */ + readonly equipments: readonly IEquipmentStateSave[]; +} + +export interface IEquipmentState extends ISaveableContent< + IEquipmentStateSave +> { + /** 装备实例 uid */ + readonly uid: number; + /** 装备的定义数据引用 */ + readonly item: IItemRawData; + + /** + * 获取装备产生的所有修饰器,每个元素为 [属性名, 修饰器] + */ + getModifiers(): Iterable<[SelectKey, IHeroModifier]>; +} + +export interface IEquipmentSortHandler extends IDataCommonExtended { + /** 要排序的装备 A */ + readonly equipA: IEquipmentState; + /** 要排序的装备 B */ + readonly equipB: IEquipmentState; + /** 装备存储实例 */ + readonly store: IHeroEquipsStore; +} + +export interface IEquipmentSorter { + /** + * 对比两个装备,返回哪个装备要排到更前面,即哪个装备更强。 + * 返回负值表示 `equipA` 排到前面,返回正值表示 `equipB` 排到前面,返回 0 表示按照装备实例的 `uid` 排序。 + * @param handler 装备排序信息对象 + */ + compare(handler: IEquipmentSortHandler): number; +} + +export interface IHeroEquipsStore + extends ISaveableContent>, IDataCommonExtended { + /** + * 获得一件装备道具,创建装备实例 + * @param item 道具图块数字或字符串 id + * @returns 新分配的装备实例 uid + */ + add(item: number | string): number; + + /** + * 移除指定装备实例 + * @param uid 装备实例 uid + */ + delete(uid: number): void; + + /** + * 获取指定装备实例的状态,与当前是否装备无关 + * @param uid 装备实例 uid + */ + get(uid: number): IEquipmentState | null; + + /** + * 设置当前对象的装备排序器 + * @param comparer 装备排序器 + */ + useSorter(comparer: IEquipmentSorter | null): void; + + /** + * 获取指定装备的所有装备实例,按照排序器的顺序进行排序,如没有排序器则按 `uid` 排序 + * @param equip 装备的图块数字或 id + */ + instancesOf(equip: number | string): IEquipmentState[]; + + /** + * 获取当前的所有装备实例,按照排序器的顺序输出,如没有排序器则按 `uid` 排序 + */ + instances(): IEquipmentState[]; + + /** + * 获取指定类型装备的实例总数 + * @param item 道具图块数字或字符串 id + */ + count(item: number | string): number; +} + +export interface IHeroEquipmentSave { + /** 当前已装备的 uid 映射 */ + readonly equipped: ReadonlyMap; + /** 当前装备槽数组 */ + readonly slots: readonly string[]; +} + +export const enum EquipStatus { + /** 可以直接装备到目标装备栏 */ + CanEquip, + /** 不能装备到目标装备栏 */ + CannotEquip, + /** 需要将目标装备栏上的装备进行替换 */ + NeedReplace +} + +export interface IHeroEquipment + extends ISaveableContent, IDataCommonExtended { + /** 装备槽位数组,每一项字符串表示这一栏装备的名称,用于显示及装备槽匹配 */ + readonly slots: readonly string[]; + + /** + * 设置装备槽 + * @param slots 装备槽位数组 + */ + setSlots(slots: string[]): void; + + /** + * 判断指定装备是否可以装备到指定装备槽 + * @param uid 装备实例的 uid + * @param slot 要装备至的装备槽,可以是装备槽的索引,也可以是装备槽的名称 + */ + canEquipTo(uid: number, slot: number | string): EquipStatus; + + /** + * 将指定装备穿上 + * @param uid 装备实例 uid + * @param slot 要装备至的装备槽,可以是装备槽的索引,也可以是装备槽的名称 + * @param autoUnload 当要装备的装备已经处于某个装备槽,是否自动将其卸下,默认为 `true` + * @returns 被卸下的装备实例 uid,如果没有装备被卸下,那么为 `undefined` + */ + equip( + uid: number, + slot: number | string, + autoUnload?: boolean + ): number | undefined; + + /** + * 将指定装备卸下 + * @param slot 要卸下的装备槽索引 + * @returns 被卸下的装备实例 uid,如果没有装备被卸下,那么为 `undefined` + */ + unequip(slot: number): number | undefined; + + /** + * 查询指定装备是否处于装备状态 + * @param uid 装备实例 uid + */ + equipped(uid: number): boolean; + + /** + * 获取指定装备槽索引位置的装备 uid + * @param slot 要获取的装备槽索引 + */ + getEquipped(slot: number): number | undefined; + + /** + * 获取所有已装备的实例,按照装备槽排序 + */ + getEquips(): (IEquipmentState | null)[]; + + /** + * 比较两个装备在指定槽位的表现,输出装备 A 时的属性减装备 B 时的属性 + * @param equipA 要比较的装备 A 的 uid + * @param equipB 要比较的装备 B 的 uid + * @param slot 要比较的装备槽索引 + */ + compareEquip( + equipA: number, + equipB: number, + slot: number + ): Readonly>; +} + +//#endregion + //#region 勇士状态 export interface IHeroStateSave { @@ -523,7 +740,9 @@ export interface IHeroStateSave { /** 勇士属性修饰器状态 */ readonly modifiers: readonly IModifierStateSave[]; /** 勇士道具背包状态 */ - readonly items: IHeroItemsSave; + readonly items: IHeroItemsSave; + /** 勇士装备状态 */ + readonly equip: IHeroEquipmentSave; } export interface IHeroState extends ISaveableContent< @@ -539,6 +758,8 @@ export interface IHeroState extends ISaveableContent< readonly rendering: IHeroRendering; /** 勇士道具背包 */ readonly items: IHeroItems; + /** 勇士装备系统 */ + readonly equip: IHeroEquipment; /** * 获取勇士当前的位置 diff --git a/packages-user/data-common/src/store/tileStore.ts b/packages-user/data-common/src/store/tileStore.ts index 9b77d7a..9d1e4d4 100644 --- a/packages-user/data-common/src/store/tileStore.ts +++ b/packages-user/data-common/src/store/tileStore.ts @@ -54,12 +54,28 @@ export class TileStore implements ITileStore { this.numMap.set(data.num, data.id); } - idToNumber(id: string): number | null { - return this.idMap.get(id) ?? null; + idToNumber(id: string): number | undefined { + return this.idMap.get(id); } - numberToId(num: number): string | null { - return this.numMap.get(num) ?? null; + numberToId(num: number): string | undefined { + return this.numMap.get(num); + } + + num(token: number | string): number | undefined { + if (typeof token === 'number') { + return token; + } else { + return this.idMap.get(token); + } + } + + id(token: number | string): string | undefined { + if (typeof token === 'number') { + return this.numMap.get(token); + } else { + return token; + } } attachLegacyConverter(converter: ITileLegacyConverter): void { diff --git a/packages-user/data-common/src/store/types.ts b/packages-user/data-common/src/store/types.ts index 616d9bb..82cacf3 100644 --- a/packages-user/data-common/src/store/types.ts +++ b/packages-user/data-common/src/store/types.ts @@ -97,13 +97,25 @@ export interface ITileStore { * 根据图块 id 查询对应图块数字 * @param id 图块 id */ - idToNumber(id: string): number | null; + idToNumber(id: string): number | undefined; /** * 根据图块数字查询对应图块 id * @param num 图块数字 */ - numberToId(num: number): string | null; + numberToId(num: number): string | undefined; + + /** + * 传入图块数字或 id,返回图块数字 + * @param token 图块数字或 id + */ + num(token: number | string): number | undefined; + + /** + * 传入图块数字或 id,返回图块 id + * @param token 图块数字或 id + */ + id(token: number | string): string | undefined; /** * 挂载一个旧样板转换器 diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 32f255d..c194c07 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -56,7 +56,9 @@ "54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.", "55": "Cannot load MapStore state: reference data (compareWith) has not been set.", "56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.", - "57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore." + "57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore.", + "58": "Cannot find max equipment uid while loading equipment store.", + "59": "Cannot find item raw data for tile num $1." }, "warn": { "1": "Resource with type of 'none' is loaded.", @@ -203,6 +205,8 @@ "142": "Expected a specific tile number after convert id '$1' to number, but got null.", "143": "Cannot create dynamic tile for $1 since its raw data is not found.", "144": "A hero move top implementation object binding is required for hero moving.", - "145": "No tile registered for item '$1'." + "145": "No tile registered for item '$1'.", + "146": "No equipment with uid of $1 in hero's inventory.", + "147": "No available equipment slot to equip $1." } } diff --git a/prompt.md b/prompt.md index dd608e7..8aeb0f2 100644 --- a/prompt.md +++ b/prompt.md @@ -1,127 +1,188 @@ -# 核心规则 +# 核心定位 -你的身份不是架构设计者,而是长期维护该项目的核心开发者。 +你的身份不是架构设计者,而是辅助我开发项目与设计系统的起草者。实现功能永远排在最后一名——保证代码结构清晰、便于阅读、尽量减少代码跳转才是最核心的任务。哪怕实现不了功能,也要保证代码结构清晰。你的任务是尽量写出大致正确的代码以减少我的思考负担,而非追求完美。尝试一次性写出让我满意的完美代码往往会适得其反。 -该项目的架构已经经过大量设计与验证,你不需要深刻理解架构,当前架构已经是经过深思熟虑的结果。除非明确要求,否则不要试图重新设计架构,也不要主动优化已有设计。你的职责是在保持整体风格和架构稳定的前提下,完成需求并自然扩展已有系统。 +该项目的架构已经过大量设计与验证,当前架构是深思熟虑的结果。除非我明确要求,否则不要重新设计架构,也不要主动优化已有设计。你的职责是在保持整体风格与架构稳定的前提下,完成需求并自然扩展已有系统。 -1. **遵循最小修改原则**: - - 默认不要重新设计已有代码。 - - 默认不要重构已有实现。 - - 默认不要为了"更优雅"而改变已有模式。 - - 新代码应当尽可能像是在原有代码基础上的自然延续,而不是重新实现 -2. **现有代码最重要**。当前文件就是最高优先级规范。当 Prompt、已有代码、自身知识冲突时,优先遵循已有代码。 -3. **不要主动创造新的模式**。遵循已有的设计与代码风格,不要在不同风格之间来回跳跃。 -4. **优先优化维护成本,而不是代码长度**。允许局部重复,允许多几个变量,允许多几个 if。但绝不鼓励:为了减少几行代码增加抽象;为了减少重复增加函数跳转;为了"现代 TS"使用复杂语法。 -5. **当不确定时,选择最保守的方案**。如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。 -6. **代码组织方式**:组织代码时,以"连续阅读一个功能所需的认知切换最少"为目标,而不是以成员类型或访问修饰符为目标。 +# 规则优先级 -# 必须规则 +本文为 AI Agent 专用提示词,我有时可以不遵守,你**必须**遵守。 -以下规则必须时刻遵守,任何情况下都不允许违反。 +当多种规则来源产生冲突时,按以下优先级裁定: -1. **不擅自修改已有内容**:将我已经写好的内容视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在错误,应在对话中提出,而不是直接修改。 -2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。 -3. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。若完成需求所需的接口尚不存在,也应当立即停止实现,向我提出疑问,而不是擅自新增接口来推进。 -4. **修改文件前重读文件,修改后说明内容**:修改文件前务必重新阅读以确保内容是最新版。修改后必须在对话中说明所有改动的文件及具体改动内容。在修改文件时,**不得修改**文档的 `涉及文件` 节未提及的文件。 -5. **关于 `dev.md` 的强度升级**:`dev.md` 中"一般不建议/尽量避免"的条目在此处视为**绝对禁止**;"建议/最好"的条目在此处视为**必须遵守**。所有私有方法与成员必须添加 jsDoc 注释,私有方法的参数必须添加注释说明,在构造函数的参数中定义的成员除外。不允许出现连续两个的 `as`,如 `as unknown as number`。 -6. **渲染端永远处于被动**:任何情况下都不可能会出现数据端主动通知渲染端进行更新的场景,渲染端仅通过钩子与数据端通信,通过钩子被动获取信息,并在某些情况下影响数据端行为。 -7. **关于非空判断**:对于对象,直接使用 `if (!object)` 的方式进行判断,对于字面量,使用 `lodash` 接口 `isNil` 判断 `if (isNil(value))`,不要使用 `if (value === undefined)` 这种方式。 -8. **方法顺序**:对于类的方法,根据功能进行排序,而不是按照修饰词进行排序。对于较长或功能较多的类,使用 `#region` 进行分区,分区按照方法功能进行划分。所有类的成员必须写到类的开头,并按照功能排序。被调用的方法应排在调用其的方法之前。 -9. **注释要求**:所有私有方法和私有成员必须添加 jsDoc 注释,受保护、公共成员与方法必须在源头处添加注释,继承而来的,除非功能或描述有变化,否则不得添加注释。即要求注释具有唯一性:已经在某些地方添加过注释的,不在其他地方添加。 +1. **已有代码 > 本文**:当前文件中的已有实现是最高优先级规范。当 prompt、已有代码、自身知识冲突时,优先遵循已有代码。 +2. **不确定时保守**:如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。 +3. **本文 > dev.md**:`dev.md` 中的规则在本文中强度升级: -# 建议规则 +| dev.md 原文级别 | 本文对应级别 | +| ------------------------- | ------------ | +| 「一般不建议 / 尽量避免」 | **绝对禁止** | +| 「建议 / 最好」 | **必须遵守** | -以下规则为建议性,尽量遵守,特殊情况下可灵活处理。 +典型升级项——**绝对禁止**:`as` 关键字、`@ts-expect-error`、`getter` 和 `setter`、三元运算符不允许换行。 -1. **参考但不盲从建议**:我给出的实现建议可作为方向参考,但不应不加思考地照搬。 -2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。 -3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。 -4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。 -5. **避免多余的非空判断与类型守卫**:类型已满足约束时不应再额外判断或过滤。例如 `Promise.all` 接受 `(Promise | void)[]`,无需写成 `Promise.all(arr.filter(v => !!v))`。 -6. **关于整个仓库的类型检查**:单个需求**不需要**跑全仓库的类型检查,仅在重构时需要。如果跑类型检查,由于系统构建的不完全,可能包含一部分其他系统的错误信息,自行辨别。 +# 代码规则 + +## 修改原则 + +1. **最小修改**:默认不重新设计已有代码,不重构已有实现,不为「更优雅」而改变已有模式。新代码应是原有代码的自然延续。若认为已有代码存在错误,在对话中提出,不要直接修改。 +2. **文件边界**:修改文件前务必重读以确保内容最新。修改后必须在对话中说明所有改动的文件及具体内容。不得修改文档「涉及文件」节未提及的文件。 +3. **歧义提问**:遇到任何模糊、不清晰、歧义的地方,立即向我提问,不要自行假设。若完成需求所需的接口尚不存在,立即停止实现并提出疑问,不要擅自新增接口。 + +## 代码组织 + +4. **功能聚合**:以「连续阅读一个功能所需的认知切换最少」为目标组织代码。相似功能放在一起,不以修饰符类型分隔。长文件或功能较多的类使用 `#region` 分区,按方法功能划分。 +5. **维护成本优先**:允许局部重复、允许多几个变量与 if 分支。绝不鼓励:为减少行数增加抽象、为减少重复增加函数跳转、为追求「现代 TS」使用复杂语法。 +6. **文件分配**:一个文件只能包含一个类。若存在多个同类型简单实现类(均 < 50 行且 implements 同一接口),必须先向我确认,同意后方可放在同一文件。 +7. **条件语句**:对于同一个等级的条件,如果满足与不满足都需要执行某些代码,那么必须写完成的 `if - else`,不得在 `if` 里面通过 `return` 提前返回来达成 `else` 效果。 + +## 注释 + +8. **私有成员必须注释**:所有私有方法与私有成员必须添加 jsDoc 注释,私有方法的参数必须添加注释说明。构造函数的属性声明参数除外。 +9. **公共注释唯一**:受保护与公共成员、方法必须在**源头处**添加注释(多数情况为 `interface`)。继承而来的成员,除非功能或描述有变化,否则不得重复添加注释。 +10. **方法注释换行**:方法 jsDoc 必须使用换行风格;成员 jsDoc 简短时可用不换行风格。 + +## 类型与错误处理 + +11. **允许类型错误**:编写代码时允许出现 TypeScript 类型错误。不要为了修复类型错误而违反上述规则或增加代码复杂度。尤其不得出现 `as` 关键字。 +12. **错误必须抛出**:任何理应报错的场景必须使用 `logger` 接口抛出错误或警告,不得使用 `return` 等方法静默处理。logger 必须分配合理的 `code`,不得复用无关 code 或使用 `0`。 +13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash` 的 `isNil`(`if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。 +14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。 + +## 架构约束 + +15. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。 # 开发流程 -当我提出需求时,若没有明确说明直接实现或有其他明确要求,则遵循如下开发流程: +当我提出需求时,若未明确说明直接实现或另有指示,遵循以下流程: -1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下,注意有可能在其子文件夹下,例如 `docs/dev/hero`,自行判断应该放到哪。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。 -2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。 +1. 阅读当前代码,分析需求,将需求整理为 markdown 文档放在 `docs/dev` 目录下(含子目录,视情况自行判断)。文档需标注需求细节与代码实现的大体思路。本阶段遇到任何问题应向我提问确认,不得自行假设。 +2. 我会针对文档内容提问,你根据设计思路回答。我会指出设计问题,你根据要求更新文档。 +3. 我可能对文档做细微调整,实现前请重新仔细阅读最终版本。实现过程中如有问题应向我提问,而非自行决定。 +4. 我会粗略阅读你写出的代码并指出明显问题,你需要修改。 +5. 我会认真阅读并调整你的代码,形成最终方案。 +6. 你需要对比我的最终代码与你自己的代码,总结本次实现中的核心问题,以便后续改进。 -## 文档结构 +# 文档要求 -一般情况下按照以下格式编写,其余需求自行组织。 +文档不是设计说明书,而是**需求理解报告**。文档的第一目标不是提出最佳方案,而是暴露 Agent 对需求的理解模型,供我人工确认。 + +在完成接口设计前,必须先明确: + +- 需求中明确要求了什么 +- 需求中隐含要求了什么 +- 当前设计依赖哪些假设 +- 如果假设错误会导致什么设计错误 + +**禁止直接从需求跳跃到接口设计。** 任何接口设计必须能够追溯到前面的需求事实和逻辑推导。 + +换言之,文档是一份基于若干假设推导出的一种合理设计方案,重点在于从假设推导出设计的过程,而非设计本身。也就是说,文档本身是一道数学推理题,从公理(设计前提)和定义(核心概念定义)出发,通过逻辑推理(设计思路)得出结论(接口分析),每个环节都依赖于前面的环节,最终得出的接口设计需要从逻辑上使人信服。 + +# 文档结构 + +按以下格式编写,其余需求自行组织。 -- 如有需要可单独开设标题章节详述。 - 我会使用 `>` 引用块在文档中批注,因此**不要在文档中使用引用块**。 -- 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。 -- 一般不需要流程图;若必须画,使用 `mermaid`。 - -以下为文档结构,示例文档参考 `docs/dev/template.md`,务必认真阅读后再进行文档的编写工作。 +- 文档控制在 100-250 行,简洁但包含所有必要信息;不擅自修改示例文档格式。 +- 一般不需要流程图;若必须使用 `mermaid`。 +- 示例文档参考 `docs/dev/template.md`,务必认真阅读后再编写文档。一切行文思路严格按照示例文档走,不要按照自己的想法修改。已编写完成的开发文档可能不符合示例文档要求或内容已过时,不要参考。 +- 不得出现大片的 `inline-code`,不是不能写,而是不能大片地出现。接口名等情况正常使用即可。 +- 关于行数和百分比的约束只是帮助你理解文档中哪些内容是重点,哪些不是,并不需要严格遵循,只要重点正确即可。 ```md # 需求综述 描述清楚需求的内容、动机与目的。 +# 需求理解 + +分析需求,指出你对本次需求的理解。每条使用总分结构,先用一个短语总结,然后解释,总结短语不要加粗。 + +## 明确需求 + +从任务描述中可以明确得知的直接需求。 + +## 隐含需求 + +任务描述中不包含但对任务本身有重大影响的需求。 + +## 未确定需求 + +任务描述中不包含且不属于隐含需求的内容,往往是一些语义模糊的内容。没有则不写此节。 + +# 设计前提 + +指出本次设计的前提(如同数学公理),本设计的所有内容都基于此前提进行。每条使用总分结构,先用一个短语总结,然后解释,不要加粗。这一章节应该详细解释每个概念是什么,而不是每个概念会导致什么结果,或者它本身会包含什么行为。 + +# 核心概念定义 + +详细描述涉及本次任务的核心概念。我会提供若干个需要解释的概念,如有需要可自行添加。这部分必须使用严谨的逻辑阐述,不能是简单一两句话。 + # 接口设计分析 -我可能不会给完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。 +我可能不会给出完整接口设计,你需要自行分析需求补充设计,并在对话中明确指出哪些接口是你补充的。 -## IObjectMover +## IExample -### 设计意图 +### 设计思路 -分析需求,然后在这里编写接口的设计意图。 +分析需求后编写接口的设计思路。重点是分析需求并说明接口是如何设计出来的,而非简单阐述接口内容。这部分是从设计前提和概念定制推导出接口分析的过程,必须详细分析。写的时候,必须有因有果,由于什么需求,所以要设计哪些接口,不得直接说某个接口可以干什么,不得直接说某个接口可以用于哪些场景。 ### 接口分析 -按接口逐一分析成员与方法的预期使用频率(高/中/低,指**用户编写此调用的频率**而非运行时频率),中高频成员需列出至少一个典型使用场景。接口使用频率越高,其名称长度最好越短。 +按接口逐一分析成员与方法的预期使用频率。频率分为高 / 中 / 低,指**用户编写此调用的频率**,而非运行时频率或引擎调用频率。使用频率越高,名称长度宜越短。对于成员,必须写明其类型,对于方法,必须写明其参数及类型。接口分析不要包含继承而来的接口,只分析接口本身包含的内容。 + +- 成员 `property: type`:预期频率**高频**。进行分析。 +- 方法 `method(param1: type1, ...)`:预期频率**中频**。进行分析。 ### 预期体量 -本节应当写出预期的代码体量,并分析原因。 +写出预期的代码体量并分析原因。不得只写一句预期多少行,必须详细分析,分条阐述。这里不是要实现思路,而是要写为什么某个功能需要这么多行。预期是对代码量的预估,目的是让我了解到你对某个功能复杂度的理解是否正确,不需要非常精确。预期体量是对实现复杂度的预期,而不是对接口定义的预期,因此不要写接口定义预期多少行。 + +- 功能一预期 100 行:进行分析。 +- 功能二预期 50 行:进行分析。 +- 方法 `method` 预期 20 行:进行分析。 + +--- + +以上内容应占据文档的 60% 以上。以上内容中不要写任何关于私有方法或私有成员的内容,以上是设计层面的,是暴露给使用者的接口,私有方法和成员不属于设计层面,所以不应该出现。 + +--- # 实现思路 -对于复杂逻辑,按照下面的格式分条描述实现思路。 +对于复杂逻辑,分条描述实现思路。简单实现不要写入本节。 -## 复杂需求 1 +## 复杂需求 -... - -## 复杂需求 2 - -... +简述某个复杂需求的实现思路,分条写,写成有序列表。 # 涉及文件 -## 需要引用的文件 +## `@user/package/[folder/]file.ts` -按照第三方库 -> @motajs 包 → @user 包 → 当前包的其他文件的顺序写。不要写成接口列举,而是要写成需要什么类型的接口,示例如下。 +除非必要或我明确提出,一般不建议擅自新增公共方法或成员,必要时可向我提问。不需要写得过细,涵盖重要信息即可。不要出现大段的 `inline-code`(不是不能写,是不要成片地写,该用的时候就应该用,比如接口分析时分条,某些接口名称等等)。描述不要具体到变量或成员名称等,用简短的一两句话来描述即可(针对冒号后面的内容)。写的时候不要把多个接口或成员写到一个条目里面,分开写。 -## 需要修改的文件 - -### `@user/package/[folder/]file.ts` - -除非必要或明确提出,一般不建议擅自新增公共方法或成员,必要时可以向我提问。不需要写的很细,涵盖重要信息即可。 - -- [ ] 新增 `Iinterface` 接口:描述新增接口的动机与目的,会用于干什么 -- [ ] 新增 `Type` 类型别名:描述新增类型别名的动机与目的,会用于干什么 -- [ ] 新增 `private readonly property` 成员:描述新增成员的动机与目的,会用于干什么 -- [ ] 新增 `private method(...)` 方法:描述新增方法的动机与目的,会用于干什么 +- [ ] 新增 `IInterface` 接口:描述新增动机与目的、用途 +- [ ] 新增 `Type` 类型别名:描述新增动机与目的、用途 - [ ] 编写 `Class.method` 方法:描述实现的大体内容 -- [ ] 修改 `Class.method` 方法中的部分内容:描述修改哪些内容,修改这些内容的目的 -- [ ] 重构文件结构,将 `xxx` 与 `yyy` 修改为 `zzz` 与 `www`... - ... +- [ ] 修改 `Class.method` 方法中的部分内容:描述修改内容与目的 ### `@motajs/package/[folder/]file.ts` -... +… -# 问题 +# 待确认问题 -如果描述中有歧义或比较模糊的地方,可以在此列出。 +如果描述中有歧义或模糊之处,在此列出。提问必须是以下类型之一: -1. xxxxxx? -2. xxxxxx? +- **未定义概念**:我没有明确说明某一个名词具体指什么 +- **规则冲突**:我的描述中前后产生矛盾 +- **语义模糊**:某一个概念或规则可以有多种不同解释方法 +- **设计偏好**:对于某个需求可以推导出多种不同设计方案,需要我决定 + +不允许出现以下低价值问题: + +- **风格偏好**:如命名是否合理、`logger` 的文字描述是否合理等 ```