feat: 勇士身上的道具

This commit is contained in:
unanmed 2026-07-03 20:05:31 +08:00
parent c97ed868e7
commit 3ac14f8757
9 changed files with 440 additions and 52 deletions

17
dev.md
View File

@ -51,6 +51,7 @@
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。注意,当前设计理念下,**不应该**有场景会需要这种函数。 - **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。注意,当前设计理念下,**不应该**有场景会需要这种函数。
- **不转发导出**:不允许一个文件导出不属于当前 monorepo 或当前文件夹的内容。 - **不转发导出**:不允许一个文件导出不属于当前 monorepo 或当前文件夹的内容。
- **无循环引用**:不允许出现循环引用。若遇到不得不循环引用的情况,应首先反思接口设计是否存在问题。 - **无循环引用**:不允许出现循环引用。若遇到不得不循环引用的情况,应首先反思接口设计是否存在问题。
- **无类型导入**:除非某些极其特殊的场景,否则不得出现 `import type` 引入,全部使用普通引入。
### 命名规则 ### 命名规则
@ -116,7 +117,7 @@
### 渲染端 ### 渲染端
渲染端目前已基本制作完成,分为两层: 渲染端目前已基本制作完成,但底层架构仍需重构,目前分为两层:
| 包 | 层级 | 说明 | | 包 | 层级 | 说明 |
| ---------------------- | ------ | ------------------------------------ | | ---------------------- | ------ | ------------------------------------ |
@ -131,11 +132,11 @@
**Layer 1 — 数据层**:包含所有会影响游戏存档与流程的数据内容,如地图、怪物、玩家属性等。本层通过统一接口 `IStateBase` 对外暴露数据访问能力,各类数据模块均以此接口为核心组织。 **Layer 1 — 数据层**:包含所有会影响游戏存档与流程的数据内容,如地图、怪物、玩家属性等。本层通过统一接口 `IStateBase` 对外暴露数据访问能力,各类数据模块均以此接口为核心组织。
**Layer 2 — 执行层**:直接引用 Layer 1负责产生影响游戏进程的动作如玩家控制、战斗计算等。本层内容不会进入存档仅通过修改 Layer 1 的数据来影响游戏状态。 **Layer 2 — 系统层**:直接引用 Layer 1负责产生影响游戏进程的动作如玩家控制、战斗计算等。本层内容不会进入存档仅通过修改 Layer 1 的数据来影响游戏状态。
| 包 | 层级 | 说明 | | 包 | 层级 | 说明 |
| ------------------- | ------- | ------------------------------------------------------------------------------- | | ------------------- | ------- | -------------------------------------------------------------------------------- |
| `@user/data-common` | Layer 0 | 公共层,定义 `IDataCommon` 及公共无依赖接口 | | `@user/data-common` | Layer 0 | 公共层,定义 `IDataCommon` 及公共无依赖接口 |
| `@user/data-base` | Layer 1 | 数据层,定义 `IStateBase` 及可存档游戏数据(地图、怪物、玩家属性等) | | `@user/data-base` | Layer 1 | 数据层,定义 `IDataBase` 及可存档游戏数据(地图、怪物、玩家属性等) |
| `@user/data-system` | Layer 2 | 执行层,定义 `ICoreState`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 | | `@user/data-system` | Layer 2 | 系统层,定义 `IDataSystem`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 |
| `@user/data-state` | Layer 3 | 数据端的顶层模块,一般仅用于初始化以及仅供渲染端调用的顶层模块 | | `@user/data-state` | Layer 3 | 数据端的顶层模块,一般仅用于初始化以及仅供渲染端调用的顶层模块 |

131
docs/dev/hero/hero-items.md Normal file
View File

