docs(10-11): 用户修订循环 batch1(战斗概念/说明)

- combat-overview/enemy-specials: 说明上下文重建时机(惰性、一般无需手动)
- enemy-final/aura/query: 补「既依赖又改外部」不提供的原因(循环依赖)
- enemy-specials: getSpecial<T> 泛型说明 + 特殊属性定义逐行注释与动态文案
- enemy-aura: 转换器/视图大白话,每条属性可对应不同转换器
- enemy-query: query() 必须调用 + 查询上下文改为只读 IReadonlyEnemyContext
- 追加 10-11-READER-REVIEW.md 用户循环 batch1 记录
This commit is contained in:
unanmed 2026-10-18 14:07:25 +08:00
parent 4f4090d5c5
commit 8c5e8979e1
6 changed files with 174 additions and 17 deletions

View File

@ -187,4 +187,115 @@
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 结论:第 2 轮遗留的 B-03 与 R3-01..R3-04 全部消除;本轮为第 3 轮(上限),未发现新的阻断性理解问题。
---
## 第 3 轮 · 复审结果(Task 3 收尾复议)
**触发:** 第 3 轮修正 + prettier 格式化(提交 `4f4090d style(10-11): prettier 格式化中级教程文档`)之后,再次派发一个**独立 persona 子代理**(同一人格设定,逐字同 Task 1:「假设你是一个不懂代码的游戏作者,你正在阅读这个样板的使用文档,现在看到了中级教程,你了解魔塔游戏的基本概念」),让它重新通读全部 24 篇并回答「现在还有哪些看不懂的地方?」。
**复审结论:阻断性问题 = 0,审阅通过。**
### 逐条复核(首轮 B-01..B-13 · 第 2 轮 R2-01..R2-06 · 第 3 轮 R3-01..R3-04)
| # | 结论 | 说明 |
|---|------|------|
| B-01 | 已消除 | `src-tour.md`「包从哪来」讲清 `@motajs/*` / `@user/*` / `src` 三层与单向依赖,各处 `import` 有据可循。 |
| B-02 | 已消除 | `enemy-specials.md` / `enemy-map-damage.md` 说明 `map` 即 `createSpecials(state)` 返回的、最终注册的那张属性表。 |
| B-03 | 已消除 | 内置编号以 `special.ts` 为准给出 0–26 权威表;地图伤害(13 领域 / 14 夹击 / 16 阻击 / 22 激光 / 25 捕捉)在 `enemy-specials.md` 与 `enemy-map-damage.md` 两处一致,并写明「挂上对应内置属性即可生效」。 |
| B-04 | 已消除 | `enemy-map-damage.md` 的 warning 改为「今日可用 vs 待补齐」,五类地图伤害可用性明确。 |
| B-05 | 已消除 | `replay-commands.md` 补指向「包从哪来」,`packages-user` 有解释。 |
| B-06 | 已消除 | 装饰器 `@shouldReplay` / `@ignoreReplay` 前有铺垫并链到 `code-basics.md` 的「装饰器」小节。 |
| B-07 | 已消除 | `map-tiles.md` 定义「图块数字」= 内容层 `tile.jsonc` 的 `num`,并给 `state.tileStore.num/id` 换算。 |
| B-08 | 已消除 | `putMapData` 数组按行排列、高度 = `array.length / width` 已说明。 |
| B-09 | 已消除 | `ILayerStatic`(静态那半)与 `ILayerDynamic`(动态那半)已在开头解释。 |
| B-10 | 已消除 | `events.md` 说明自定义事件的类放 `src/data/event/` 下并由 `registrations.ts` 引入注册。 |
| B-11 | 已消除 | `onSetFloor` 归属统一为 `IHeroLocationHooks`,与 `IHeroStateHooks` 分族。 |
| B-12 | 已消除 | `data-plugins.md` 区分定义时的 id 数组与时机方法收到的 `IDataPlugin[]` 对象。 |
| B-13 | 已消除 | `enemy-query.md` 新增「查询上下文 ctx」小节。 |
| R2-01 | 已消除 | `BuiltInFunction` 来源 `@motajs/anon-tokyo` 已注明。 |
| R2-02 | 已消除 | 内置光环 code 23 的 `ISpecial23Value` 字段(`range`/`square`/`hpBuff`/`atkBuff`/`defBuff`)已给表。 |
| R2-03 | 已消除 | `initializeEnemy` 已在 `enemy-specials.md` / `enemy-aura.md` 介绍。 |
| R2-04 | 已消除 | `events.md` 注明参数接口应放 `src/data/event/types.ts`。 |
| R2-05 | 已消除 | `compareEquip` 已列入 `IHeroEquipment` 接口清单。 |
| R2-06 | 已消除 | code-43 示例中的 `FaceGroup.Dir4` / `dir.mapMovement()` / `IFaceHandler` / `MapDamageType.Layer` 均有注解。 |
| R3-01 | 已消除 | 删除「15 双重含义」冲突叙述,15 现在只表示仇恨。 |
| R3-02 | 已消除 | 「27 / 伏击」统一为「25 / 捕捉」。 |
| R3-03 | 已消除 | `registerCommonQueryEffect(code, effect)` 与 `registerSpecialQueryEffect(effect)` 的签名差异已解释。 |
| R3-04 | 已消除 | 「同化」描述与示例逻辑(遍历全场、数值加算)已对齐。 |
### 非阻断 residual(仅记录,不在本计划修正)
以下为第 3 次复议后仍存在的**非阻断**残项,均属内容层 / 编辑层或命名措辞范畴,不影响读者按文档完成操作,记录供用户判断(D-14):
| # | 残项 | 定位 / 性质 |
|---|------|-------------|
| a | 这 24 篇内没有 `enemy.jsonc` 中「一条特殊属性条目带值长什么样」的示例 | 内容层 / 编辑器范畴,超出中级教程定位 |
| b | 查询示例的 code(40/41/42)没有像光环 / 最终效果页那样给出 `map.set` 定义 | 示例完整性 |
| c | 「最终效果 / 脚本」与「光环 / 查询」两处的优先级作用范围未完全区分 | 描述精度 |
| d | 「普通光环 / 特殊光环」命名过载(阶段名 vs code 23/26) | 命名措辞 |
| e | `data-plugins.md` 表格里的 `awake` 签名与占位示例不一致 | 占位示例 |
| f | `IEnemyDamageInfo` 与 `IEnemyDamageInfoBase` 命名漂移 | 命名措辞 |
| g | 措辞「30 及以上是示例编号」可能被读成 27–29 已被占用 | 措辞 |
### 校验
- 结构门禁:24 篇全部 CRLF、单 H1(`STRUCT-OK`)。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 结论:阻断性理解问题 = 0,审阅通过;非阻断残项(a)–(g)仅记录,留待用户判断(D-14)。
---
## 用户修订循环 · 第 1 轮(用户主导,领域视角)
> 规则见 `10-CONTEXT.md` D-46:子代理自审后,必须再经用户主导的修订循环(用户提问题 → AI 派子代理修改 → 回报 → 继续),经用户确认后才可收尾。
| # | 用户意见 | 主要落点 |
|---|----------|----------|
| U-01 | `content` 文件夹主要是在编辑器内修改的,对应**初级教程**,需明确说明 | `src-tour.md` |
| U-02 | 为什么有些地方获得的 `state` 主对象不包含所有层级?需解释 | `data-state-overview.md` |
| U-03 | 图块数字与图块 id 的区别、为何要区分;**新增一篇基础接口文档**简述 `TileStore` 基本概念与 `id()` / `num()` 接口 | 新文档 + `map-tiles.md` / `hero-items.md` |
| U-04 | 怎么**获取地图的指定图层**?目前只讲遍历所有图层 | `map-structure.md` |
| U-05 | 地图批量接口怎么用?说明**高级教程会详细解释** | `map-tiles.md` |
| U-06 | Flag 的**字段与字段值**区别;如何选择用哪个 | `flags.md` |
| U-07 | 怪物上下文重建**何时调用**、是否需要手动;说明样板已内置处理大部分情况,一般不需要手动重建(**不写「代码未实装」**) | 战斗各篇 |
| U-08 | 为什么不提供「既依赖外部环境、又修改外部环境」的能力——会引入**循环依赖**(依赖修改前还是修改后的环境?),要提 | `enemy-aura` / `enemy-query` / `enemy-final` |
| U-09 | `getSpecial<T>()` 的 `T` 是什么、怎么填、可否不填 | 战斗各篇 |
| U-10 | 特殊属性定义示例代码逐行是什么意思;`name` 为何可填字符串也可填函数(代码基础衔接) | `enemy-specials.md` |
| U-11 | 光环运作方式:作者能否理解转换器/视图;需说明「每个特殊属性可对应不同的转换器」 | `enemy-aura.md` |
| U-12 | 转换器/视图代码逐行注释;哪些是模板可直接照抄、哪些需自己改 | 战斗各篇 |
| U-13 | 注册上下文:给出 `initializeEnemy` 函数体(无关内容可注释省略),说明代码放哪 | 战斗各篇 |
| U-14 | 常规查询能否用 `handler.context` 替代 `query`?必须用 `query`,否则上下文认为未查询 → 后续计算不一致(严重) | `enemy-query.md` |
| U-15 | 查询上下文是 `IReadonlyEnemyContext`(只读),文档写成 `IEnemyContext` | `enemy-query.md` |
| U-16 | 其他示例也有 U-12 的问题,全部要改 | 战斗各篇 |
| U-17 | 中级应主要教**改内置代码**而非自创新类;说明直接改内置 vs 自己写的区别;仅必须自定义的(光环/查询)说明模板 vs 自己写;钩子建议直接改 `hook/` 下内置 | 战斗各篇 + `hooks.md` |
| U-18 | 「受地形遮挡的激光」示例过复杂,换成更简单的**八方向激光** | `enemy-map-damage.md` |
| U-19 | 计算器**必须无副作用**(临界/显示/手册会多次调用);「下次战斗伤害乘二」实现错误,需修正 | `enemy-calculator.md` + `enemy-status-hooks.md` |
| U-20 | 内置事件将扩展到约 100 个(发布前完成);以**已完成视角**编写,只说明新增需要改什么 | `events.md` |
| U-21 | 录像指令会在**高级教程细讲**,要说明 | `replay-commands.md` |
| U-22 | 所有会在高级教程再细讲的内容都要说明,并**预留链接**,方便后续直接链接 | 全部 |
---
## 用户循环 · batch 1 记录(战斗概念)
**范围:** 用户修订循环第 1 轮 batch 1(U-07、U-08、U-09、U-10、U-11、U-14、U-15)。
### 逐条处置
| # | 处置 | 改动文件 |
|---|------|----------|
| U-07 | 新增「什么时候重建」小节:重建由 `enemyContext` 惰性处理,`buildup()` 为全量入口、`markDirty(view)` 为局部标记;放置/移除怪物、注册效果、绑定勇士会自动标记更新,读取计算后怪物时自动重算,一般**无需手动重建**;仅绕开正常流程直接改怪物对象时才需手动 `markDirty(view)`。**不写**「代码未实装」。同步改写 `enemy-specials.md` 自定义步骤第 5 步。 | `combat-overview.md`、`enemy-specials.md` |
| U-08 | 在三方对比后新增「为什么没有『既依赖外部、又修改外部』的能力」:说明该组合会引入**循环依赖**(无法确定结果基于改动前还是改动后的环境),故刻意不提供;`enemy-aura.md` / `enemy-query.md` 各加一句点出归类并交叉引用。 | `enemy-final.md`、`enemy-aura.md`、`enemy-query.md` |
| U-09 | 详释 `getSpecial<T>(code)`:`T` 即该特殊属性 `value` 的类型(数字如连击 4、对象如光环 23 的 `ISpecial23Value`),给出 `getSpecial<number>(4)` 与 `getSpecial<ISpecial23Value>(23)`;说明可省略,但省略后 `value` 退化为取不到具体类型的宽泛类型(需自行断言),故建议显式写 `<T>`;链到 `code-basics` 的泛型。 | `enemy-specials.md` |
| U-10 | 为特殊属性定义示例补逐行注释(无参坚固 / 有参连击 / 对象参数吸血),并解释 `name` / `desc` / `color` 为何可写字符串或函数(函数收到该特殊属性、可用其 `value` 拼出动态文字),链到 `code-basics` 的函数一节。 | `enemy-specials.md` |
| U-11 | 在「运作方式」用大白话讲清转换器(分诊台)与视图(操作说明)的分工,并明确**每条特殊属性可对应不同转换器**、多个 code 也可共用一个转换器;内置三条光环各有转换器。 | `enemy-aura.md` |
| U-14 | 将「建议」改为**必须**调用 `query()`:直接读 `handler.context` 不会登记「查询过外部状态」,环境变化时上下文会视其为未查询、不重算,导致后续伤害等结果不一致甚至出错(严重)。 | `enemy-query.md` |
| U-15 | 查询上下文类型由 `IEnemyContext` 更正为 `IReadonlyEnemyContext`,并说明其只读是有意为之(避免查询效果依赖可变外部上下文)。 | `enemy-query.md` |
### 校验
- 源码核对:`packages-user/data-base/src/enemy/types.ts`(`getSpecial<T>(code): ISpecial<T> | null`、`IEnemy.getModifiableEnemy`)、`packages-user/data-base/src/enemy/special.ts`(`ICommonSpecialConfig<T>` 的 `name`/`desc`/`color: string | (special) => string`)、`packages-user/data-system/src/combat/types.ts`(`IAuraConverter` / `IAuraView` / `IEnemyAuraView`;`query: () => IEnemyContext` / `for(ctx: IEnemyContext)`;`IReadonlyEnemyContext` / `IEnemyContext`)、`packages-user/data-system/src/combat/context.ts`(`buildup()` 惰性早退、`markDirty(view)`、`requestRefresh`、`buildupQuery` 对 `queried` 的登记)。
- 结构门禁:被改的 5 篇(`combat-overview`、`enemy-specials`、`enemy-aura`、`enemy-query`、`enemy-final`)均 CRLF、单 H1。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 备注(待用户确认):`combat/types.ts` 当前 `query: () => IEnemyContext` / `for(ctx: IEnemyContext)` 的签名仍为可写 `IEnemyContext`;U-15 已按用户口径把**文档**改为只读 `IReadonlyEnemyContext`。若需源码同步为只读,请另行派发。

