diff --git a/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md b/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md index b9cf5ac..f9b24b4 100644 --- a/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md +++ b/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md @@ -61,3 +61,15 @@ - `mover.md`:新增首节「移动器模型」;删除正文中的使用概率与源码 / 实现差异叙述;保留 D-76 面朝朝向与移动朝向的语义区分。 - `face-handler.md`:首节改为「朝向模型」,补齐五个组件与两条链路;删除源码级与实现 / 注释差异说明;自定义一节改为六边形布局示例(`HexFaceHandler`,六个邻接方向),注册改用字符串 id `hex`;验证段同步改为六方向检查。 - 验证:`prettier --write` 两篇 unchanged;`pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build` 输出 `build complete in 17.97s`,无 dead link;CRLF 与单 H1 保持。 + +## 第 3 轮(2026-10-24) + +**用户意见:** 高级教程不要写「模板」「[需改]」等标记或模板章节(D-83);提示性内容只保留注释说明下一行做什么;游戏领域的「怪物模板 / 原始模板 / 默认模板」术语保留。 + +### 执行记录 + +- 仅修改 `docs/advanced/**/*.md`;未改代码、配置、状态文件,未写 SUMMARY。 +- 移除全部 `[需改]` 标记(`mover.md`、`face-handler.md`、`ii/adding-enemy-attribute.md`、`ii/adding-hero-attribute.md`),对应注释改为说明下一行做什么。 +- 移除文档模板措辞:`hooks-in-depth.md`、`plugin-system.md`(含两个章节标题与代码注释)、`declarative-vs-imperative.md`、`replay-consistency.md`、`saveable-content.md`、`data-stores.md`;改用「示例」或直接去掉标签。 +- 保留游戏领域术语:`怪物模板`、`原始模板`、`默认模板`、`怪物属性模板` 等。 +- 验证:`prettier --write` 十篇 unchanged;`pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build` 输出 `build complete in 18.62s`,无 dead link;CRLF 与单 H1 保持。 diff --git a/docs/advanced/data-stores.md b/docs/advanced/data-stores.md index 2dd8f8a..93279eb 100644 --- a/docs/advanced/data-stores.md +++ b/docs/advanced/data-stores.md @@ -16,7 +16,7 @@ Store 是查表用的注册表,不进存档。勇士、地图、Flag 这些游 ## 怎么读 Store -五个 Store 都是 `state` 上的只读字段,没有各自的管理器,也不需要你 `new` 一个出来。它们不是需要你实现的模板——接口已经由 `data-common` 提供,你只需要调用。 +五个 Store 都是 `state` 上的只读字段,没有各自的管理器,也不需要你 `new` 一个出来。它们不是需要你补全的半成品——接口已经由 `data-common` 提供,你只需要调用。 查询接口在找不到内容时不会抛错:取定义一般返回 `null`,换算 id 与数字一般返回 `undefined`,取图块类型返回 `TileType.Unknown`,取默认事件返回空 `Map`。所以读之前先判断结果是否存在,再使用。 diff --git a/docs/advanced/declarative-vs-imperative.md b/docs/advanced/declarative-vs-imperative.md index 22a67a8..c2a4b14 100644 --- a/docs/advanced/declarative-vs-imperative.md +++ b/docs/advanced/declarative-vs-imperative.md @@ -51,7 +51,7 @@ function healHero(hero: IHeroState): void { ### 伤害计算器 -你声明什么:一组属性到伤害结果的换算规则。如果你要替换内置公式,可以参考下面的模板;它只负责声明计算规则,注册那一步由 `useCalculator` 完成: +你声明什么:一组属性到伤害结果的换算规则。如果你要替换内置公式,可以参考下面的示例;它只负责声明计算规则,注册那一步由 `useCalculator` 完成: ```ts import { IEnemyAttr, IHeroAttr } from '@user/data-common'; @@ -62,7 +62,7 @@ import { IReadonlyDamageHandler } from '@user/data-system'; -// 伤害计算器模板:只读输入,返回这次战斗的伤害与回合数 +// 伤害计算器:只读输入,返回这次战斗的伤害与回合数 export class MyDamageCalculator implements IDamageCalculator< IEnemyAttr, IHeroAttr @@ -84,7 +84,7 @@ export class MyDamageCalculator implements IDamageCalculator< } ``` -模板声明了 `MyDamageCalculator` 类,实现 `IDamageCalculator` 接口。你需要改的是 `calculate` 的方法体,以及返回的 `damage` 与 `turn`;`getCriticalLimit` 只在临界值计算时用到,改公式时按内置实现 `MainDamageCalculator` 的写法保留即可。`calculate` 是日常唯一需要关心的成员:它接收 `handler`,返回 `{ damage, turn }`。内置实现通过 `useCalculator(new ...)` 注册一次。 +示例声明了 `MyDamageCalculator` 类,实现 `IDamageCalculator` 接口。`calculate` 是日常唯一需要关心的成员:它接收 `handler`,返回 `{ damage, turn }`;`getCriticalLimit` 只在临界值计算时用到,按内置实现 `MainDamageCalculator` 的写法保留即可。内置实现通过 `useCalculator(new ...)` 注册一次。 引擎在哪个时机执行:任何需要伤害数值的地方。显示伤害预览、计算临界值、真正结算战斗,都会调用计算器;同一场战斗里,它可能被反复调用很多次。 @@ -207,7 +207,7 @@ export class BadDamageCalculator implements IDamageCalculator< ## 什么时候仍然用命令式 -声明式不是万能模板。下面这些场景仍然应该用命令式接口: +声明式不是万能的。下面这些场景仍然应该用命令式接口: - 玩家直接产生的一次性动作:走一步、使用一个道具、触发一段一次性剧情。它们的语义就是「现在立刻发生」。 - 只在该时刻生效、且不需要回放的脚本:如果一段脚本的效果本来就只应该发生一次,写成命令式没有问题。 diff --git a/docs/advanced/face-handler.md b/docs/advanced/face-handler.md index 3fc6c44..3efe3f6 100644 --- a/docs/advanced/face-handler.md +++ b/docs/advanced/face-handler.md @@ -90,7 +90,7 @@ function targetOf( const handler = state.faceManager.get(FaceGroup.Dir8); if (!handler) return null; - // [需改] 把 direction 换成你要算的方向 + // 把 direction 换算成一步的坐标偏移 const { x, y } = handler.movement(direction); const loc = state.hero.getLocation(); return { x: loc.x + x, y: loc.y + y }; @@ -143,7 +143,7 @@ import { ICoreState } from '@user/data-state'; function bindFacingTiles(state: ICoreState): void { const binder = state.roleFace; - // [需改] 1 是主图块数字(朝下),2 是同一角色的另一张朝向图块 + // 把图块 1 登记为朝下的主图块,再把图块 2 绑成它的朝右图 binder.malloc(1, FaceDirection.Down); binder.bind(2, 1, FaceDirection.Right); @@ -198,13 +198,13 @@ function bindFacingTiles(state: ICoreState): void { - `mapMovement` 必须包含 `Unknown` 及其零偏移,调用方靠这张表把每个方向都映射到偏移。 - `dirOf` 匹配不到时返回 `Unknown`,不要抛错;只遍历本组自己的偏移表。 -下面以六边形布局为例:只认左右与四个斜向,上下不是邻接方向。偏移量按示例的行列坐标给出,换坐标系时替换成自己的增量。`[需改]` 标出需要按布局替换的部分: +下面以六边形布局为例:只认左右与四个斜向,上下不是邻接方向。偏移量按示例的行列坐标给出,换成别的坐标系时增量随之改变: ```ts import { IFaceDescriptor, IFaceHandler } from '@motajs/common'; import { FaceDirection } from '@user/data-common'; -// [需改] 降级表:六边形只认六个邻接方向,其余方向无法降级 +// 降级表:六边形只认六个邻接方向,其余方向无法降级 const HEX_DEGRADE: ReadonlyMap = new Map([ [FaceDirection.Left, FaceDirection.Left], [FaceDirection.Right, FaceDirection.Right], @@ -214,7 +214,7 @@ const HEX_DEGRADE: ReadonlyMap = new Map([ [FaceDirection.RightDown, FaceDirection.RightDown] ]); -// [需改] 运动表:六个方向的单步偏移,必须包含 Unknown 的零偏移 +// 运动表:六个方向的单步偏移,必须包含 Unknown 的零偏移 const HEX_MOVEMENTS: ReadonlyMap = new Map([ [FaceDirection.Unknown, { x: 0, y: 0 }], [FaceDirection.Left, { x: -1, y: 0 }], diff --git a/docs/advanced/hooks-in-depth.md b/docs/advanced/hooks-in-depth.md index d4c6e06..6bbec20 100644 --- a/docs/advanced/hooks-in-depth.md +++ b/docs/advanced/hooks-in-depth.md @@ -2,7 +2,7 @@ 中级教程的[钩子](../intermediate/hooks.md)回答的是「什么需求该用哪个钩子」;这一篇讲另一件事:怎么把钩子限定到你关心的那个对象上。做法很简单——建议一个对象一个钩子实例。共享实例是允许的,但不建议;除非你明确了解钩子的内部实现与后果,否则不要这样做。 -钩子接口本身不难,容易出错的是「挂上去了,触发的却不只是你想管的那张地图、那个图层」。下面先讲实例与对象的关系,再给一个完整模板,最后是注册、移除与异步的接口事实。 +钩子接口本身不难,容易出错的是「挂上去了,触发的却不只是你想管的那张地图、那个图层」。下面先讲实例与对象的关系,再给一个完整示例,最后是注册、移除与异步的接口事实。 阅读前需要会基础语法:`class`、`new`、`import`、`async` / `await`。本篇只讲数据端,示例都按当前源码里的接口书写。 @@ -22,7 +22,7 @@ ## 示例:图层级钩子 -如果你想给某个图层挂一个只处理它的钩子,可以参考下面的模板:钩子类只处理注册它的那个图层,注册函数负责找到目标图层并把实例挂上去。 +如果你想给某个图层挂一个只处理它的钩子,可以参考下面的示例:钩子类只处理注册它的那个图层,注册函数负责找到目标图层并把实例挂上去。 ```ts import { IMapLayer, IMapLayerHooks } from '@user/data-base'; @@ -69,9 +69,9 @@ export function registerGroundHooks( } ``` -这个模板声明了 `GroundBlockHooks` 类,实现 `IMapLayerHooks`;`registerGroundHooks` 负责按 `alias` 找到目标图层,再调用 `layer.addHook` 注册实例。你需要改的是:目标楼层 id 与图层 `alias`、`onUpdateBlock` 的方法体,以及 `registerGroundHooks` 的调用位置(通常放在插件的 `afterLoad`)。 +这个示例声明了 `GroundBlockHooks` 类,实现 `IMapLayerHooks`;`registerGroundHooks` 负责按 `alias` 找到目标图层,再调用 `layer.addHook` 注册实例。 -模板里的 `readonly layer` 让钩子能访问目标图层本身(读宽高、调用图层方法),不是用来比较一个不存在的参数。`afterLoad` 时楼层未必已经由加载流程构造出来(取决于内容加载方式与插件顺序),`state.maps.getMap('floor-1')` 可能返回 `null`;这时至少要把原因报出来,或者把注册推迟到楼层创建之后。另外,`onUpdateBlock` 只在图块数字真的变化时触发,设置成相同数字不会触发。 +示例里的 `readonly layer` 让钩子能访问目标图层本身(读宽高、调用图层方法),不是用来比较一个不存在的参数。`afterLoad` 时楼层未必已经由加载流程构造出来(取决于内容加载方式与插件顺序),`state.maps.getMap('floor-1')` 可能返回 `null`;这时至少要把原因报出来,或者把注册推迟到楼层创建之后。另外,`onUpdateBlock` 只在图块数字真的变化时触发,设置成相同数字不会触发。 如果钩子要在构造时保持通用、注册时才确定图层,可以在 `awake` 里取控制器。`MapLayer` 的控制器是 `IMapLayerHookController`,带 `layer` 字段与 `getMapData()`: diff --git a/docs/advanced/ii/adding-enemy-attribute.md b/docs/advanced/ii/adding-enemy-attribute.md index 51368d0..2971237 100644 --- a/docs/advanced/ii/adding-enemy-attribute.md +++ b/docs/advanced/ii/adding-enemy-attribute.md @@ -158,7 +158,7 @@ export class EnemyAttributeCalculator implements IDamageCalculator< const monDef = enemy.getAttribute('def'); const monHp = enemy.getAttribute('hp'); - // [需改] 护甲是怪物受到的百分比减伤,先夹到 0 到 100 + // 护甲是怪物受到的百分比减伤,先夹到 0 到 100 // 夹紧可以避免负伤害,也避免超过 100 后变成「反向治疗」 const armor = Math.min(Math.max(enemy.getAttribute('armor'), 0), 100); // 勇士每回合伤害:先减怪物防御,再乘护甲系数 @@ -171,7 +171,7 @@ export class EnemyAttributeCalculator implements IDamageCalculator< // 回合数随每回合伤害同步变化:护甲越高,需要的回合越多 const turn = Math.ceil(monHp / heroPerDamage); - // [需改] 攻速缩放怪物每回合伤害;100 表示正常速度 + // 攻速缩放怪物每回合伤害;100 表示正常速度 // 用 Math.max 防止负数攻速把伤害变成治疗 const atkSpeed = Math.max(enemy.getAttribute('atkSpeed'), 0); let enemyPerDamage = Math.max(monAtk - def, 0) * (atkSpeed / 100); @@ -229,7 +229,7 @@ export class ArmorAura implements IAuraView { handler: IEnemyHandler, baseEnemy: IReadonlyEnemy ): void { - // [需改] 修改的是计算后怪物;光环的加成会进入伤害计算 + // 修改的是计算后怪物;光环的加成会进入伤害计算 handler.enemy.addAttribute('armor', 10); // 若光环要按目标原始值算百分比,应读取 baseEnemy,而不是 handler.enemy @@ -275,7 +275,7 @@ export class PackAtkSpeedQuery implements IEnemyCommonQueryEffect< special: ISpecial, query: () => IReadonlyEnemyContext ): void { - // [需改] 必须通过 query() 取上下文: + // 必须通过 query() 取上下文: // 它会把「这只怪物查询过外部状态」登记下来,局部刷新时才会重算它 const ctx = query(); let count = 0; @@ -293,7 +293,7 @@ export class PackAtkSpeedQuery implements IEnemyCommonQueryEffect< } } - // [需改] 每有一个同类,攻速提高 5 + // 每有一个同类,攻速提高 5 handler.enemy.addAttribute('atkSpeed', count * 5); } } @@ -302,7 +302,7 @@ export class PackAtkSpeedQuery implements IEnemyCommonQueryEffect< 注册时把效果绑定到一个特殊属性 `code` 上: ```ts -// [需改] code 换成触发这条查询的特殊属性编号(示例自选 40) +// 把这条查询绑定到特殊属性 40;挂上该属性的怪物才会执行它 state.enemyContext.registerCommonQueryEffect(40, new PackAtkSpeedQuery()); ``` @@ -326,7 +326,7 @@ export class ArmorFinalEffect implements IEnemyFinalEffect< readonly priority = 0; apply(handler: IEnemyHandler): void { - // [需改] 最终阶段只改自身基础属性,不再查询上下文 + // 最终阶段只改自身基础属性,不再查询上下文 handler.enemy.addAttribute('armor', 5); } } diff --git a/docs/advanced/ii/adding-hero-attribute.md b/docs/advanced/ii/adding-hero-attribute.md index 436b619..10087fd 100644 --- a/docs/advanced/ii/adding-hero-attribute.md +++ b/docs/advanced/ii/adding-hero-attribute.md @@ -220,7 +220,7 @@ export class MainDamageCalculator implements IDamageCalculator< let heroPerDamage = 0; let enemyPerDamage = 0; - // [需改] 攻速缩放勇士每回合伤害。 + // 攻速缩放勇士每回合伤害。 // 插入点就是原来的 `heroPerDamage += atk - monDef`: // 必须在 `turn = Math.ceil(monHp / heroPerDamage)` 之前, // 也要在下面的 `<= 0` 早退之前,否则攻速为 0 时会除出 Infinity。 @@ -328,7 +328,7 @@ export class MainDamageCalculator implements IDamageCalculator< damage += handler.state.flags.getFieldValueDefaults('hatred', 0); } - // [需改] 护甲按最终伤害百分比减伤。 + // 护甲按最终伤害百分比减伤。 // 放在魔防、负伤归零、固伤、仇恨之后,因此这些部分都会被护甲缩放; // 放在自爆之前:自爆只把低于 hp - 1 的伤害抬到下限,下限本身不缩放。 // 如果你希望固伤/仇恨绕过护甲,把这段移到固伤之前。 diff --git a/docs/advanced/mover.md b/docs/advanced/mover.md index b78b65b..85c6cdc 100644 --- a/docs/advanced/mover.md +++ b/docs/advanced/mover.md @@ -41,7 +41,7 @@ import { FaceDirection } from '@user/data-common'; import { ICoreState } from '@user/data-state'; async function heroStepRight(state: ICoreState): Promise { - // [需改] 换成你要的方向与步数 + // 向右走一格并开始执行,拿到本次执行的控制器 const controller = state.hero.location.mover .step(FaceDirection.Right) .start(); @@ -64,7 +64,7 @@ async function pushTileRight(layer: IMapLayer): Promise { const tile = layer.transferToDynamic(5, 5); if (!tile) return; - // [需改] 换成你要的方向与步数 + // 向右推一格并开始执行,拿到本次执行的控制器 const controller = tile.mover.step(FaceDirection.Right).start(); await controller?.onEnd; } @@ -133,7 +133,7 @@ async function walkRightThenDown(state: ICoreState): Promise { const mover = state.hero.location.mover; const before = state.hero.getLocation(); - // [需改] 按需求换成你要的步数与方向 + // 链式排出三个步骤:向右两格、转向下、再向下一格 mover .step(FaceDirection.Right, 2) // 向右两格 .face(FaceDirection.Down) // 只转向,不移动 @@ -276,7 +276,7 @@ const { ignoreTerrain, autoSave } = state.hero.location.mover.getConfig(); - `onStepEnd` 计算这一步的落点,返回一个 `ITileLocator`。基类拿它和当前坐标比较,不同才调用 `tile.setPos`。不要在这里自己写坐标。 - `onStepSettled` 在坐标写入之后运行,参数里带着移动前坐标 `before` 与落点 `curr`。这里适合触发进入 / 离开 / 踩踏一类的事件,或者把朝向同步回对象。 -最小实现如下,`[需改]` 标出需要按需求替换的部分: +最小实现如下: ```ts import { @@ -297,7 +297,7 @@ const enum SimpleMoveCode { Blocked } -// 只处理四方向与瞬移的最小移动器;T 换成你自己的可移动对象 +// 只处理四方向与瞬移的最小移动器;T 是可移动对象类型 export class SimpleMover extends ObjectMover { constructor( readonly tile: T, @@ -314,7 +314,7 @@ export class SimpleMover extends ObjectMover { protected async onStepStart( step: Readonly ): Promise { - // [需改] 在这里决定这一步能不能走 + // 跳跃步骤判为不可执行,其余步骤放行 return step.type === ObjectMoveType.Jump ? SimpleMoveCode.Blocked : SimpleMoveCode.Success; @@ -349,7 +349,7 @@ export class SimpleMover extends ObjectMover { } } - // [需改] 把朝向同步给对象;方法体里有写操作,所以要标 @shouldReplay + // 标 @shouldReplay:重放时也要执行这个会写朝向的方法 @shouldReplay('Simple tile facing should be replayed.') protected async onStepSettled( before: ITileLocator, @@ -407,7 +407,7 @@ export class NoLeftMoveTopImpl implements IHeroMoveTopImpl { this.base = new DefaultHeroMoveTopImpl(state); const inner = this.base.predicate(); this.passPredicate = { - // [需改] 把这里换成你要拦的条件 + // 与默认谓词组合:向左的步骤一律不允许 canPass: handler => handler.direction !== FaceDirection.Left && inner.canPass(handler), diff --git a/docs/advanced/plugin-system.md b/docs/advanced/plugin-system.md index c8c4b4f..fb80928 100644 --- a/docs/advanced/plugin-system.md +++ b/docs/advanced/plugin-system.md @@ -85,9 +85,9 @@ export const clientPlugins: IClientPluginDefine[] = [ 数据端插件实现 `IDataPluginDefine`(来自 `@user/data-state`),渲染端插件实现 `IClientPluginDefine`(来自 `@user/client-modules`)。两者都由 `id`、依赖声明与三个时机方法组成;渲染端多一份 `clientDeps`,生命周期方法也多一个依赖参数。 -如果你想新增一个插件,可以参考下面两个模板。数据端与渲染端各一份,目录、类名与公开成员换成你自己的,其余结构保持不变。 +如果你想新增一个插件,可以参考下面两个示例。数据端与渲染端各一份,结构相同。 -### 数据端插件模板 +### 数据端插件示例 ```ts // src/data/plugin/my-plugin/types.ts @@ -105,7 +105,7 @@ export interface IMyPlugin extends IDataPluginDefine { import { ICoreState, IDataPlugin } from '@user/data-state'; import { IMyPlugin } from './types'; -// 数据端插件模板 +// 数据端插件类;注册表按 id 创建它 export class MyPlugin implements IMyPlugin { // 插件标识符:在数据端注册表里唯一 readonly id = 'my-plugin'; @@ -136,7 +136,7 @@ export class MyPlugin implements IMyPlugin { } ``` -### 渲染端插件模板 +### 渲染端插件示例 ```ts import { @@ -146,7 +146,7 @@ import { } from '@user/client-modules'; import { IDataPlugin } from '@user/data-state'; -// 渲染端插件模板:写在 src/client/plugin/my-client/plugin.ts +// 渲染端插件类,文件放在 src/client/plugin/my-client/plugin.ts export class MyClientPlugin implements IClientPluginDefine { // 插件标识符:在渲染端注册表里唯一 readonly id = 'my-client-plugin'; @@ -180,7 +180,7 @@ export class MyClientPlugin implements IClientPluginDefine { } ``` -两个模板都声明了一个实现插件接口的类:数据端的 `MyPlugin` 实现 `IMyPlugin extends IDataPluginDefine`,渲染端的 `MyClientPlugin` 实现 `IClientPluginDefine`。它们都没有构造器,插件需要的对象要在 `awake` 里自己创建。你需要改的是:`id`(在对应端唯一)、依赖数组(没有就留空)、以及三个时机方法的方法体——`awake` 里装配对象、`initialize` 里做加载前的异步准备、`afterLoad` 里做依赖加载内容的初始化。数据端模板把对外接口放在 `types.ts`,是为了让依赖它的插件能按接口调用;渲染端模板暂时没有单独拆文件。 +两个示例都声明了一个实现插件接口的类:数据端的 `MyPlugin` 实现 `IMyPlugin extends IDataPluginDefine`,渲染端的 `MyClientPlugin` 实现 `IClientPluginDefine`。它们都没有构造器,插件需要的对象要在 `awake` 里自己创建。三个时机方法各管一段:`awake` 里装配对象、`initialize` 里做加载前的异步准备、`afterLoad` 里做依赖加载内容的初始化。数据端示例把对外接口放在 `types.ts`,是为了让依赖它的插件能按接口调用;渲染端示例暂时没有单独拆文件。 ### 逐项说明 diff --git a/docs/advanced/replay-consistency.md b/docs/advanced/replay-consistency.md index 0998803..3496433 100644 --- a/docs/advanced/replay-consistency.md +++ b/docs/advanced/replay-consistency.md @@ -30,7 +30,7 @@ ## 给会改状态的方法加标记 -标记加在你自己的、会改状态的方法上。如果你不确定该把标记写在哪里,可以参考下面的模板:它把一个会改状态的公开方法标成「应计入录像」,其余成员用注释省略。商店插件是现成的例子,它的 `ShopSystem` 在 `src/data/plugin/shop/system.ts` 里给开店、关店、购买、出售都加了标记: +标记加在你自己的、会改状态的方法上。如果你不确定该把标记写在哪里,可以参考下面的示例:它把一个会改状态的公开方法标成「应计入录像」,其余成员用注释省略。商店插件是现成的例子,它的 `ShopSystem` 在 `src/data/plugin/shop/system.ts` 里给开店、关店、购买、出售都加了标记: ```ts // shouldReplay 从数据端公共包引入 @@ -50,7 +50,7 @@ export class ShopSystem implements IShopSystem { } ``` -这个模板声明了 `ShopSystem` 类,实现 `IShopSystem` 接口;`@shouldReplay` 紧贴在 `buyItem` 方法上方,装饰的是这个方法。你需要改的是:把类与方法换成你真正会改状态的那一个,把 `message` 写成这个方法改变了什么状态;同一个类里还有其它会改状态的入口时,按同样方式逐个标注。 +这个示例声明了 `ShopSystem` 类,实现 `IShopSystem` 接口;`@shouldReplay` 紧贴在 `buyItem` 方法上方,装饰的是这个方法。`message` 应当写清这个方法改变了什么状态;同一个类里还有其它会改状态的入口时,按同样方式逐个标注。 装饰器紧贴方法声明上方,中间不要隔别的成员。`shouldReplay` 从 `@user/data-common` 引入;商店插件的文件顶部已经有这行 `import`,新文件才需要自己补。同一个插件里,`openShop`、`closeShop`、`sellItem` 也都按同样方式标注。 diff --git a/docs/advanced/saveable-content.md b/docs/advanced/saveable-content.md index 48231dd..0a623ee 100644 --- a/docs/advanced/saveable-content.md +++ b/docs/advanced/saveable-content.md @@ -28,7 +28,7 @@ ## 写一个可存档对象 -可存档内容分两部分:一个实现 `ISaveableContent` 的对象,以及把它注册进存档系统的插件。如果你有一份自定义状态需要跟着存档走,可以参考下面的模板;模板把两部分一次写完,后面再逐项解释。 +可存档内容分两部分:一个实现 `ISaveableContent` 的对象,以及把它注册进存档系统的插件。如果你有一份自定义状态需要跟着存档走,可以参考下面的示例;示例把两部分一次写完,后面再逐项解释。 ```ts // src/data/plugin/my-plugin/state.ts @@ -41,10 +41,10 @@ export interface IMySave { // 可存档对象:实现 ISaveableContent,泛型参数是存档形状 export class MySaveable implements ISaveableContent { - // 运行时的真实数据;换成你真正要保存的结构 + // 运行时的真实数据:用 Map 保存键值对 private readonly values: Map = new Map(); - // 对外方法按你的需求增删 + // 对外方法:按 key 写入数值 setValue(key: string, value: number): void { this.values.set(key, value); } @@ -108,9 +108,9 @@ export class MyPlugin implements IMyPlugin { } ``` -模板一共声明了三样东西:`state.ts` 里的 `IMySave` 是存档形状,`MySaveable` 实现 `ISaveableContent`,负责在活对象与存档数据之间转换;`types.ts` 里的 `IMyPlugin` 继承 `IDataPluginDefine`,把可存档对象作为公开成员暴露出去;`plugin.ts` 里的 `MyPlugin` 实现 `IMyPlugin`,在 `awake` 里创建对象并调用 `addSaveableContent` 注册。 +示例一共声明了三样东西:`state.ts` 里的 `IMySave` 是存档形状,`MySaveable` 实现 `ISaveableContent`,负责在活对象与存档数据之间转换;`types.ts` 里的 `IMyPlugin` 继承 `IDataPluginDefine`,把可存档对象作为公开成员暴露出去;`plugin.ts` 里的 `MyPlugin` 实现 `IMyPlugin`,在 `awake` 里创建对象并调用 `addSaveableContent` 注册。 -你需要改的是:`IMySave` 的字段(只保留重建必需的数据)、`MySaveable` 里真正的状态与对外方法、插件 `id`、注册 id,以及把 `MySaveable` 换成你自己的类。`initialize` / `afterLoad` 在模板里是空的,需要等加载内容时再填。下面解释 `saveState` / `loadState` 的约束,以及注册时要注意什么。 +`IMySave` 的字段只保留重建必需的数据,`MySaveable` 里放真正的状态与对外方法,插件 `id` 与注册 id 分别命名。`initialize` / `afterLoad` 在示例里是空的,需要等加载内容时再填。下面解释 `saveState` / `loadState` 的约束,以及注册时要注意什么。 ## saveState 与 loadState @@ -203,7 +203,7 @@ export class BadSaveable implements ISaveableContent { ### 压缩参数可以省略 -`ISaveableContent` 声明的是 `saveState(compression)` / `loadState(state, compression)`,TypeScript 允许实现时少写参数。模板里就没有写 `compression`:调用方仍会按接口传两个实参,多出的那个被忽略。需要按压缩级别改变存法时,再把 `compression: SaveCompression` 加回来。 +`ISaveableContent` 声明的是 `saveState(compression)` / `loadState(state, compression)`,TypeScript 允许实现时少写参数。示例里就没有写 `compression`:调用方仍会按接口传两个实参,多出的那个被忽略。需要按压缩级别改变存法时,再把 `compression: SaveCompression` 加回来。 `saveState` 里不要修改对象状态;`loadState` 不要假设旧数据还在。 @@ -341,7 +341,7 @@ import { SaveCompression } from '@user/data-common'; import { ICoreState, PluginStatus } from '@user/data-state'; import { IMyPlugin } from './types'; -// 验证:存档 → 改状态 → 读档;IMyPlugin / MySaveable 换成你自己的定义 +// 验证流程:存档 → 改状态 → 读档,确认恢复为存档时的值 export function verifySaveLoad(state: ICoreState): void { const holder = state.getDataPlugin('my-plugin'); if (!holder || holder.status !== PluginStatus.Loaded) return; @@ -371,7 +371,7 @@ export function verifySaveLoad(state: ICoreState): void { ## 小结 -- 先写可存档对象与插件注册模板,再逐项调整存档形状、返回值与注册时机。 +- 先写可存档对象与插件注册,再逐项调整存档形状、返回值与注册时机。 - 只存重建必需的状态;缓存、派生数据与 L2 系统不进存档。 - `ISaveableContent` 的 `T` 是存档形状;`saveState` 必须返回独立于活对象的数据,顶层不会替你 `structuredClone`。 - 存档数据必须能被结构化克隆;类实例会丢原型与方法,读档时要自己重建。