@ -0,0 +1,131 @@
# 需求综述
为勇士添加背包系统用于存储永久道具Constant、消耗道具Consumable和装备道具Equipment。即捡即用道具Pick在 addItem/getItem 时直接执行效果,不进入背包。
背包系统由 `IHeroItems` 接口定义,作为 `IHeroState` 的新成员挂载。三种类型的道具分别存储在三张 `Map<number, IHeroItemState>` 中,键为道具图块数字 `num`。背包需要支持存档读档,继承 `ISaveableContent<IHeroItemsSave>`
# 接口设计分析
## IHeroItemState
### 设计意图
单个道具在背包中的运行时状态,存储道具的基础标识信息与当前数量。`raw` 为对 `IItemRawData` 的引用,提供 `category`、`name` 等定义数据的便捷访问,读档时通过 `state.itemStore.getData(num)` 恢复。
### 接口分析
- `IHeroItemState.id`:预期频率**中频**。道具的字符串标识符,用于通过 id 寻址时做匹配。典型使用场景:通过 id 判断是否持有某特定道具。
- `IHeroItemState.num`:预期频率**高频**。道具的图块数字,是 Map 键及与地图关联的核心标识。典型使用场景:地图事件中检查玩家是否拥有 `num` 对应的钥匙。
- `IHeroItemState.raw`:预期频率**高频**。道具原始定义数据引用,提供 category、name、effect 等属性。典型使用场景:显示道具名称、判断道具类型、执行道具效果。
- `IHeroItemState.count`:预期频率**中频**。道具当前持有数量。三种可存储类型的道具均正常存储数量,不存在时视为无此道具。
### 预期体量
约 12-16 行。
## IHeroItemSave
### 设计意图
单个道具的存档格式,仅需存储可序列化的字段(`num`、`count``id` 和 `raw` 在读档时通过 `itemStore``tileStore` 恢复。
### 预期体量
约 5-8 行。
## IHeroItemsSave
### 设计意图
整个背包的存档格式,包含三种类型道具的存档数组。由于三种类型的道具分表存储,存档也对应分表。
### 预期体量
约 5-8 行。
## IHeroItems
### 设计意图
勇士背包管理接口,作为 `IHeroState` 的新成员。提供道具的增减查用等基础操作,所有操作均支持通过 `num`(数字)或 `id`(字符串)寻址。
`addItem` 是核心接口,负责向勇士给予道具(`count` 可为负数以实现扣除)。`getItem` 是 `addItem(item, 1)` 的便捷写法,语义为勇士在游戏中"拾取"一个道具。`getItemState` 是查询接口,获取道具的只读状态。
对于 Pick 类型道具,`addItem` / `getItem` 不将其存入背包,而是直接执行 `raw.effect.pickEffect`
### 接口分析
- `IHeroItems.addItem(item, count?)`:预期频率**中频**。向勇士添加或减少道具。若道具为 Pick 类型则立刻执行其效果;否则根据 `category` 选择对应分表,增加或减少 `count`,数量归零或负数时自动删除条目。典型使用场景:勇士在地图上走到道具格后拾取道具;事件脚本中给予或扣除玩家道具。
- `IHeroItems.getItem(item)`:预期频率**高频**。使勇士获取一个道具,即 `addItem(item, 1)` 的另一种写法。Pick 类型立即执行效果,其他类型背包数量加一。典型使用场景:勇士走到地图道具格上触发拾取。
- `IHeroItems.getItemState(item)`:预期频率**高频**。获取指定道具的只读状态,是所有查询操作的入口。返回 `null` 表示未持有。典型使用场景NPC 检查玩家是否持有某任务道具UI 显示道具详情。
- `IHeroItems.useItem(item)`:预期频率**中频**。使用道具,仅对 Constant 和 Consumable 类型生效。调用 `raw.effect.useEffect`Consumable 类型使用后数量减一。返回 `boolean` 表示是否使用成功。
- `IHeroItems.itemCount(item)`:预期频率**高频**。获取道具当前持有数量,不存在时返回 0。典型使用场景UI 中显示道具剩余数量。
### 预期体量
接口约 20-25 行。
实现类 `HeroItems` 约 80-100 行。内部维护三张 `Map<number, IHeroItemState>`,提供增删查用及存档读档逻辑。
# 实现思路
## id 与 num 的统一寻址
所有接口参数均接受 `number | string` 类型的 `item`。当传入为数字时,直接用作 Map 键;当传入为字符串时,需要先通过 `state.tileStore.idToNumber(item)` 转换为 `num`,再用于查找。若转换结果为 `null` 则视为不存在。
注意:道具 `id``num` 的转换依赖 `ITileStore.idToNumber`,而非 `IItemStore`(道具本身是 tile 的一类具体属性,通过 tile 即可定位,`IItemStore` 只负责道具定义存储,不需要 id↔num 双向索引)。
## 道具存取流程
1. `addItem(item, count)`:将 `item` 解析为 `num`,调用 `state.itemStore.getData(num)` 获取 `raw`。若 `raw``null` 则忽略。若 `raw.category === ItemCategory.Pick`,执行 `raw.effect.pickEffect(raw)` 后直接返回。否则,根据 `raw.category` 选择对应分表 `Map<number, IHeroItemState>`,若已存在条目则加大 `count`,结果 ≤ 0 则删除条目,否则创建新条目插入。
2. `getItem(item)`:调用 `addItem(item, 1)`
3. `getItemState(item)`:将 `item` 解析为 `num`,通过 `state.itemStore.getCategory(num)` 获取道具类型并定位对应分表,从分表中获取条目并返回只读快照,不存在返回 `null`
4. `useItem(item)`:通过 `getItemState` 获取条目,若不存在或非 Constant/Consumable 类型则返回 `false`;若 `raw.effect.canUse``false` 则返回 `false`。调用 `raw.effect.useEffect`。若为 Consumable 类型,`count--`,归零时删除条目。返回 `true`
5. `itemCount(item)`:通过 `getItemState` 获取条目,存在则返回 `count`,不存在返回 `undefined`
## 存档读档
- `saveState`:遍历三张分表,将每个条目提取 `{ num, count }` 形成 `IHeroItemSave`,构造 `IHeroItemsSave` 并返回。
- `loadState`:遍历 `IHeroItemsSave` 中三种类型数组,对每条 `IHeroItemSave` 调用 `state.itemStore.getData(num)` 恢复 `raw`,重建分表。
# 涉及文件
## 需要引用的文件
- `@user/data-common`:引用 `ISaveableContent`、`IDataCommonExtended`、`SaveCompression`、`IItemRawData`、`ItemCategory`、`IDataCommon`。
- `@user/data-base/hero/types.ts`:引用 `IHeroState`,向其新增 `items` 成员。
## 需要修改的文件
### `@user/data-base/hero/types.ts`
- [x] 新增 `IHeroItemSave` 接口:包含 `readonly num: number`、`readonly count: number`。
- [x] 新增 `IHeroItemsSave` 接口:包含 `readonly constants: readonly IHeroItemSave[]`、`readonly consumables: readonly IHeroItemSave[]`、`readonly equipments: readonly IHeroItemSave[]`。
- [x] 新增 `IHeroItemState` 接口:包含 `readonly id: string`、`readonly num: number`、`readonly raw: IItemRawData<THero>`、`count: number`。
- [x] 新增 `IHeroItems<THero>` 接口:继承 `ISaveableContent<IHeroItemsSave>`、`IDataCommonExtended`,提供 `addItem`、`getItem`、`getItemState`、`useItem`、`itemCount` 方法。
- [x] 修改 `IHeroStateSave<THero>`:新增 `readonly items: IHeroItemsSave` 成员。
- [x] 修改 `IHeroState<THero>`:新增 `readonly items: IHeroItems<THero>` 成员。
### `@user/data-base/hero/items.ts`(新建)
- [ ] 实现 `HeroItemState` 类:实现 `IHeroItemState` 接口,存储 `id`、`num`、`raw`、`count`。
- [ ] 实现 `HeroItems` 类:实现 `IHeroItems<THero>` 接口,按功能分区(查询 / 增删 / 使用 / 存档 / 私有辅助)。
- [ ] `HeroItems` 内部维护 `constants`、`consumables`、`equipments` 三张 `Map<number, HeroItemState>`
- [ ] 实现 `internalGetItemState` 私有方法:通过 `itemStore.getCategory` 定位分表后从中获取,返回可修改引用。
- [ ] 实现 `getItemState`:包装 `internalGetItemState`,返回 `Readonly` 快照。
- [ ] 实现 `addItem`:通过 `state.itemStore.getData` 获取 `raw`Pick 类型执行效果后直接返回;其他类型按 `category` 插入对应分表,支持负数删除。
- [ ] 实现 `getItem`:转发至 `addItem(item, 1)`
- [ ] 实现 `useItem`:仅对 Constant/Consumable 调用 `raw.effect.useEffect`Consumable 减量,返回成功与否。
- [ ] 实现 `itemCount`:通过 `getItemState` 返回 count 或 0。
- [ ] 实现 `mapToSave` / `loadMap`:存档仅存 `{ num, count }`,读档通过 `itemStore.getData` 恢复 `raw`
- [ ] 实现 `saveState` / `loadState`:序列化与反序列化三张分表。
### `@user/data-base/hero/state.ts`
- [ ] `HeroState` 构造函数中新增 `this.items = new HeroItems(state)`
- [ ] `saveState` 中新增 `items: this.items.saveState(compression)`
- [ ] `loadState` 中新增 `this.items.loadState(state.items, compression)`
### `@user/data-base/hero/index.ts`
- [ ] 新增 `export * from './items'` 导出。

View File

@ -1,5 +1,6 @@
export * from './attribute'; export * from './attribute';
export * from './follower'; export * from './follower';
export * from './items';
export * from './location'; export * from './location';
export * from './rendering'; export * from './rendering';
export * from './mover'; export * from './mover';

View File

@ -0,0 +1,179 @@
import { isNil } from 'lodash-es';
import { IDataCommon, ItemCategory } from '@user/data-common';
import {
IHeroItems,
IHeroItemSave,
IHeroItemsSave,
IHeroItemState
} from './types';
export class HeroItems<THero> implements IHeroItems<THero> {
/** 永久道具 */
private readonly constants: Map<number, IHeroItemState<THero>> = new Map();
/** 消耗道具 */
private readonly consumables: Map<number, IHeroItemState<THero>> =
new Map();
/** 装备道具 */
private readonly equipments: Map<number, IHeroItemState<THero>> = new Map();
constructor(readonly state: IDataCommon) {}
/**
* item num id tileStore num
* @param item id
*/
private resolveNum(item: number | string): number | null {
if (typeof item === 'number') return item;
return this.state.tileStore.idToNumber(item);
}
/**
*
* @param category
*/
private getMap(category: ItemCategory): Map<number, IHeroItemState<THero>> {
switch (category) {
case ItemCategory.Constant:
return this.constants;
case ItemCategory.Consumable:
return this.consumables;
case ItemCategory.Equipment:
return this.equipments;
default:
return this.constants;
}
}
/**
* 使
* @param item id
*/
private internalGetItemState(
item: number | string
): IHeroItemState<THero> | null {
const num = this.resolveNum(item);
if (isNil(num)) return null;
const category = this.state.itemStore.getCategory(num);
const map = this.getMap(category);
return map.get(num) ?? null;
}
getItemState(
item: number | string
): Readonly<IHeroItemState<THero>> | null {
return this.internalGetItemState(item);
}
itemCount(item: number | string): number {
return this.getItemState(item)?.count ?? 0;
}
addItem(item: number | string, count: number = 1): void {
const num = this.resolveNum(item);
if (isNil(num)) return;
const raw = this.state.itemStore.getData(num);
if (!raw) return;
if (raw.category === ItemCategory.Pick) {
for (let i = 0; i < count; i++) {
raw.effect.useEffect(raw);
}
return;
}
const map = this.getMap(raw.category);
const existing = map.get(num);
if (existing) {
existing.count += count;
if (existing.count <= 0) {
map.delete(num);
}
} else if (count > 0) {
map.set(num, { id: raw.id, num: raw.num, raw, count });
}
}
getItem(item: number | string): void {
this.addItem(item, 1);
}
useItem(item: number | string): boolean {
const state = this.internalGetItemState(item);
if (!state) return false;
const { raw } = state;
if (
raw.category !== ItemCategory.Constant &&
raw.category !== ItemCategory.Consumable
) {
return false;
}
if (!raw.effect.canUse(raw)) return false;
raw.effect.useEffect(raw);
if (raw.category === ItemCategory.Consumable) {
state.count--;
if (state.count <= 0) {
this.consumables.delete(raw.num);
}
}
return true;
}
/**
*
* @param map
*/
private mapToSave(
map: Map<number, IHeroItemState<THero>>
): readonly IHeroItemSave[] {
const result: IHeroItemSave[] = [];
for (const state of map.values()) {
result.push({ num: state.num, count: state.count });
}
return result;
}
/**
*
* @param map
* @param saves
*/
private loadMap(
map: Map<number, IHeroItemState<THero>>,
saves: readonly IHeroItemSave[]
): void {
for (const save of saves) {
const raw = this.state.itemStore.getData(save.num);
if (!raw) continue;
map.set(save.num, {
id: raw.id,
num: raw.num,
raw,
count: save.count
});
}
}
saveState(): IHeroItemsSave {
return {
constants: this.mapToSave(this.constants),
consumables: this.mapToSave(this.consumables),
equipments: this.mapToSave(this.equipments)
};
}
loadState(state: IHeroItemsSave): 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);
}
}