View File

@ -18,6 +18,16 @@ state.enemyContext.buildup();
`buildup()` 会按固定顺序处理每只怪物,把所有特殊属性的作用真正写进属性里。之后伤害计算器读到的就是最终结果,而不是模板上的原始数字。
### 什么时候重建
重建的时机由怪物上下文自己掌握,而且是**惰性**的:真正读取某只怪物的计算后属性时,上下文才会检查它需不需要重算。因此日常流程里,你一般**不需要**手动重建:
- 放置或移除怪物、注册光环转换器 / 查询效果 / 最终效果、绑定勇士,都会自动把上下文标记为「需要更新」;
- `buildup()` 是全量重建的入口,会把所有怪物的光环、查询与最终效果重新算一遍;若上下文没有变化,它会直接跳过;
- `markDirty(view)` 只把某一只怪物标记为待重算,下次读取它时做局部刷新。
也就是说,「什么时候重建」大多由样板与引擎内部处理好了。只有当你绕过正常流程、直接改动了怪物对象(例如用可写接口拿到怪物后改属性),才需要自己对那只怪物调用 `markDirty(view)`,让改动生效。
### 为什么要重建
一句话:**为了拿到准确的怪物属性**。

View File

@ -4,6 +4,8 @@
光环的效果作用在范围内的其他怪物身上(也可以包括自己)。它要回答两个问题:先算谁、后算谁?改基础属性还是改特殊属性?下面先从优先级讲起,再看内置光环与自定义写法。基础概念见[战斗总览](./combat-overview.md)。
光环属于「不读取外部环境、但会修改外部」的那一类:它只按范围命中目标,不会先观察环境再决定自己。(为什么没有「既读取外部、又修改外部」的能力,见[最终属性效果](./enemy-final.md)。)
## 优先级
每只怪物可能同时受到多个光环影响。谁先算、谁后算,会直接改变结果,所以光环用 `priority` 排队。
@ -47,6 +49,10 @@
- **转换器** `IAuraConverter`:`shouldConvert(special, handler)` 判断某个特殊属性是否归自己处理;`convert(special, handler, context)` 返回光环视图;
- **视图** `IEnemyAuraView`:描述优先级、范围、影响方式。
用大白话说:**转换器**像一个「分诊台」——引擎拿到一条特殊属性后,会挨个问每个已注册的转换器「这条你管吗?」(`shouldConvert`),哪个回答「管」,就由它把这条属性翻译成一份**视图**;**视图**则是一张「操作说明」,写清这条光环的范围多大、优先级多少、具体要改目标的哪些属性(`apply` / `applySpecial`)。
因此,**每一条特殊属性都可以对应不同的转换器**,不必都挤进同一个里;反过来,只要 `shouldConvert` 认得出来,多个 code 也可以共用同一个转换器。内置的三条光环就各有各的转换器(`CommonAuraConverter` / `GuardAuraConverter` / `SpecialAuraConverter`),下面两个自定义示例也各自写了一个转换器。
视图的核心成员:
- `priority`:优先级;

