mirror of
https://github.com/motajs/template.git
synced 2026-07-18 01:41:10 +08:00
feat: 勇士身上的道具
This commit is contained in:
parent
c97ed868e7
commit
3ac14f8757
17
dev.md
17
dev.md
@ -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
131
docs/dev/hero/hero-items.md
Normal 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'` 导出。
|
||||||
@ -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';
|
||||||
|
|||||||
179
packages-user/data-base/src/hero/items.ts
Normal file
179
packages-user/data-base/src/hero/items.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取勇士当前的位置
|
* 获取勇士当前的位置
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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` 类型别名:描述新增类型别名的动机与目的,会用于干什么
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user