View File

@ -1,10 +1,12 @@
import { HeroAttribute } from './attribute'; import { HeroAttribute } from './attribute';
import { HeroFollowersController } from './follower'; import { HeroFollowersController } from './follower';
import { HeroItems } from './items';
import { HeroLocation } from './location'; import { HeroLocation } from './location';
import { HeroRendering } from './rendering'; import { HeroRendering } from './rendering';
import { import {
IHeroAttribute, IHeroAttribute,
IHeroFollowersController, IHeroFollowersController,
IHeroItems,
IHeroLocation, IHeroLocation,
IHeroModifier, IHeroModifier,
IHeroRendering, IHeroRendering,
@ -31,6 +33,7 @@ export class HeroState<THero> implements IHeroState<THero> {
readonly location: IHeroLocation; readonly location: IHeroLocation;
readonly rendering: IHeroRendering; readonly rendering: IHeroRendering;
readonly followers: IHeroFollowersController; readonly followers: IHeroFollowersController;
readonly items: IHeroItems<THero>;
constructor( constructor(
state: IDataCommon, state: IDataCommon,
@ -49,6 +52,7 @@ export class HeroState<THero> implements IHeroState<THero> {
this.location, this.location,
faceHandler faceHandler
); );
this.items = new HeroItems(state);
} }
attachAttribute(attribute: IHeroAttribute<THero>): void { attachAttribute(attribute: IHeroAttribute<THero>): void {
@ -119,7 +123,8 @@ export class HeroState<THero> implements IHeroState<THero> {
location: this.location.saveState(compression), location: this.location.saveState(compression),
rendering: this.rendering.saveState(compression), rendering: this.rendering.saveState(compression),
followers: followerSaves, followers: followerSaves,
modifiers modifiers,
items: this.items.saveState(compression)
}; };
} }
@ -138,6 +143,7 @@ export class HeroState<THero> implements IHeroState<THero> {
this.attribute = newAttribute; this.attribute = newAttribute;
this.location.loadState(state.location, compression); this.location.loadState(state.location, compression);
this.rendering.loadState(state.rendering, compression); this.rendering.loadState(state.rendering, compression);
this.items.loadState(state.items, compression);
void this.followers.removeAllFollowers(); void this.followers.removeAllFollowers();
for (const save of state.followers) { for (const save of state.followers) {
const follower = this.followers.addFollower(save.num); const follower = this.followers.addFollower(save.num);

View File

@ -8,6 +8,7 @@ import {
FaceDirection, FaceDirection,
IDataCommonExtended, IDataCommonExtended,
IFaceHandler, IFaceHandler,
IItemRawData,
IObjectMovable, IObjectMovable,
IObjectMover, IObjectMover,
ISaveableContent ISaveableContent
@ -441,6 +442,73 @@ export interface IHeroFollowersController
//#endregion //#endregion
//#region 勇士道具
export interface IHeroItemSave {
/** 道具图块数字 */
readonly num: number;
/** 道具持有数量 */
readonly count: number;
}
export interface IHeroItemsSave {
/** 永久道具存档 */
readonly constants: readonly IHeroItemSave[];
/** 消耗道具存档 */
readonly consumables: readonly IHeroItemSave[];
/** 装备道具存档 */
readonly equipments: readonly IHeroItemSave[];
}
export interface IHeroItemState<THero> {
/** 道具字符串 id */
readonly id: string;
/** 道具图块数字 */
readonly num: number;
/** 道具原始定义数据引用 */
readonly raw: IItemRawData<THero>;
/** 道具持有数量 */
count: number;
}
export interface IHeroItems<THero>
extends ISaveableContent<IHeroItemsSave>, IDataCommonExtended {
/**
* count `Pick`
* @param item id
* @param count 1
*/
addItem(item: number | string, count?: number): void;
/**
* `Pick` 使
* `addItem(item, 1)`
* @param item id
*/
getItem(item: number | string): void;
/**
*
* @param item id
*/
getItemState(item: number | string): Readonly<IHeroItemState<THero>> | null;
/**
* 使 Constant Consumable Consumable 使
* @param item id
* @returns 使
*/
useItem(item: number | string): boolean;
/**
*
* @param item id
*/
itemCount(item: number | string): number;
}
//#endregion
//#region 勇士状态 //#region 勇士状态
export interface IHeroStateSave<THero> { export interface IHeroStateSave<THero> {
@ -454,6 +522,8 @@ export interface IHeroStateSave<THero> {
readonly followers: readonly IHeroFollowerSave[]; readonly followers: readonly IHeroFollowerSave[];
/** 勇士属性修饰器状态 */ /** 勇士属性修饰器状态 */
readonly modifiers: readonly IModifierStateSave<THero>[]; readonly modifiers: readonly IModifierStateSave<THero>[];
/** 勇士道具背包状态 */
readonly items: IHeroItemsSave;
} }
export interface IHeroState<THero> extends ISaveableContent< export interface IHeroState<THero> extends ISaveableContent<
@ -467,6 +537,8 @@ export interface IHeroState<THero> extends ISaveableContent<
readonly followers: IHeroFollowersController; readonly followers: IHeroFollowersController;
/** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */ /** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */
readonly rendering: IHeroRendering; readonly rendering: IHeroRendering;
/** 勇士道具背包 */
readonly items: IHeroItems<THero>;
/** /**
* *

View File

@ -137,25 +137,21 @@ export const enum ItemCategory {
} }
export interface IItemEffect<THero> { export interface IItemEffect<THero> {
/** 即捡即用事件内容Pick 类型) */ /**
readonly pickEvent: unknown; * 使 `Pick`
/** 道具使用事件内容Constant 和 Consumable 类型) */ * `Constant` `Consumable` 使
*/
readonly useEvent: unknown; readonly useEvent: unknown;
/** /**
* * 使使 `Pick`
* @param item * `Constant` `Consumable` 使
*/
pickEffect(item: IItemRawData<THero>): void;
/**
* 使使
* @param item * @param item
*/ */
useEffect(item: IItemRawData<THero>): void; useEffect(item: IItemRawData<THero>): void;
/** /**
* 使 * 使 `Constant` `Consumable`
* @param item * @param item
*/ */
canUse(item: IItemRawData<THero>): boolean; canUse(item: IItemRawData<THero>): boolean;

View File

@ -23,8 +23,6 @@ class LegacyItemEffect implements IItemEffect<IHeroAttr> {
pickEvent: unknown; pickEvent: unknown;
useEvent: unknown; useEvent: unknown;
/** 即捡即用效果 */
private readonly pickFn: LegacyItemEffectFn | null = null;
/** 道具使用效果 */ /** 道具使用效果 */
private readonly useFn: LegacyItemEffectFn | null = null; private readonly useFn: LegacyItemEffectFn | null = null;
/** 是否能够使用道具 */ /** 是否能够使用道具 */
@ -37,33 +35,32 @@ class LegacyItemEffect implements IItemEffect<IHeroAttr> {
this.pickEvent = undefined; this.pickEvent = undefined;
this.useEvent = legacy.useItemEvent; this.useEvent = legacy.useItemEvent;
if (legacy.itemEffect) { if (legacy.cls === 'items') {
this.pickFn = new Function( if (legacy.itemEffect) {
'state', this.useFn = new Function(
'item', 'state',
legacy.itemEffect 'item',
) as LegacyItemEffectFn; legacy.itemEffect
) as LegacyItemEffectFn;
}
} else if (legacy.cls === 'constants' || legacy.cls === 'tools') {
if (legacy.useItemEffect) {
this.useFn = new Function(
'state',
'item',
legacy.useItemEffect
) as LegacyItemEffectFn;
}
if (typeof legacy.canUseItemEffect === 'string') {
this.canUseFn = new Function(
'state',
'item',
legacy.canUseItemEffect
) as LegacyItemCanUseFn;
} else {
this.canUseFn = () => !!legacy.canUseItemEffect;
}
} }
if (legacy.useItemEffect) {
this.useFn = new Function(
'state',
'item',
legacy.useItemEffect
) as LegacyItemEffectFn;
}
if (typeof legacy.canUseItemEffect === 'string') {
this.canUseFn = new Function(
'state',
'item',
legacy.canUseItemEffect
) as LegacyItemCanUseFn;
} else {
this.canUseFn = () => !!legacy.canUseItemEffect;
}
}
pickEffect(item: IItemRawData<IHeroAttr>): void {
this.pickFn?.(this.state, item);
} }
useEffect(item: IItemRawData<IHeroAttr>): void { useEffect(item: IItemRawData<IHeroAttr>): void {
@ -96,6 +93,10 @@ export class ItemLegacyBridge implements IItemLegacyConverter<
}; };
} }
/**
*
* @param legacy
*/
private convertEquip(legacy: Equip): IItemEquipData<IHeroAttr> { private convertEquip(legacy: Equip): IItemEquipData<IHeroAttr> {
const valueMap = new Map<SelectKey<IHeroAttr, number>, number>(); const valueMap = new Map<SelectKey<IHeroAttr, number>, number>();
const perMap = new Map<SelectKey<IHeroAttr, number>, number>(); const perMap = new Map<SelectKey<IHeroAttr, number>, number>();

View File

@ -13,6 +13,7 @@
3. **不要主动创造新的模式**。遵循已有的设计与代码风格,不要在不同风格之间来回跳跃。 3. **不要主动创造新的模式**。遵循已有的设计与代码风格,不要在不同风格之间来回跳跃。
4. **优先优化维护成本,而不是代码长度**。允许局部重复,允许多几个变量,允许多几个 if。但绝不鼓励为了减少几行代码增加抽象为了减少重复增加函数跳转为了"现代 TS"使用复杂语法。 4. **优先优化维护成本,而不是代码长度**。允许局部重复,允许多几个变量,允许多几个 if。但绝不鼓励为了减少几行代码增加抽象为了减少重复增加函数跳转为了"现代 TS"使用复杂语法。
5. **当不确定时,选择最保守的方案**。如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。 5. **当不确定时,选择最保守的方案**。如果两种方案都能实现需求,默认选择修改最少、风险最低、最接近已有实现的方案。
6. **代码组织方式**:组织代码时,以"连续阅读一个功能所需的认知切换最少"为目标,而不是以成员类型或访问修饰符为目标。
# 必须规则 # 必须规则
@ -22,10 +23,10 @@
2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。 2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
3. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。若完成需求所需的接口尚不存在,也应当立即停止实现,向我提出疑问,而不是擅自新增接口来推进。 3. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。若完成需求所需的接口尚不存在,也应当立即停止实现,向我提出疑问,而不是擅自新增接口来推进。
4. **修改文件前重读文件,修改后说明内容**:修改文件前务必重新阅读以确保内容是最新版。修改后必须在对话中说明所有改动的文件及具体改动内容。在修改文件时,**不得修改**文档的 `涉及文件` 节未提及的文件。 4. **修改文件前重读文件,修改后说明内容**:修改文件前务必重新阅读以确保内容是最新版。修改后必须在对话中说明所有改动的文件及具体改动内容。在修改文件时,**不得修改**文档的 `涉及文件` 节未提及的文件。
5. **关于 `dev.md` 的强度升级**`dev.md` 中"一般不建议/尽量避免"的条目在此处视为**绝对禁止**"建议/最好"的条目在此处视为**必须遵守**。所有私有方法与成员必须添加 jsDoc 注释,在构造函数的参数中定义的成员除外。 5. **关于 `dev.md` 的强度升级**`dev.md` 中"一般不建议/尽量避免"的条目在此处视为**绝对禁止**"建议/最好"的条目在此处视为**必须遵守**。所有私有方法与成员必须添加 jsDoc 注释,私有方法的参数必须添加注释说明,在构造函数的参数中定义的成员除外。不允许出现连续两个的 `as`,如 `as unknown as number`
6. **渲染端永远处于被动**:任何情况下都不可能会出现数据端主动通知渲染端进行更新的场景,渲染端仅通过钩子与数据端通信,通过钩子被动获取信息,并在某些情况下影响数据端行为。 6. **渲染端永远处于被动**:任何情况下都不可能会出现数据端主动通知渲染端进行更新的场景,渲染端仅通过钩子与数据端通信,通过钩子被动获取信息,并在某些情况下影响数据端行为。
7. **关于非空判断**:对于对象,直接使用 `if (!object)` 的方式进行判断,对于字面量,使用 `lodash` 接口 `isNil` 判断 `if (isNil(value))`,不要使用 `if (value === undefined)` 这种方式。 7. **关于非空判断**:对于对象,直接使用 `if (!object)` 的方式进行判断,对于字面量,使用 `lodash` 接口 `isNil` 判断 `if (isNil(value))`,不要使用 `if (value === undefined)` 这种方式。
8. **方法顺序**:对于类的方法,根据功能进行排序,而不是按照修饰词进行排序。对于较长或功能较多的类,使用 `#region` 进行分区,分区按照方法功能进行划分。所有类的成员必须写到类的开头,并按照功能排序。 8. **方法顺序**:对于类的方法,根据功能进行排序,而不是按照修饰词进行排序。对于较长或功能较多的类,使用 `#region` 进行分区,分区按照方法功能进行划分。所有类的成员必须写到类的开头,并按照功能排序。被调用的方法应排在调用其的方法之前。
9. **注释要求**:所有私有方法和私有成员必须添加 jsDoc 注释,受保护、公共成员与方法必须在源头处添加注释,继承而来的,除非功能或描述有变化,否则不得添加注释。即要求注释具有唯一性:已经在某些地方添加过注释的,不在其他地方添加。 9. **注释要求**:所有私有方法和私有成员必须添加 jsDoc 注释,受保护、公共成员与方法必须在源头处添加注释,继承而来的,除非功能或描述有变化,否则不得添加注释。即要求注释具有唯一性:已经在某些地方添加过注释的,不在其他地方添加。
# 建议规则 # 建议规则
@ -55,7 +56,7 @@
- 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。 - 文档控制在 100-250 行,简洁但包含所有必要信息,不擅自修改示例文档格式。
- 一般不需要流程图;若必须画,使用 `mermaid` - 一般不需要流程图;若必须画,使用 `mermaid`
以下为文档结构,示例文档参考 `docs/dev/template.md` 以下为文档结构,示例文档参考 `docs/dev/template.md`,务必认真阅读后再进行文档的编写工作。
```md ```md
# 需求综述 # 需求综述
@ -102,7 +103,7 @@
### `@user/package/[folder/]file.ts` ### `@user/package/[folder/]file.ts`
除非必要或明确提出,一般不建议擅自新增公共方法或成员,必要时可以向我提问。 除非必要或明确提出,一般不建议擅自新增公共方法或成员,必要时可以向我提问。不需要写的很细,涵盖重要信息即可。
- [ ] 新增 `Iinterface` 接口:描述新增接口的动机与目的,会用于干什么 - [ ] 新增 `Iinterface` 接口:描述新增接口的动机与目的,会用于干什么
- [ ] 新增 `Type` 类型别名:描述新增类型别名的动机与目的,会用于干什么 - [ ] 新增 `Type` 类型别名:描述新增类型别名的动机与目的,会用于干什么