View File

@ -34,6 +34,14 @@ state.enemyContext.registerFinalEffect(effect);
最终效果可以读取勇士属性(模仿与仿攻都需要),但不会扫描周围怪物,也不会改变其他怪物。
### 为什么没有「既依赖外部、又修改外部」的能力
看到这里可能会问:为什么没有一种效果,既能像查询那样读取周围环境,又能像光环那样修改其他怪物?
因为那会引入**循环依赖**。如果一个效果既读取外部、又改动外部,就再也说不清一只怪物最终的结果是根据**改动前**的环境、还是**改动后**的环境算出来的——而环境本身又被这次效果改掉了,前后互相牵扯,结果无法稳定确定。
所以上表里「读取外部」与「影响外部」同时为「是」的那一格被刻意留空:需要「读外部之后改变自己的结果」,就用查询;需要「影响其他怪物」,就用光环。两者分开、按固定阶段执行,循环依赖就不会出现。
## 示例:仿攻
「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御(这里的 code 32 是示例自选的未占用编号;内置的最终效果是坚固 code 3、模仿 code 8,内置编号见[特殊属性](./enemy-specials.md)):

View File

@ -21,7 +21,7 @@ state.enemyContext.registerCommonQueryEffect(code, effect);
- `priority`:优先级;
- `apply(handler, special, query)`:处理逻辑。`handler.enemy` 是正在计算的怪物,`special` 是触发查询的特殊属性,`query()` 返回怪物上下文。
`query()` 不只是取上下文,它还告诉上下文「这个怪物依赖外部状态」。调用后,周围怪物或位置变化时,上下文会把这个怪物标记为需要重新查询。建议始终通过 `query()` 取上下文,而不是直接读 `handler.context`。
`query()` 不只是取上下文,它还告诉上下文「这个怪物依赖外部状态」。**常规查询里必须调用 `query()`**:直接读 `handler.context` 虽然也能拿到同一个上下文对象,却不会把「这只怪物查询过外部状态」这件事登记下来。于是当周围怪物或位置发生变化时,上下文会认为这只怪物「没有进行过查询」,不会重新计算它,导致后续的伤害等计算结果不一致、甚至出错——这是个很容易踩、后果又严重的坑。因此请始终通过 `query()` 取上下文,不要用 `handler.context` 代替。
## 特殊查询(SpecialQuery)
@ -51,9 +51,11 @@ state.enemyContext.registerSpecialQueryEffect(effect);
常规查询与特殊查询的 `priority` 和光环共用同一套优先级,从高到低处理。高优先级的效果先执行,低优先级能看到它留下的结果;反过来不行。查询新增的光环只能影响之后的阶段,规则与[怪物光环](./enemy-aura.md)一致。
查询属于「依赖外部、但只改自己」的那一类;「既依赖外部、又修改外部」的能力是刻意不提供的,原因见[最终属性效果](./enemy-final.md)。
## 查询上下文 ctx
常规查询里的 `query()` 与特殊查询里的 `for(ctx)`,拿到的都是**查询上下文**(类型 `IEnemyContext`),它代表「这次查询所在的怪物战场」。示例里用到它的这几个成员:
常规查询里的 `query()` 与特殊查询里的 `for(ctx)`,拿到的都是**查询上下文**(类型 `IReadonlyEnemyContext`),它代表「这次查询所在的怪物战场」。它是**只读**的,这是有意为之:查询效果应当只读取外部状态、再改自己,而不应反过来依赖可变的外部上下文,否则「谁先算、谁后算」的结果就无法确定。示例里用到它的这几个成员:
- `ctx.scanRange(range, param)`:用一个范围对象扫出范围内的怪物,逐项返回 `[位置, 怪物视图]`;
- `ctx.iterateEnemy()`:遍历上下文中的全部怪物,同样逐项返回 `[位置, 怪物视图]`;

