docs(10-11): 迭代修正读者阻断问题(第1轮)

- 修正 B-01..B-13 共 13 条阻断性理解问题(详见 10-11-READER-REVIEW.md 第 1 轮记录)

- src-tour 新增「包从哪来」概念,并在用到包名 import 的篇目交叉引用

- 结构门禁 STRUCT-OK;构建 green、dead link = 0
This commit is contained in:
unanmed 2026-10-17 21:50:12 +08:00
parent b09da5b129
commit 927611182b
18 changed files with 166 additions and 18 deletions

View File

@ -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):本轮不修,留待用户判断。

View File

@ -141,6 +141,21 @@ class FastRunner implements Runner {
这是一种**建议但非强制**的约定:遵循它能让代码结构更清晰、类型更可靠;不遵循也不会导致程序出错。
### 装饰器
有时你会看到某个类或方法上方单独写着一行 `@名字(...)`,例如:
```ts
class Service {
@shouldReplay('移动')
move() {
// ...
}
}
```
这种以 `@` 开头的写法叫**装饰器(decorator)**:它像给方法贴一张标签,用来给这个方法附加额外的信息或行为,方法本身的代码不用改。本引擎只在少数地方(如录像安全检测)用到它,遇到时照着写即可。
## 进一步学习
本篇只覆盖了编写本引擎所需的最小语法。如果你想系统学习 JavaScript / TypeScript,下面这份教程更适合本引擎的作者:

View File

@ -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';

View File

@ -54,6 +54,8 @@
范围对象由 `@motajs/common` 提供,常用的有 `FullRange`(全场)、`RectRange`(矩形)、`ManhattanRange`(曼哈顿距离/菱形)与 `RayRange`(射线)。
下方示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。
## 示例:基础光环(减少周围怪物的金币与经验)
先从一个只改基础属性的例子开始:让范围内的怪物金币与经验下降。第一步是定义一条带参数的特殊属性(code 取一个未占用的值,这里用 30):

View File

@ -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 {

View File

@ -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';

View File

@ -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 - 仿攻

View File

@ -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 {

View File

@ -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';

View File

@ -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<T>(code, defaultValue, config)`:有参数,参数会进入存档,适合连击、破甲、光环这类需要数值描述的效果。

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -25,7 +25,7 @@
- `useItem(item)`:使用道具,**仅对 `Constant` 与 `Consumable` 生效**,`Consumable` 使用后数量减一;
- `itemCount(item)`:获取指定道具的持有数量。
`item` 参数可以传道具的图块数字或字符串 id。
`item` 参数可以传道具的图块数字或字符串 id(图块数字的含义见[地图图块](./map-tiles.md))。
```ts
// 发一把黄钥匙(+1)

View File

@ -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` 等篇会进一步说明相关钩子的用法)。
## 注册与移除钩子

View File

@ -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)`:取指定矩形区域的图块数组。

View File

@ -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` 作为信息记录;一旦检查发现问题,就用这些信息告诉你哪些调用路径没有被计入录像;

View File

@ -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` 里的代码决定。它是本教程后续的重点,常见子目录: