docs(10-11): 按 D-48 重做实操(战斗组)

- 战斗组 9 篇的实操/示例改为六步(目标→改哪里→复制什么→改什么→怎么验证→注意)
- 各篇代码片段去掉 import,并补统一复制说明(指向 code-basics)
- 保留系统/原理讲解与 U-07..U-19 既有修正;combat-overview 无实操不改
- 追加 10-11-READER-REVIEW.md 的 batch 5 记录
This commit is contained in:
unanmed 2026-10-19 15:30:42 +08:00
parent cfa602f25c
commit 454325052f
9 changed files with 206 additions and 142 deletions

View File

@ -384,3 +384,34 @@
- 边界:仅改 `docs/intermediate/*.md`(`data-vs-render`、`src-tour` 无实操、不改);未改 `config.ts` 与任何生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释)。
- 未决:战斗组与「其他」组(`combat-overview`、`enemy-*`、`events`、`replay-commands`、`data-plugins`)的实操尚未按 D-48 重做,其中仍是含 `import` 的代码块;留待后续批次。
---
## 用户循环 · batch 5 记录(D-48 实操·战斗)
**范围:** 用户修订循环第 1 轮 batch 5(D-48 实操写法)——战斗组 9 篇(`combat-overview`、`enemy-specials`、`enemy-aura`、`enemy-query`、`enemy-final`、`enemy-calculator`、`enemy-map-damage`、`enemy-combat-script`、`enemy-status-hooks`)。沿用 batch 4 的口径:**保留系统/原理讲解**,只重做「实操/示例」部分,使其可分步照抄(目标 → 改哪里 → 复制什么 → 改什么 → 怎么验证 → 注意);片段一律去掉 `import`,并显式给出「顶部通常已有、不要重复粘,报缺再补」的说明(统一指向 `code-basics.md` 的「照抄教程里的代码时,怎么处理 import」)。既有的 U-07/U-08/U-09/U-10/U-11/U-13/U-14/U-15/U-17/U-18/U-19 修正全部保留。
### 逐条处置
| 文档 | 处置 |
| --- | --- |
| `combat-overview.md` | 无「实操/示例」段落(纯概念 + 流程图 + 一处 `buildup()` 单行接口引用,本就不含 `import`),本批不改。 |
| `enemy-specials.md` | 「自定义一个新特殊属性」的 5 步清单改写为六步实操;属性定义片段补 `[需改]` 标注;「两类定义」「内置属性编号表」等系统讲解保留。 |
| `enemy-aura.md` | 两个示例(基础光环 / 特殊光环)各改写为六步实操;删去两个代码块的 `import`,补复制说明;优先级、运作方式、「改内置还是自己写」等系统讲解保留。 |
| `enemy-query.md` | 三个示例(抱团 / 同化 / 号角)各改写为六步实操;删去三处 `import`,补复制说明;`ctx` 说明、warning、优先级、签名差异说明等保留。 |
| `enemy-final.md` | 「示例:仿攻」改写为六步实操;删去 `import`,补复制说明;「注册在哪里」(U-13)保留。 |
| `enemy-calculator.md` | 「示例:等差数列属性」改写为六步实操;删去 `import`,补复制说明;「计算器必须无副作用」(U-19)warning 与内置公式逐段导读保留。 |
| `enemy-map-damage.md` | 「示例:八方向激光」(U-18)改写为六步实操;删去 `import`,补复制说明;五类内置表、编号/结算说明、`handleMapDamage` 结算段保留。 |
| `enemy-combat-script.md` | 战前 / 战后两个示例各改写为六步实操;删去两处 `import`,补复制说明;「脚本 vs 钩子」选型表与「注册在哪里」保留。 |
| `enemy-status-hooks.md` | 「下次战斗伤害乘二」示例改写为六步实操(一处「复制什么」统领「属性定义 + 钩子 + 脚本」三个片段,第三步计算器只读片段单列);删去两处 `import`;按 U-19 保留「钩子挂状态 → 计算器只读 flag → 战斗脚本每场消费一次」的三系统结构(消费必须每场只跑一次,属 D-48「确实需要」的额外脚本)。 |
### 共用复制说明(各篇统一)
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 `code-basics.md` 的「照抄教程里的代码时,怎么处理 import」)。
### 校验
- 结构门禁:24 篇全部 CRLF、单 H1(`STRUCT-OK`)。
- 格式门禁:`node node_modules/prettier/bin/prettier.cjs --write`(D-45)已跑,改动篇目全部 `unchanged`(内容本就符合 prettier)。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 边界:仅改 `docs/intermediate/*.md`(`combat-overview` 无实操、不改);未改 `config.ts` 与任何生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释);未引入 `dev.md` 引擎开发约定,未写发布前/数量类信息。

View File

@ -79,11 +79,17 @@
- **转换器**:只有 `shouldConvert` 与 `convert` 两个方法,负责「认领」特殊属性并构造视图。`shouldConvert` 通常照抄、只换一个 code;`convert` 里构造视图那一行要按你的视图构造函数改。
- **视图**:真正描述效果的地方(范围、优先级、`apply` / `applySpecial`),这部分必须按你的需求写。
下面两个示例用 `[模板]` 标出可原样照抄的行(多为依赖导入、接口签名、`shouldConvert`),用 `[需改]` 标出必须按需求改写的行(code、范围、优先级,以及 `apply` / `applySpecial` 里做的事)。
下面两个示例用 `[模板]` 标出可原样照抄的行(多为接口签名、`shouldConvert`、范围对象的构建),用 `[需改]` 标出必须按需求改写的行(code、范围、优先级,以及 `apply` / `applySpecial` 里做的事)。
## 示例:基础光环(减少周围怪物的金币与经验)
先从一个只改基础属性的例子开始:让范围内的怪物金币与经验下降。第一步是定义一条带参数的特殊属性(code 取一个未占用的值,这里用 30):
先从一个只改基础属性的例子开始:让范围内的怪物金币与经验下降。它用 code 30(示例自选的未占用号)定义一条带参数的特殊属性,再配一个转换器与视图。
**目标**:站在带「削弱光环」怪物周围的怪物,金币与经验按参数下降。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写转换器与视图;`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**复制什么**:先在 `special.ts` 登记属性——
```ts
// [需改] 在 src/data/enemy/special.ts 里新增这条属性定义(编号换成你没用过的)
@ -111,29 +117,11 @@ map.set(
);
```
然后写转换器与视图:
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写转换器与视图——
```ts
// [模板] 依赖导入:@motajs/common 提供范围对象,其余来自用户包
import {
IManhattanRangeParam,
IRange,
IRectRangeParam,
ITileLocator,
ManhattanRange,
RectRange
} from '@motajs/common';
import {
IAuraConverter,
IEnemyAuraView,
IEnemyHandler,
IEnemySpecialModifier,
IReadonlyEnemyHandler
} from '@user/data-system';
import { IReadonlyEnemy, ISpecial } from '@user/data-base';
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { ISpecial30Value } from './special';
// [模板] 两个范围对象各建一次复用
const MANHATTAN_RANGE = new ManhattanRange();
const RECT_RANGE = new RectRange();
@ -227,17 +215,27 @@ export class WeakAura implements IEnemyAuraView<
}
```
注册:
最后把注册行加进 `initializeEnemy`(`src/data/enemy/initialize.ts`,见文末「注册在哪里」):
```ts
state.enemyContext.registerAuraConverter(new WeakAuraConverter());
```
`apply` 里用 `baseEnemy` 的原始值做百分比,因此多个光环不会把彼此的结果继续乘进去。
**改什么**:编号(`30`)、类名、`priority`、范围参数与 `apply` 里改的字段(本示例 `moneyBuff` / `expBuff`)换成你自己的。
**怎么验证**:把带该属性的怪物与另一只怪物放在范围内,看后者的金币 / 经验是否按参数下降。
**注意**:`apply` 里用 `baseEnemy` 的原始值做百分比,因此多个光环不会把彼此的结果继续乘进去。只改基础属性就实现 `apply`;要改目标的特殊属性才需要 `applySpecial`。
## 示例:特殊光环(给周围怪物上光环)
如果光环要给目标「加一条特殊属性」,而不是直接改基础属性,就把逻辑放进 `applySpecial`。下面的例子给范围内没有普通光环的怪物加上 code 23 的普通光环:
如果光环要给目标「加一条特殊属性」,而不是直接改基础属性,就把逻辑放进 `applySpecial`。下面的例子给范围内没有普通光环的怪物加上 code 23 的普通光环。
**目标**:站在带「传染光环」怪物附近的怪物,若自己没有普通光环,就获得一层 code 23 的普通光环。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写转换器与视图;`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**复制什么**:先在 `special.ts` 登记属性——
```ts
// [需改] 这条无参属性定义同样加进 src/data/enemy/special.ts
@ -252,18 +250,11 @@ map.set(
);
```
```ts
// [模板] 依赖导入
import { FullRange, ITileLocator } from '@motajs/common';
import {
IAuraConverter,
IEnemyAuraView,
IEnemySpecialModifier,
IReadonlyEnemyHandler
} from '@user/data-system';
import { IReadonlyEnemy, ISpecial } from '@user/data-base';
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写转换器与视图——
```ts
// [需改] 类名:换成你的修饰器名;它负责「给目标增删改特殊属性」
export class ChainAuraModifier implements IEnemySpecialModifier<IEnemyAttr> {
// [模板] 构造函数存下来源属性
@ -351,9 +342,11 @@ export class ChainAura implements IEnemyAuraView<IEnemyAttr, void, void> {
}
```
这里 `priority = 25` 高于内置特殊光环的 20,所以它在特殊光环之前执行;它新加上的普通光环(priority 10)会在之后的阶段生效。
**改什么**:编号(`31`)、类名、`priority`、范围与 `add` 里设置的参数(本示例的 `range` / `hpBuff` 等光环字段)换成你自己的。
不要把 `couldApplySpecial` 设为 `false` 后又指望 `applySpecial` 被调用:上下文只会对 `couldApplySpecial === true` 的光环调用它。
**怎么验证**:把带该属性的怪物与一只没有普通光环的怪物放在范围内,看后者是否被加上了普通光环(属性提升)。
**注意**:`priority = 25` 高于内置特殊光环的 20,所以它在特殊光环之前执行;它新加上的普通光环(priority 10)会在之后的阶段生效。不要把 `couldApplySpecial` 设为 `false` 后又指望 `applySpecial` 被调用:上下文只会对 `couldApplySpecial === true` 的光环调用它。
## 注册与全局光环

View File

@ -70,18 +70,15 @@ state.enemyContext.getDamageSystem()?.useCalculator(new MainDamageCalculator());
「等差数列属性」指怪物攻击每回合按固定值递增。属性本身在一次计算中是常量,所以不能靠给 `atk` 加值实现;要在 `calculate` 里把「每回合伤害」按等差数列求和:
示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。下面用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求修改的行。
示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。下面用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。
**目标**:让怪物的每回合伤害按固定公差逐回合递增(本示例公差为 5)。
**改哪里**:`src/data/enemy/calculator.ts` 的 `MainDamageCalculator.calculate`(改内置),或另建文件写一个计算器再用 `useCalculator` 替换。
**复制什么**:下面是只保留本示例所需分支的最小计算器——
```ts
// [模板] 依赖导入:类型分别来自 @user/data-common 与 @user/data-system
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
CriticalableHeroStatus,
IDamageCalculator,
IEnemyDamageInfoBase,
IReadonlyEnemyHandler
} from '@user/data-system';
// [需改] 类名:换成你自己的计算器名
export class ArithmeticCalculator implements IDamageCalculator<
IEnemyAttr,
@ -127,7 +124,13 @@ export class ArithmeticCalculator implements IDamageCalculator<
}
```
这是只保留本示例所需分支的最小计算器;内置公式里的无敌、吸血、支援等分支需要你按上文顺序自行保留。
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:`step`(公差,本示例为 `5`)与 `base` 的算法换成你要的递增规则;换算成「改内置」时,把这段逻辑并入 `calculate` 的对应段落即可。
**怎么验证**:给怪物挂上后开战,看显示的回合伤害是否随回合数按公差递增、总伤害是否等于求和结果。
**注意**:这是只保留本示例分支的最小计算器;若用 `useCalculator` 替换,内置的无敌、吸血、支援等分支需要你按上文顺序自行保留。计算器**必须无副作用**(见上方 warning)。
## 临界

View File

@ -59,17 +59,15 @@ state.combat.addCombatScript(new MainCombatScript());
## 战前示例:拥有某特殊属性时无法战斗
下面的脚本检查怪物是否拥有 code 18 的特殊属性(无敌,编号见[特殊属性](./enemy-specials.md)的「内置属性编号表」),有则直接放弃战斗(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。代码中用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求修改的行。
下面的脚本检查怪物是否拥有 code 18 的特殊属性(无敌,编号见[特殊属性](./enemy-specials.md)的「内置属性编号表」),有则直接放弃战斗(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。代码中用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。
**目标**:带「无敌」(code 18)的怪物不能被打,点它时直接放弃战斗并给出提示。
**改哪里**:另建文件写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [模板] 依赖导入:类型分别来自 @user/data-common 与 @user/data-system
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
ICombatFlowHandler,
ICombatScript,
IEnemyDamageInfo
} from '@user/data-system';
// [需改] 类名:换成你的脚本名
export class SealCombatScript implements ICombatScript<IEnemyAttr, IHeroAttr> {
// [需改] priority:越大越先执行;内置脚本是 0
@ -99,23 +97,25 @@ export class SealCombatScript implements ICombatScript<IEnemyAttr, IHeroAttr> {
state.combat.addCombatScript(new SealCombatScript());
```
`priority = 100` 让它先于内置脚本执行;返回 `false` 后,后续战前脚本、战后脚本与 `onAfterCombat` 钩子都不会执行。
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
这里的 18 号在内置属性编号表里就是「无敌」,地图伤害的转换器也使用同一套编号(阻击是 16),所以这个判断与其他系统没有编号冲突。
**改什么**:`priority`(本示例 `100`,越大越先执行,内置脚本是 `0`)、`hasSpecial(18)` 的判断与提示文字换成你自己的。
**怎么验证**:给一只怪物挂上「无敌」后点它开战,应看到提示、且勇士不入场、战斗不结算。
**注意**:`before` 一旦返回 `false`,后续战前脚本、战后脚本与 `onAfterCombat` 钩子都不会执行。这里的 18 号在内置属性编号表里就是「无敌」,地图伤害使用同一套编号(阻击是 16),所以这个判断与其他系统没有编号冲突。
## 战后示例:战后玩家回复固定量生命值
下面的脚本在每场战斗后给勇士回复 20 点生命:
```ts
// [模板] 依赖导入
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
ICombatFlowHandler,
ICombatScript,
IEnemyDamageInfo
} from '@user/data-system';
**目标**:每场战斗结束后,给勇士回复固定量生命(本示例 20 点)。
**改哪里**:另建文件写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [需改] 类名:换成你的脚本名
export class HealAfterCombatScript implements ICombatScript<
IEnemyAttr,
@ -143,9 +143,13 @@ export class HealAfterCombatScript implements ICombatScript<
state.combat.addCombatScript(new HealAfterCombatScript());
```
`priority = -10` 让它在内置脚本之后执行,避免在奖励结算之前改动勇士状态。`handler.hero` 是可修改对象,直接 `add` 即可。
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
`priority` 只需要记住一句:数值越大越先执行;战前与战后各自按这个顺序遍历脚本列表。
**改什么**:`priority`(本示例 `-10`,设成负值排在内置脚本之后)、回复量 `20` 与回复的属性换成你自己的。
**怎么验证**:打完一场战斗,看勇士生命是否回复了设定量,且奖励结算仍正常。
**注意**:`handler.hero` 是可修改对象,直接 `add` 即可。`priority` 越大越先执行;战前与战后各自按这个顺序遍历脚本列表。把 `priority` 设成负值可在内置脚本之后执行,避免在奖励结算前改动勇士状态。
## 注册在哪里

View File

@ -53,11 +53,15 @@ state.enemyContext.registerFinalEffect(effect);
## 示例:仿攻
「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御(这里的 code 32 是示例自选的未占用编号;内置的最终效果是坚固 code 3、模仿 code 8,内置编号见[特殊属性](./enemy-specials.md)):
「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御(这里的 code 32 是示例自选的未占用编号;内置的最终效果是坚固 code 3、模仿 code 8,内置编号见[特殊属性](./enemy-specials.md))。
示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。
示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。下面代码用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。
下面代码中用 `[模板]` 标出可以原样照抄的部分,用 `[需改]` 标出必须按你的需求修改的部分。特殊属性定义写在 `src/data/enemy/special.ts`:
**目标**:让带「仿攻」(code 32)的怪物,攻击力始终等于勇士的最终攻击力。
**改哪里**:`src/data/enemy/special.ts` 登记一条属性;另建一个文件放最终效果的类;`src/data/enemy/initialize.ts` 注册这个效果(见文末「注册在哪里」)。
**复制什么**:先在 `special.ts` 里登记属性——
```ts
// special.ts:32 - 仿攻
@ -72,13 +76,11 @@ map.set(
);
```
然后写最终效果本身:
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写最终效果的类——
```ts
// [模板] 依赖导入:类型分别来自 @user/data-common 与 @user/data-system
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemyFinalEffect, IEnemyHandler } from '@user/data-system';
// [需改] 类名:换成你自己的效果名
export class AttackCopyFinal implements IEnemyFinalEffect<
IEnemyAttr,
@ -103,9 +105,11 @@ export class AttackCopyFinal implements IEnemyFinalEffect<
state.enemyContext.registerFinalEffect(new AttackCopyFinal());
```
`apply` 里直接 `setAttribute`,因为最终阶段拿到的 `handler.enemy` 就是可修改的怪物对象。最终阶段之后不再有别的属性修正,所以这里写进去的值会直接进入伤害计算。
**改什么**:编号(`32`)、名称、描述与颜色换成你自己的;`hasSpecial(32)` 的触发条件与 `setAttribute` 里的效果换成你要的逻辑。
`priority` 只需要记住一句:数值越大越先执行;内置最终效果的 `priority` 是 `0`。
**怎么验证**:给一只怪物挂上「仿攻」后重建并开战,看它显示的攻击是否等于勇士当前最终攻击(勇士换装备后应同步变化)。
**注意**:`apply` 里 `handler.enemy` 已是可修改对象,直接 `setAttribute` 即可;最终阶段之后不再有别的属性修正,这里写进去的值会直接进入伤害计算。`priority` 越大越先执行,内置最终效果的 `priority` 是 `0`。
### 注册在哪里

View File

@ -64,7 +64,13 @@ mapDamage?.useConverter(new MainMapDamageConverter());
`useConverter` 是**替换**当前转换器:选了自己的就把内置的整个换掉,若要同时保留内置五类,需要在自己的转换器里把它们的视图也收集进去,或直接改内置转换器。
下面代码用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求修改的行。先在 `src/data/enemy/special.ts` 登记这条属性:
下面代码用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。
**目标**:新增一个「八方向激光」特殊属性(示例 code 43),沿上下左右加四个斜向共八个方向发射,其余与内置激光一致。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写伤害视图与转换器;`initializeEnemy` 里用 `useConverter` 替换(见下方「注册在哪里」)。
**复制什么**:先在 `src/data/enemy/special.ts` 登记这条属性——
```ts
/** 43 - 八方向激光 */
@ -88,30 +94,11 @@ map.set(
);
```
再写伤害视图与转换器:
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写伤害视图与转换器——
```ts
// [模板] 依赖导入:类型分别来自 @motajs/common、@user/data-system、@user/data-base 与 @user/data-common
import {
IFaceDescriptor,
IFaceHandler,
IRange,
IRayRangeParam,
ITileLocator,
RayRange
} from '@motajs/common';
import {
IEnemyContext,
IMapDamageConverter,
IMapDamageInfo,
IMapDamageView,
IReadonlyEnemyHandler
} from '@user/data-system';
import { ISpecial } from '@user/data-base';
import { IEnemyAttr, IHeroAttr, FaceGroup } from '@user/data-common';
import { MapDamageType } from './types';
import { ISpecial43Value } from './special';
// [模板] 范围对象可以复用,在模块顶层建一次即可
const RAY_RANGE = new RayRange();
@ -198,6 +185,12 @@ const mapDamage = state.enemyContext.getMapDamage();
mapDamage?.useConverter(new EightDirLaserConverter());
```
**改什么**:编号(`43`)、类名、方向组(本示例 `FaceGroup.Dir8`)与 `getDamageWithoutCheck` 返回的伤害 / 类型换成你自己的。
**怎么验证**:给带该属性的怪物挂上后,站在它八个方向任一格的勇士都应受到伤害;斜向也要覆盖到(这正是与内置四方向激光的区别)。
**注意**:`useConverter` 是**替换**当前转换器——选了自己的就把内置的整个换掉;要同时保留内置五类,需要在自己的转换器里把它们的视图也收集进去,或直接改内置转换器。
### 注册在哪里
`useConverter` 要写进 `initializeEnemy`。下面是 `src/data/enemy/initialize.ts` 中该函数的样子(省略了无关的行):

View File

@ -74,14 +74,15 @@ state.enemyContext.registerSpecialQueryEffect(effect);
## 示例:抱团
「抱团」的规则是:范围内每有一个带同种特殊属性的怪物,自身攻击力就增加固定值。它只需要读周围、改自己的攻击,所以用常规查询实现(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」):
「抱团」的规则是:范围内每有一个带同种特殊属性的怪物,自身攻击力就增加固定值。它只需要读周围、改自己的攻击,所以用常规查询实现(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。
**目标**:带「抱团」(示例 code 40)的怪物,范围内每多一个同种特殊属性的怪物,自身攻击加固定值。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [模板] 依赖导入
import { ManhattanRange } from '@motajs/common';
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemyCommonQueryEffect } from '@user/data-system';
// [需改] 变量名:这是一个常规查询效果对象
const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
// [需改] priority:与光环共用一套优先级,越大越先执行
@ -125,18 +126,27 @@ const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
state.enemyContext.registerCommonQueryEffect(40, effect);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:`priority`、触发用的 `special.code` 与注册时的 code(本示例 40)、`radius` / `atk` 的读法换成你自己的。
**怎么验证**:给带该属性的怪物周围放上同类怪物,看它的攻击是否随同类数量按 `atk` 递增。
**注意**:`apply` 里必须用 `query()` 取上下文(直接读 `handler.context` 不会被登记为「查询过」,见上文)。
这个需求如果改用光环实现会麻烦得多:光环只能逐个目标施加减益或增益,很难先统计「范围内有多少个同类」再一次性加攻。查询可以在一次 `apply` 里完成扫描与累加。
## 示例:同化
「同化」的规则是:把场上其他怪物的特殊属性搬到自己身上——自己还没有的属性直接照抄(先按位置索引从小到大排序,靠前位置的属性先被抄过来);自己已经有的数值型属性则与场上其他怪物的同一属性加算叠加。它要增删、改写自己拥有的特殊属性,所以用特殊查询(示例用 `ctx.iterateEnemy()` 遍历全场;若要限定范围,把遍历换成 `ctx.scanRange`):
「同化」的规则是:把场上其他怪物的特殊属性搬到自己身上——自己还没有的属性直接照抄(先按位置索引从小到大排序,靠前位置的属性先被抄过来);自己已经有的数值型属性则与场上其他怪物的同一属性加算叠加。它要增删、改写自己拥有的特殊属性,所以用特殊查询(示例用 `ctx.iterateEnemy()` 遍历全场;若要限定范围,把遍历换成 `ctx.scanRange`)。
**目标**:带「同化」(示例 code 41)的怪物,把其他怪物的特殊属性搬到自己身上(没有的照抄,数值型的叠加)。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerSpecialQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [模板] 依赖导入
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemySpecialQueryEffect } from '@user/data-system';
import { ISpecial } from '@user/data-base';
// [需改] 变量名:特殊查询效果对象
const effect: IEnemySpecialQueryEffect<IEnemyAttr, IHeroAttr> = {
priority: 15, // [需改] 优先级
@ -202,17 +212,25 @@ const effect: IEnemySpecialQueryEffect<IEnemyAttr, IHeroAttr> = {
state.enemyContext.registerSpecialQueryEffect(effect);
```
`add` 负责处理「自己还没有的属性」,`modify` 负责处理「自己已经有、需要叠加数值」的属性。两者配合,就能把范围内怪物的特殊属性合并到自身。
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:`priority`、`shouldQuery` 里的判断(本示例 `hasSpecial(41)`)、`add` / `modify` 的合并逻辑换成你自己的。
**怎么验证**:给一只怪物挂上「同化」,让它周围有其他带特殊属性的怪物,看它是否照抄了别人的属性、数值型是否叠加。
**注意**:`add` 与 `delete` 不要同时返回非空结果(见上文);`add` 负责处理「自己还没有的属性」,`modify` 负责处理「自己已经有、需要叠加数值」的属性。两者配合,就能把范围内怪物的特殊属性合并到自身。
## 示例:号角
「号角」的规则是:场上每有一只怪物,自身就获得一份加成。它不关心范围与同类,用常规查询遍历全场即可:
```ts
// [模板] 依赖导入
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemyCommonQueryEffect } from '@user/data-system';
**目标**:带「号角」(示例 code 42)的怪物,场上每有一只怪物就给自身加一份攻击。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [需改] 变量名:同样是常规查询效果对象
const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
priority: 5, // [需改] 优先级
@ -234,6 +252,14 @@ const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
state.enemyContext.registerCommonQueryEffect(42, effect);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:`priority`、注册时的 code(本示例 42)、`special.value` 的用法换成你自己的。
**怎么验证**:把怪物放到场上(场上有其他怪物),看它的攻击是否随全场怪物数量递增。
**注意**:`apply` 里同样必须用 `query()` 取上下文。
三个示例分别覆盖了「按范围统计同类」「合并特殊属性」「遍历全场计数」三种典型用法。
## 注册在哪里

View File

@ -152,24 +152,32 @@ map.set(
当你需要引擎里没有的机制时,就新增一条特殊属性。以「每回合给勇士造成固定伤害」为例(假设使用 code 50):
1. **选一个未使用的 code**。先搜一遍 `special.ts` 中已有的 `map.set(...)`,确认没有冲突。
2. **有参数时定义参数接口**。在文件顶部 `//#region 复合属性值类型` 内声明 `ISpecial50Value`,并在接口前写 `/** 50 - 属性名 */`。
3. **在 `//#region 属性定义` 内注册**。每条定义前写 `// 50 - 属性名`,再用 `map.set(50, define...)` 添加。
4. **实现效果**。按下方「属性实现位置」表,把效果写进对应的计算器、光环、地图伤害或钩子;如果不属于现有分类,再考虑新增对应的转换器 / 效果 / 钩子。
5. **刷新上下文(通常无需手动)**。重建由怪物上下文惰性处理:放置 / 移除怪物、注册效果、绑定勇士都会自动标记更新,读取计算后怪物时自动重算。只有当你绕开正常流程、直接改动了怪物对象时,才需要手动 `state.enemyContext.markDirty(view)` 标记脏数据(必要时 `state.enemyContext.buildup()` 全量重建)。详见[战斗总览](./combat-overview.md)的「什么时候重建」。
**目标**:新增一条「每回合对勇士造成固定伤害」的特殊属性(示例 code 50)。
**改哪里**:`src/data/enemy/special.ts`——有参数时在顶部 `//#region 复合属性值类型` 内加参数接口,在 `//#region 属性定义` 内加 `map.set(...)`;效果实现按下方「属性实现位置」表找落点。
**复制什么**:
```ts
// 50 - 灼烧
map.set(
50,
50, // [需改] 编号
defineCommonSerializableSpecial(50, 0, {
name: '灼烧',
desc: special => `每回合对勇士造成${special.value}点伤害。`,
color: '#F80'
name: '灼烧', // [需改] 名称
desc: special => `每回合对勇士造成${special.value}点伤害。`, // [需改] 说明
color: '#F80' // [需改] 颜色
})
);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:先搜一遍 `special.ts` 中已有的 `map.set(...)`,确认这个 code 没被占用;再把编号、名称、默认值与颜色换成你自己的。有参数且参数是对象时,还要按上面的步骤在顶部补一个 `ISpecial{code}Value` 参数接口。
**怎么验证**:把这条新属性挂到怪物上(在怪物数据里填上 code 50 与参数),编辑器应能识别并显示名称与描述;再按「属性实现位置」实现的效果应在战斗中生效。
**注意**:别破坏编辑器识别约定——`//#region` 标记、`// {code} - {name}` 与 `/** {code} - {name} */` 注释、参数接口名 `ISpecial{code}Value`(见下方「编辑器识别约定」)。刷新上下文通常无需手动:重建由怪物上下文惰性处理(放 / 移怪物、注册效果、绑定勇士都会自动标记),读取计算后怪物时自动重算;只有绕开正常流程、直接改怪物对象时才需要手动 `state.enemyContext.markDirty(view)`(必要时 `state.enemyContext.buildup()` 全量重建)。详见[战斗总览](./combat-overview.md)的「什么时候重建」。
`createSpecials(state)` 返回 `Map<number, SpecialCreation<any>>`。`initializeEnemy`(`src/data/enemy/initialize.ts`,数据端集中注册内置怪物功能的入口函数)会遍历这个 Map,逐个调用 `state.enemyManager.registerSpecial(code, cons)`。也就是说,内置属性与你自己新增的属性走的是同一套注册流程:只要把新属性放进这个 Map,它就会随 `initializeEnemy` 一起完成注册。
## 属性实现位置

View File

@ -66,7 +66,13 @@ state.hero.location.addHook(new EnemyChangeFloorHooks(state));
不要在伤害计算器里 `deleteField` 之类地去「消费」这个 flag。计算器在一次战斗里会被反复调用(算临界、显示伤害、图鉴等),谁先调用谁就把 flag 用掉了,真正开打时反而不翻倍;而且这属于改动外部状态,违反「计算器必须无副作用」的规则(见[伤害计算](./enemy-calculator.md))。正确做法是:**计算器只读 flag**,由「一场战斗只跑一次」的**战斗脚本**去消费它。
:::
先定义一个无参数特殊属性(code 45)。下面代码中用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求修改的行:
本示例要串起三个系统:先用战斗钩子在战斗结束时挂上「蓄力」状态,让伤害计算器只读这个 flag 把伤害翻倍,再由战斗脚本在一次战斗后把 flag 消费掉。
**目标**:带「蓄力」(code 45)的怪物打完后,下一场战斗的伤害翻倍,且只翻倍一次。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写战斗钩子与战斗脚本;`initializeEnemy` 里注册(见文末)。另外在伤害计算器里加一段只读逻辑(见第三步)。
**复制什么**:先定义一个无参数特殊属性(code 45)——
```ts
// special.ts:45 - 蓄力
@ -81,14 +87,11 @@ map.set(
);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**第一步:用战斗钩子「挂上状态」。** 它不修改内置的 `EnemyCombatHooks`,而是作为另一个钩子并存(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」):
```ts
// [模板] 依赖导入
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { ICoreState } from '@user/data-state';
import { ICombatFlowHooks, IEnemyDamageInfo } from '@user/data-system';
// [需改] 类名
export class ChargedHooks implements ICombatFlowHooks<IEnemyAttr, IHeroAttr> {
constructor(readonly state: ICoreState) {} // [模板] 存下数据端状态,供读写 flag
@ -112,11 +115,6 @@ state.combat.addHook(new ChargedHooks(state));
**第二步:用战斗脚本「消费状态」。** 脚本的 `after` 一场战斗只执行一次,正好用来把 flag 清掉:
```ts
// [模板] 依赖导入
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { ICoreState } from '@user/data-state';
import { ICombatScript } from '@user/data-system';
// [需改] 类名
export class ConsumeDoubleScript implements ICombatScript<
IEnemyAttr,
@ -157,6 +155,10 @@ if (double) {
把这段读 flag 的逻辑放在 `calculate` 返回之前即可;伤害公式的其余部分见[伤害计算](./enemy-calculator.md)。
这个例子体现了「一个状态要用多个系统」:`onAfterCombat` 钩子负责记录状态,计算器负责读取并翻倍,战斗脚本负责在真正的战斗里消费掉它。若把消费放进计算器,就会因计算器被反复调用而提前把 flag 用掉——只有放进「每次战斗只跑一次」的脚本里才可靠。
**改什么**:编号(`45`)、类名、flag 名(`nextBattleDouble`)、翻倍写法与注册位置换成你自己的。
**怎么验证**:让带「蓄力」的怪物战斗一场,再打一场,看第二场伤害是否翻倍;连打第三场时应恢复原值(flag 已被消费)。
**注意**:这里要三个系统配合,缺一不可——钩子负责记录状态,计算器只读并翻倍,战斗脚本负责在真正的战斗里消费掉它。若把消费放进计算器,就会因计算器被反复调用(临界、显示、图鉴等)而提前把 flag 用掉,只有放进「每场战斗只跑一次」的脚本里才可靠。
状态里用到的临时加减或百分比,优先用修饰器实现,概念与 `save` 参数见[勇士属性](./hero-attribute.md)。