View File

@ -52,6 +52,13 @@
- `hasSpecial(code)`:判断是否拥有指定特殊属性;
- `iterateSpecials()`:迭代全部特殊属性。
其中 `getSpecial<T>(code)` 的尖括号 `T` 是**这条特殊属性的参数类型**,也就是它 `value` 的类型:连击(code 4)的 `value` 是次数(`number`),光环(code 23)的 `value` 是 `ISpecial23Value` 对象。写 `<T>` 就是在告诉引擎「我要按这个类型读它的 `value`」:
- 参数是数字:`enemy.getSpecial<number>(4)`,之后 `value` 就是 `number`,可以直接参与计算;
- 参数是对象:`enemy.getSpecial<ISpecial23Value>(23)`,之后 `value.range`、`value.hpBuff` 等字段都能直接读取。
`T` 也可以省略(写成 `getSpecial(4)`)。但省略后引擎推断不出具体类型,`value` 会退化为一个取不到具体类型的宽泛类型,必须自己断言之后才能当数字或对象用。因此读取已知属性时,建议把 `<T>` 显式写出来。`T` 的写法(尖括号里填类型)就是[代码基础](./code-basics.md)里讲的泛型。
每个特殊属性对象实现 `ISpecial<T>`,常用成员是 `code`、`value` 与 `getDescription()`:
```ts
@ -80,27 +87,38 @@ if (enemy.hasSpecial(3)) {
`config` 的 `name`、`desc`、`color` 都可以写成字符串,也可以写成 `(special) => string` 的函数。需要根据参数变化显示不同文字时,就写成函数:
```ts
// 无参:坚固
// 无参:坚固。用 defineNonePropertySpecial 定义,value 为空,所以不需要默认值
map.set(
3,
3, // 往属性表里登记 code 3
defineNonePropertySpecial(3, {
name: '坚固',
desc: '怪物防御不小于勇士攻击-1。',
color: '#C0B088'
name: '坚固', // 名称:固定字符串,直接显示
desc: '怪物防御不小于勇士攻击-1。', // 描述:固定字符串
color: '#C0B088' // 名称显示颜色
})
);
// 有参:n连击,默认 2 次
// 有参:n连击。用 defineCommonSerializableSpecial 定义
map.set(
4,
defineCommonSerializableSpecial(4, 2, {
name: special => `${special.value}连击`,
desc: special => `怪物每回合攻击${special.value}次。`,
color: '#FE7'
})
defineCommonSerializableSpecial(
4, // code
2, // value 的默认值:默认每回合攻击 2 次
{
name: special => `${special.value}连击`, // 名称写成函数:按 value 拼出「2连击」
desc: special => `怪物每回合攻击${special.value}次。`, // 描述随 value 变化
color: '#FE7'
}
)
);
```
`name`、`desc`、`color` 既可以直接写字符串,也可以写成 `(special) => string` 的函数,区别在于文字是否要随参数变化:
- 写字符串时,这段文字是固定的,例如坚固永远显示「坚固」;
- 写函数时,引擎会在需要显示时把这条特殊属性对象传进来(就是参数 `special`),你就能用它的 `value` 拼出动态文字——上面的连击在 `value` 为 2 时显示「2连击」,为 3 时显示「3连击」。
函数这种「接收参数、返回结果」的写法,见[代码基础](./code-basics.md)的函数一节。
参数可以不只是数字。比如吸血用对象保存比例与是否加到自身:
```ts
@ -114,11 +132,13 @@ export interface ISpecial9Value {
map.set(
9,
// 参数是对象时,把类型写进尖括号,value 默认值也是一个对象
defineCommonSerializableSpecial<ISpecial9Value>(
9,
{ vampire: 0, add: false },
9, // code
{ vampire: 0, add: false }, // value 的默认值:不吸血
{
name: '吸血',
name: '吸血', // 固定名称
// 描述写成函数,从对象 value 里读字段拼出文字
desc: special => `战斗前吸取勇士${special.value.vampire}%生命。`,
color: '#DD4448'
}
@ -134,7 +154,7 @@ map.set(
2. **有参数时定义参数接口**。在文件顶部 `//#region 复合属性值类型` 内声明 `ISpecial50Value`,并在接口前写 `/** 50 - 属性名 */`。
3. **在 `//#region 属性定义` 内注册**。每条定义前写 `// 50 - 属性名`,再用 `map.set(50, define...)` 添加。
4. **实现效果**。按下方「属性实现位置」表,把效果写进对应的计算器、光环、地图伤害或钩子;如果不属于现有分类,再考虑新增对应的转换器 / 效果 / 钩子。
5. **刷新上下文**。修改怪物模板或新增属性后,调用 `state.enemyContext.markDirty(view)` 标记脏数据,必要时调用 `state.enemyContext.buildup()` 全量重建计算后怪物。
5. **刷新上下文(通常无需手动)**。重建由怪物上下文惰性处理:放置 / 移除怪物、注册效果、绑定勇士都会自动标记更新,读取计算后怪物时自动重算。只有当你绕开正常流程、直接改动了怪物对象时,才需要手动 `state.enemyContext.markDirty(view)` 标记脏数据(必要时 `state.enemyContext.buildup()` 全量重建)。详见[战斗总览](./combat-overview.md)的「什么时候重建」。
```ts
// 50 - 灼烧