diff --git a/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md new file mode 100644 index 0000000..c4cf275 --- /dev/null +++ b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md @@ -0,0 +1,93 @@ +# Phase 10 · 10-11 读者视角审阅记录 + +> 用户要求的「读者视角审阅」机制:派发独立子代理,以「不懂代码的游戏作者」人格通读 24 篇中级教程,产出理解性问题清单,再据此迭代修正(上限 3 轮)。 + +## Persona 提示词(逐字) + +> 假设你是一个不懂代码的游戏作者,你正在阅读这个样板的使用文档,现在看到了中级教程,你了解魔塔游戏的基本概念。 + +## 阻断性判定口径 + +读者据此**无法完成操作**,或会对**概念产生实质误解**。 + +--- + +## 第 1 轮 · 首轮问题清单(重写后 24 篇) + +### 阻断性(13) + +| # | 问题 | 定位 | +|---|------|------| +| B-01 | `@user/...`、`@motajs/...` 包名从未解释,示例里的 `import` 无法照抄 | `hero-attribute.md`、`enemy-aura.md`、`enemy-query.md`、`enemy-final.md`、`enemy-calculator.md`、`enemy-map-damage.md`、`enemy-status-hooks.md`、`events.md`、`data-plugins.md` 等 | +| B-02 | 示例里凭空出现的 `map`(`map.set(3, ...)`),未说明是什么、从哪来 | `enemy-specials.md`「两类定义」「自定义一个新特殊属性」 | +| B-03 | 特殊属性 `code` 无统一编号表,且文档自承地图伤害编号未对齐,读者不知该用哪个号、内置能否用 | `enemy-specials.md`、`enemy-final.md`、`enemy-map-damage.md` | +| B-04 | `enemy-map-damage.md` 通篇「功能没做完」,读者无法确认五类地图伤害是否生效 | `enemy-map-damage.md` | +| B-05 | `packages-user` 从未介绍,却要求在 `packages-user` 下新增录像指令 | `replay-commands.md`「新增自定义录像指令」 | +| B-06 | 装饰器 `@shouldReplay` / `@ignoreReplay` 完全没铺垫 | `replay-commands.md`「录像安全检测」 | +| B-07 | `图块数字`(tile number)反复使用但从未定义,也不知道去哪查 | `hero-items.md`、`map-tiles.md` 全篇 | +| B-08 | `putMapData(array, x, y, width)` 只给宽度、没给高度,照做会卡 | `map-tiles.md`「批量读写」 | +| B-09 | `ILayerStatic`「图层的静态部分」概念未解释 | `map-tiles.md` 开头 | +| B-10 | 自定义事件写的「类」该放哪个文件没说 | `events.md`「参数接口」「注册事件」 | +| B-11 | 钩子归属自相矛盾:`onSetFloor` 在 `hooks.md` 归 `IHeroStateHooks`,在 `enemy-status-hooks.md` 却挂到 `state.hero.location` | `hooks.md`、`enemy-status-hooks.md` | +| B-12 | `dependencies` 是 id 数组还是对象,前后不一 | `data-plugins.md`「插件定义」「时机次序」 | +| B-13 | 查询上下文 `ctx` 的方法(`ctx.scanRange`/`ctx.indexer.index`/`ctx.iterateEnemy`)凭空使用,未介绍 `ctx` | `enemy-query.md`「示例」 | + +### 非阻断(18) + +| # | 问题 | 定位 | +|---|------|------| +| N-01 | 「最小语法」未覆盖正文出现的 `?.`、`?? 0`、泛型、`Map` 等写法 | 多处 | +| N-02 | 访问勇士属性前后不一致(`state.hero.attribute.*` vs `state.hero.getModifiableAttribute()`) | `data-state-overview.md`、`hero-attribute.md` | +| N-03 | `IItemEffect` 与示例 `ItemEffect` 名字对不上 | `hero-items.md` | +| N-04 | `uid`、`setSlots(slots)` 的 `slots` 来源未解释 | `hero-equipment.md` | +| N-05 | 属性名只给「常用」,无完整清单 | `hero-attribute.md` | +| N-06 | `flags` 接口偏多且重叠,日常该用哪个不清楚 | `flags.md` | +| N-07 | 流程图把「伤害计算」排在「战前脚本」前,但脚本可放弃战斗,读起来别扭 | `combat-overview.md` | +| N-08 | 「四个阶段」与「为什么分四步」措辞重复 | `combat-overview.md` | +| N-09 | `markDirty(view)` 的 `view` 未说明 | `enemy-specials.md` | +| N-10 | 临界整节抽象;内置公式术语未与 `code` 编号对照 | `enemy-calculator.md` | +| N-11 | 接口写 `func: (parameters, env)`,正文/示例用 `param` | `events.md` | +| N-12 | 插件示例为占位,读者看不到完整插件 | `data-plugins.md` | +| N-13 | `iterateActiveMaps()`/`iterateInactiveMaps()` 未列在上一节接口清单 | `map-structure.md` | +| N-14 | `IFacedTileLocator`、`FaceDirection` 类型名未解释 | `hero-location.md` | +| N-15 | `.jsonc` 与 JSON 的差别、`src/package.json` 术语未展开 | `src-tour.md` | +| N-16 | `node` 环境、`DOM` 未解释 | `data-vs-render.md` | +| N-17 | 开篇承诺与正文难度落差大 | `code-basics.md` | +| N-18 | 各示例自定义 `code` 编号互不相同,未说明是否会冲突 | 多处 | + +--- + +## 迭代记录 + +(Task 2 / Task 3 每轮追加) + +## 第 1 轮迭代记录(Task 2 · round 1) + +**本轮目标:** 消除首轮清单中的 13 条阻断性理解问题(B-01..B-13)。非阻断项(N-*)不修,仅留档供用户判断(D-14)。 + +### 逐条处置 + +| # | 处置 | 改动文件 | +|---|------|----------| +| B-01 | `src-tour.md` 新增「包从哪来:src、packages-user 与 packages」小节,讲清 `@motajs/*`(核心引擎 `packages/`)、`@user/*`(用户工程 `packages-user/`)、`src`(游戏本体)三层与单向依赖 `src → packages-user → packages`;并在用到包名 `import` 的篇目各加一句交叉引用 | `src-tour.md`(新增)、`hero-attribute.md`、`enemy-aura.md`、`enemy-final.md`、`enemy-calculator.md`、`enemy-status-hooks.md`、`enemy-query.md`、`events.md`、`data-plugins.md`、`enemy-combat-script.md`、`enemy-map-damage.md` | +| B-02 | `enemy-specials.md`「两类定义」说明 `map` 就是 `createSpecials(state)` 创建、最终返回注册的那张属性表(`map.set(code, ...)` 即登记一条);`enemy-map-damage.md` 同处补一句 | `enemy-specials.md`、`enemy-map-damage.md` | +| B-03 | `enemy-specials.md` 顶部说明内置 `code` 由 `special.ts` 的 `map.set` 注册(当前 0–26),示例中 30 及以上的 `code` 都是示例自选的未占用编号;`enemy-final.md` 示例补注内置最终效果仅坚固 3 / 模仿 8 | `enemy-specials.md`、`enemy-final.md` | +| B-04 | `enemy-map-damage.md` 的 warning 改写为「今日可用 vs 待补齐」:系统三部件已可用,编号对齐与实际结算待补齐,并给出可操作做法 | `enemy-map-damage.md` | +| B-05 | `replay-commands.md` 要求改 `packages-user` 处,补指向「包从哪来」的说明 | `replay-commands.md` | +| B-06 | `replay-commands.md` 在 `@shouldReplay` / `@ignoreReplay` 前补装饰器简介并链到 `code-basics.md`;`code-basics.md` 新增「装饰器」小节 | `replay-commands.md`、`code-basics.md` | +| B-07 | `map-tiles.md` 定义「图块数字」= 内容层 `tile.jsonc` 条目的 `num`,并给出 `state.tileStore.num/id` 换算;`hero-items.md` 交叉引用 | `map-tiles.md`、`hero-items.md` | +| B-08 | `map-tiles.md` 说明 `putMapData` 数组按行排列、高度 = `array.length / width`(非整除会告警) | `map-tiles.md` | +| B-09 | `map-tiles.md` 开头解释 `ILayerStatic`(静态图块那半)与 `ILayerDynamic`(动态图块那半)的区别 | `map-tiles.md` | +| B-10 | `events.md` 说明自定义事件的类放在 `src/data/event/` 下的文件(可新建或并入相关文件),再由 `registrations.ts` 引入注册 | `events.md` | +| B-11 | 统一 `onSetFloor` 归属:`enemy-status-hooks.md` 与 `hooks.md` 均明确它属 `IHeroLocationHooks`(挂 `state.hero.location`),与 `IHeroStateHooks` 的 `onBeforeChangeFloor` / `onAfterChangeFloor`(挂 `state.hero`)不同族 | `enemy-status-hooks.md`、`hooks.md` | +| B-12 | `data-plugins.md` 区分两处 `dependencies`:定义时是 id 字符串数组,三个时机方法收到的是解析后的 `IDataPlugin[]` 对象 | `data-plugins.md` | +| B-13 | `enemy-query.md` 新增「查询上下文 ctx」小节,介绍 `IEnemyContext` 及示例用到的 `scanRange` / `iterateEnemy` / `indexer.index`,并说明 `IEnemyView.getComputedEnemy()` | `enemy-query.md` | + +### 校验 + +- 源码核对:`src/data/enemy/special.ts`、`src/data/enemy/mapDamage.ts`(转换器读取 15/16/18/24/27)、`packages-user/data-state/src/types.ts`(`IDataPluginDefine.dependencies: string[]`)、`packages-user/data-base/src/map/types.ts` 与 `mapLayer.ts`(`putMapData` 高度推导)、`packages-user/data-common/src/store/types.ts`(`ITileRawData.num`)、`packages-user/data-base/src/hero/types.ts`(`IHeroLocationHooks.onSetFloor`)、`packages-user/data-system/src/combat/types.ts`(`IEnemyContext` / `IEnemyCommonQueryEffect` / `IEnemySpecialQueryEffect`)、`src/data/event/`。 +- 结构门禁:24 篇仍 CRLF、单 H1(`STRUCT-OK`)。 +- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。 +- 非阻断项(N-01..N-18):本轮不修,留待用户判断。 + + diff --git a/docs/intermediate/code-basics.md b/docs/intermediate/code-basics.md index 88770ea..3588e34 100644 --- a/docs/intermediate/code-basics.md +++ b/docs/intermediate/code-basics.md @@ -141,6 +141,21 @@ class FastRunner implements Runner { 这是一种**建议但非强制**的约定:遵循它能让代码结构更清晰、类型更可靠;不遵循也不会导致程序出错。 +### 装饰器 + +有时你会看到某个类或方法上方单独写着一行 `@名字(...)`,例如: + +```ts +class Service { + @shouldReplay('移动') + move() { + // ... + } +} +``` + +这种以 `@` 开头的写法叫**装饰器(decorator)**:它像给方法贴一张标签,用来给这个方法附加额外的信息或行为,方法本身的代码不用改。本引擎只在少数地方(如录像安全检测)用到它,遇到时照着写即可。 + ## 进一步学习 本篇只覆盖了编写本引擎所需的最小语法。如果你想系统学习 JavaScript / TypeScript,下面这份教程更适合本引擎的作者: diff --git a/docs/intermediate/data-plugins.md b/docs/intermediate/data-plugins.md index 48fd2f6..3866166 100644 --- a/docs/intermediate/data-plugins.md +++ b/docs/intermediate/data-plugins.md @@ -9,7 +9,7 @@ | 成员 | 说明 | | --- | --- | | `id` | 插件标识符,全局唯一 | -| `dependencies` | 依赖的其它插件 id 数组,不允许循环依赖 | +| `dependencies` | 依赖的其它插件 **id 字符串数组**,不允许循环依赖 | | `awake(state, dependencies)` | 同步初始化 | | `initialize(state, dependencies)` | 异步初始化 | | `afterLoad(state, dependencies)` | 加载完成后初始化 | @@ -28,11 +28,11 @@ awake(装配)→ initialize(加载前)→ afterLoad(加载后) - **`initialize`**:加载开始**前**调用。返回 `Promise`,可以阻塞加载——它不兑现,依赖它的插件就不会开始初始化,游戏加载也不会开始; - **`afterLoad`**:主对象加载**完毕后**调用。返回 `Promise`,同样可阻塞。适合做依赖加载内容(地图、数据等)的初始化。 -三个阶段都按依赖顺序执行:被依赖的插件先完成同一阶段,才轮到依赖它的一方。`dependencies` 里传入的正是这些依赖插件的对象。 +三个阶段都按依赖顺序执行:被依赖的插件先完成同一阶段,才轮到依赖它的一方。注意区分两处的 `dependencies`:定义插件时写的 `dependencies` 是依赖插件的 **id 字符串数组**;而 `awake` / `initialize` / `afterLoad` 三个方法收到的 `dependencies` 参数,是这些 id 解析后的**插件对象数组**(`IDataPlugin`,含 `plugin` 与 `status`),可以直接操作依赖插件本身。 ## 注册插件 -插件在 `src/data/plugin/list.ts` 的 `dataPlugins` 数组中登记: +插件在 `src/data/plugin/list.ts` 的 `dataPlugins` 数组中登记(示例里的 `@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { IDataPluginDefine } from '@user/data-state'; diff --git a/docs/intermediate/enemy-aura.md b/docs/intermediate/enemy-aura.md index 3246b89..ba05c77 100644 --- a/docs/intermediate/enemy-aura.md +++ b/docs/intermediate/enemy-aura.md @@ -54,6 +54,8 @@ 范围对象由 `@motajs/common` 提供,常用的有 `FullRange`(全场)、`RectRange`(矩形)、`ManhattanRange`(曼哈顿距离/菱形)与 `RayRange`(射线)。 +下方示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。 + ## 示例:基础光环(减少周围怪物的金币与经验) 先从一个只改基础属性的例子开始:让范围内的怪物金币与经验下降。第一步是定义一条带参数的特殊属性(code 取一个未占用的值,这里用 30): diff --git a/docs/intermediate/enemy-calculator.md b/docs/intermediate/enemy-calculator.md index 81e1e23..188b436 100644 --- a/docs/intermediate/enemy-calculator.md +++ b/docs/intermediate/enemy-calculator.md @@ -59,6 +59,8 @@ state.enemyContext.getDamageSystem()?.useCalculator(new MainDamageCalculator()); 「等差数列属性」指怪物攻击每回合按固定值递增。属性本身在一次计算中是常量,所以不能靠给 `atk` 加值实现;要在 `calculate` 里把「每回合伤害」按等差数列求和: +示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。 + ```ts import { IEnemyAttr, IHeroAttr } from '@user/data-common'; import { diff --git a/docs/intermediate/enemy-combat-script.md b/docs/intermediate/enemy-combat-script.md index d291897..328627c 100644 --- a/docs/intermediate/enemy-combat-script.md +++ b/docs/intermediate/enemy-combat-script.md @@ -52,7 +52,7 @@ state.combat.addCombatScript(new MainCombatScript()); ## 战前示例:拥有某特殊属性时无法战斗 -下面的脚本检查怪物是否拥有 code 18 的特殊属性(无敌),有则直接放弃战斗: +下面的脚本检查怪物是否拥有 code 18 的特殊属性(无敌),有则直接放弃战斗(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { IEnemyAttr, IHeroAttr } from '@user/data-common'; diff --git a/docs/intermediate/enemy-final.md b/docs/intermediate/enemy-final.md index 76f5e69..cce55af 100644 --- a/docs/intermediate/enemy-final.md +++ b/docs/intermediate/enemy-final.md @@ -36,7 +36,9 @@ state.enemyContext.registerFinalEffect(effect); ## 示例:仿攻 -「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御: +「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御(这里的 code 32 是示例自选的未占用编号;内置的最终效果是坚固 code 3、模仿 code 8,内置编号见[特殊属性](./enemy-specials.md)): + +示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。 ```ts // special.ts:32 - 仿攻 diff --git a/docs/intermediate/enemy-map-damage.md b/docs/intermediate/enemy-map-damage.md index 1ac7815..44439d6 100644 --- a/docs/intermediate/enemy-map-damage.md +++ b/docs/intermediate/enemy-map-damage.md @@ -42,8 +42,10 @@ mapDamage?.useConverter(new MainMapDamageConverter()); | 激光 | `LaserDamageView` | `getSpecial(24)` | 22 | 同行或同列时造成伤害 | | 伏击 | `AmbushDamageView` | `hasSpecial(27)` | 未注册 | 返回 0 伤害并附带捕捉标记 | -:::warning -写作时核对源码发现,上表两组 code 并不一致:转换器实际读取的是 15、16、18、24、27,而 `special.ts` 注册的是 13、14、16、22、24,且没有 27。也就是说,按当前 `src/` 代码,内置五类地图伤害与特殊属性的编号尚未对齐。自定义时请以你所用版本的 `mapDamage.ts` 为准:要么让特殊属性使用转换器实际读取的 code,要么同步修改转换器,不要把上表当成已经对齐的最终编号。 +:::warning 编号与结算:今日可用 vs 待补齐 +- **今日可用**:地图伤害系统本身的三个部件——伤害视图、转换器、合并器——都已实现,注册(`useConverter` / `useReducer`)照常可用。只要让特殊属性使用转换器真正读取的 code,或按下方「受地形遮挡的激光」那样自定义属性 + 转换器,地图伤害就能生效。 +- **待补齐(编号未对齐)**:上表两组 code 目前并不一致——转换器实际读取的是 15、16、18、24、27,而 `special.ts` 注册的是 13、14、16、22、24,且没有 27。因此按当前 `src/` 代码,直接给怪物挂上内置的「领域 / 夹击 / 阻击 / 激光」特殊属性,并不会产生对应的地图伤害;「伏击」在内置特殊属性里也没有登记。使用内置编号前,请以你的 `src/data/enemy/mapDamage.ts` 为准二选一:让特殊属性改用转换器真正读取的 code,或修改转换器使其与 `special.ts` 对齐。 +- **待补齐(实际结算)**:移动时真正的结算逻辑仍在补齐,见下文「实际结算」。 ::: 另外两点以当前代码为准: @@ -55,6 +57,8 @@ mapDamage?.useConverter(new MainMapDamageConverter()); 内置激光使用 `RayRange` 沿四个方向发射,不判断中间是否被地形挡住。下面的示例新增一个「遮挡激光」特殊属性(code 43),沿射线检查路径,遇到非空图块就停止: +示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」;`map.set(...)` 的 `map` 是 `special.ts` 中 `createSpecials(state)` 建的属性表,见[特殊属性](./enemy-specials.md)。 + ```ts /** 43 - 遮挡激光 */ export interface ISpecial43Value { diff --git a/docs/intermediate/enemy-query.md b/docs/intermediate/enemy-query.md index 0b669bd..50c024b 100644 --- a/docs/intermediate/enemy-query.md +++ b/docs/intermediate/enemy-query.md @@ -49,9 +49,19 @@ state.enemyContext.registerSpecialQueryEffect(effect); 常规查询与特殊查询的 `priority` 和光环共用同一套优先级,从高到低处理。高优先级的效果先执行,低优先级能看到它留下的结果;反过来不行。查询新增的光环只能影响之后的阶段,规则与[怪物光环](./enemy-aura.md)一致。 +## 查询上下文 ctx + +常规查询里的 `query()` 与特殊查询里的 `for(ctx)`,拿到的都是**查询上下文**(类型 `IEnemyContext`),它代表「这次查询所在的怪物战场」。示例里用到它的这几个成员: + +- `ctx.scanRange(range, param)`:用一个范围对象扫出范围内的怪物,逐项返回 `[位置, 怪物视图]`; +- `ctx.iterateEnemy()`:遍历上下文中的全部怪物,同样逐项返回 `[位置, 怪物视图]`; +- `ctx.indexer.index(x, y)`:把坐标换算成该位置在上下文中的索引(数字越小越靠前),示例用它给怪物排序。 + +这里的**怪物视图**是 `IEnemyView`:用 `view.getComputedEnemy()` 取到该怪物的计算后对象,就能读它的特殊属性。上下文还有其他成员(如按坐标取怪物、取绑定的勇士与地图伤害管理器),需要时再查 API 参考。 + ## 示例:抱团 -「抱团」的规则是:范围内每有一个带同种特殊属性的怪物,自身攻击力就增加固定值。它只需要读周围、改自己的攻击,所以用常规查询实现: +「抱团」的规则是:范围内每有一个带同种特殊属性的怪物,自身攻击力就增加固定值。它只需要读周围、改自己的攻击,所以用常规查询实现(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { ManhattanRange } from '@motajs/common'; diff --git a/docs/intermediate/enemy-specials.md b/docs/intermediate/enemy-specials.md index a966000..5bd3cb2 100644 --- a/docs/intermediate/enemy-specials.md +++ b/docs/intermediate/enemy-specials.md @@ -4,6 +4,8 @@ 每个特殊属性就是一个数字 `code` 加一份可选参数 `value`。`code` 说明这是哪一种机制,`value` 保存它的参数;没有参数的属性,`value` 为空。先攻、连击、坚固、光环、中毒都是这样挂到怪物身上的。 +内置属性都已由 `src/data/enemy/special.ts` 用 `map.set(code, ...)` 注册(当前占用 0–26 这段连续编号);文档示例里出现的 30 及以上的 code(如 30、31、32、40–43、45、50)都是示例自选的**未占用编号**,不是内置编号。要新增属性时,先按下文步骤确认这个号没有被内置属性或你自己已有的属性占用。 + 访问怪物特殊属性的入口见[战斗总览](./combat-overview.md)。 ## 在数据里的样子 @@ -34,7 +36,7 @@ if (enemy.hasSpecial(3)) { 写一条特殊属性时,先问自己一个问题:这个机制需要参数吗?答案不同,使用的工厂函数也不同。 -内置特殊属性在 `src/data/enemy/special.ts` 的 `createSpecials(state)` 中定义,使用两个工厂函数: +内置特殊属性在 `src/data/enemy/special.ts` 的 `createSpecials(state)` 中定义,使用两个工厂函数(下文反复出现的 `map` 就是 `createSpecials(state)` 里创建的那张属性表:`map.set(code, ...)` 即往表里登记一条属性): - `defineNonePropertySpecial(code, config)`:无参数,`value` 为 `void`,适合先攻、魔攻、坚固、模仿这类纯机制; - `defineCommonSerializableSpecial(code, defaultValue, config)`:有参数,参数会进入存档,适合连击、破甲、光环这类需要数值描述的效果。 diff --git a/docs/intermediate/enemy-status-hooks.md b/docs/intermediate/enemy-status-hooks.md index 6b8c7f2..cd1044a 100644 --- a/docs/intermediate/enemy-status-hooks.md +++ b/docs/intermediate/enemy-status-hooks.md @@ -45,7 +45,7 @@ state.hero.location.addHook(new EnemyChangeFloorHooks(state)); - 勇士移动器:处理正常移动; - 寻路系统:处理瞬移; - 战斗流程:处理战前/战后; -- 勇士位置:处理切换楼层。 +- 勇士位置:处理切换楼层。这里的 `onSetFloor` 属于 `IHeroLocationHooks`,挂在 `state.hero.location` 上;它和[钩子](./hooks.md)里「切换楼层前后」的 `IHeroStateHooks.onBeforeChangeFloor` / `onAfterChangeFloor`(挂在 `state.hero` 上)是两族不同的钩子,别混用。 ## 战斗流程中的时序 @@ -74,7 +74,7 @@ map.set( ); ``` -再写一个战斗钩子。它不修改内置的 `EnemyCombatHooks`,而是作为另一个钩子并存: +再写一个战斗钩子。它不修改内置的 `EnemyCombatHooks`,而是作为另一个钩子并存(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { IEnemyAttr, IHeroAttr } from '@user/data-common'; diff --git a/docs/intermediate/events.md b/docs/intermediate/events.md index 6e063ea..f2d2dee 100644 --- a/docs/intermediate/events.md +++ b/docs/intermediate/events.md @@ -54,7 +54,7 @@ interface ISetBlockEventParam { } ``` -自定义事件的参数接口也建议放在这个文件里,与内置参数放在一起。 +自定义事件的参数接口也建议放在这个文件里,与内置参数放在一起;事件函数**类**本身则放在 `src/data/event/` 目录下的文件里——可以新建文件,也可以放进与该功能相关的现有文件(例如图块相关的放进 `map.ts`)。`registrations.ts` 通过 `import` 把这些类引入后再注册。 ## 注册事件 @@ -93,7 +93,7 @@ export function createEventRegistrations(): BuiltInFunction[] { ## 示例:设置 Flag -下面这个事件函数把参数里的 Flag 名与值写进 Flag 系统: +下面这个事件函数把参数里的 Flag 名与值写进 Flag 系统(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { BuiltInFunction } from '@motajs/anon-tokyo'; diff --git a/docs/intermediate/hero-attribute.md b/docs/intermediate/hero-attribute.md index 3f5ef13..bc2d5a1 100644 --- a/docs/intermediate/hero-attribute.md +++ b/docs/intermediate/hero-attribute.md @@ -41,7 +41,7 @@ attr.mul('money', 2); // 金币翻倍 - `ValueModifier`(type `@system/value`):数值加减,最终值 = 原值 + 修饰值; - `PercentageModifier`(type `@system/percentage`):按**基础值**计算百分比,最终值 = 原值 + 基础值 × 修饰值。 -用 `addModifier(name, modifier, save?)` 挂上修饰器,用 `deleteModifier(name, modifier)` 移除: +用 `addModifier(name, modifier, save?)` 挂上修饰器,用 `deleteModifier(name, modifier)` 移除(示例里的 `@user/*`、`@motajs/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」): ```ts import { ValueModifier, PercentageModifier } from '@user/data-base'; diff --git a/docs/intermediate/hero-items.md b/docs/intermediate/hero-items.md index 41e36ce..2786edd 100644 --- a/docs/intermediate/hero-items.md +++ b/docs/intermediate/hero-items.md @@ -25,7 +25,7 @@ - `useItem(item)`:使用道具,**仅对 `Constant` 与 `Consumable` 生效**,`Consumable` 使用后数量减一; - `itemCount(item)`:获取指定道具的持有数量。 -`item` 参数可以传道具的图块数字或字符串 id。 +`item` 参数可以传道具的图块数字或字符串 id(图块数字的含义见[地图图块](./map-tiles.md))。 ```ts // 发一把黄钥匙(+1) diff --git a/docs/intermediate/hooks.md b/docs/intermediate/hooks.md index 595c786..8cbaa81 100644 --- a/docs/intermediate/hooks.md +++ b/docs/intermediate/hooks.md @@ -58,6 +58,8 @@ const hook = { | 录像记录新指令 | `IReplaySystemHooks.onRecordCommand` | | 通知渲染端进行一次提示 | `IDataCommonHooks.onTip` | +注意区分「切换楼层」相关的两族钩子,它们挂在不同的主对象上:`onSetFloor` 属于 `IHeroLocationHooks`,挂在 `state.hero.location` 上;`onBeforeChangeFloor` / `onAfterChangeFloor` 属于 `IHeroStateHooks`,挂在 `state.hero` 上。 + 若某类需求暂未在表中找到,可以在对应主对象的 `IHookable` 接口处查看它支持的全部钩子方法(例如 `hero-attribute`、`enemy-status-hooks` 等篇会进一步说明相关钩子的用法)。 ## 注册与移除钩子 diff --git a/docs/intermediate/map-tiles.md b/docs/intermediate/map-tiles.md index b9e44f6..16399a9 100644 --- a/docs/intermediate/map-tiles.md +++ b/docs/intermediate/map-tiles.md @@ -1,6 +1,10 @@ # 地图图块 -想在地图上摆一格、擦掉一格,或者整片地读写图块,该找谁?答案是**图层**——地图图块存放在图层(`IMapLayer`)上,图块的读取、写入、批量操作与迭代都由图层负责。这些接口都定义在图层的静态部分(`ILayerStatic`)。 +想在地图上摆一格、擦掉一格,或者整片地读写图块,该找谁?答案是**图层**——地图图块存放在图层(`IMapLayer`)上。 + +图层的接口分成两半:一半管**静态图块**(`ILayerStatic`,负责单点与批量读写、迭代),一半管**动态图块**(`ILayerDynamic`,负责创建、删除、移动)。本篇只讲静态图块那半,也就是 `ILayerStatic` 上的方法。 + +先明确一个词:**图块数字**(tile number)是每个图块的整数编号,以内容层 `src/content/tile.jsonc` 中每个条目的 `num` 字段为准(同一条目还有字符串名 `id`)。图层上存取的都是这个编号;想用 `id` 换编号可以用 `state.tileStore.num(id)`,反过来用 `state.tileStore.id(num)`。 ## 取到图层 @@ -33,7 +37,7 @@ layer.removeBlock(3, 5); 要一次处理一片区域,用批量接口: -- `putMapData(array, x, y, width)`:把一维数组写入地图,数组第一项对应 `(x, y)`,`width` 为矩形范围的宽度; +- `putMapData(array, x, y, width)`:把一维数组写入地图,数组第一项对应 `(x, y)`,`width` 为矩形范围的宽度。数组按行排列(先是第一行、再是第二行……);高度不用传,由数组长度推出:高度 = `array.length / width`(数组长度不是 `width` 的整数倍时引擎会告警); - `getMapData()`:取整张地图的图块数组; - `getMapData(x, y, width, height)`:取指定矩形区域的图块数组。 diff --git a/docs/intermediate/replay-commands.md b/docs/intermediate/replay-commands.md index b6312d8..38cdafd 100644 --- a/docs/intermediate/replay-commands.md +++ b/docs/intermediate/replay-commands.md @@ -24,7 +24,7 @@ ## 新增自定义录像指令 -录像指令属于**引擎内容**,需要改动 `packages-user` 下的包,而不是只在 `src` 里写几行代码。新增一条自定义录像指令大致分三步: +录像指令属于**引擎内容**,需要改动 `packages-user` 下的包(这个目录是什么、和 `src` 是什么关系,见 [src 导览](./src-tour.md) 的「包从哪来」),而不是只在 `src` 里写几行代码。新增一条自定义录像指令大致分三步: 1. 在录像指令码枚举 `ReplayCode` 中分配一个新码。该枚举属于录像格式的一部分,遵循「只增不改」——新语义必须用新码,不能复用旧码; 2. 实现 `IReplayCommand`:`type` 指明是主动还是被动录像,`execute(step)` 执行这一步对应的操作并返回 `ReplayCommandResult`;若需要「连续步骤的后处理」,再实现可选的 `finalize()`; @@ -34,6 +34,8 @@ ## 录像安全检测:@shouldReplay 与 @ignoreReplay +`@shouldReplay(message)` 与 `@ignoreReplay(message)` 写在一个方法的上方,形如 `@名字(...)`。这种以 `@` 开头的写法叫**装饰器(decorator)**:它不改变方法本身的代码,只给这个方法附加一条额外说明(在这里,就是给录像系统打一个标记)。装饰器的基础写法见[代码基础](./code-basics.md)。 + 录像系统会检查「一步录像执行后,游戏是否真的记录下了新的录像步」。为了让这个检查覆盖到正确的方法,可以用两个方法装饰器给方法打标: - `@shouldReplay(message)`:把方法标记为**应该计入录像**。它会为这次检查登记一段区间,并把 `message` 作为信息记录;一旦检查发现问题,就用这些信息告诉你哪些调用路径没有被计入录像; diff --git a/docs/intermediate/src-tour.md b/docs/intermediate/src-tour.md index 69594b5..2ee0968 100644 --- a/docs/intermediate/src-tour.md +++ b/docs/intermediate/src-tour.md @@ -4,6 +4,16 @@ 如果还不清楚数据端与渲染端的分工,先看一眼[数据端与渲染端](./data-vs-render.md)。 +## 包从哪来:src、packages-user 与 packages + +本项目的代码分三层,示例里 `import` 语句用的包名与这三层一一对应: + +- `src/`:你的**游戏本体**,入口代码与游戏内容都放这里。它对应的包名是 `@user/main`,但教程几乎不会在 `import` 里写出它; +- `packages-user/`:**用户工程包**,包名以 `@user/` 开头(如 `@user/data-base`、`@user/data-system`)。它是引擎面向游戏的二次封装,改动这里相当于扩展引擎; +- `packages/`:**核心引擎包**,包名以 `@motajs/` 开头(如 `@motajs/common`)。它由引擎作者维护,一般不需要改。 + +依赖方向是单向的:`src` → `packages-user` → `packages`。也就是说,`src` 可以引用 `@user/*` 与 `@motajs/*`,`packages-user` 只能引用 `@motajs/*`,反过来不行。所以看到示例里 `import ... from '@user/xxx'` 或 `'@motajs/xxx'`,照抄即可——这些包都已随项目安装好,不需要你手动创建。 + ## src/data:数据端 凡是「游戏世界里发生了什么」——勇士剩多少血、怪物怎么被打、地图如何读取——都由 `src/data` 里的代码决定。它是本教程后续的重点,常见子目录: