docs(10-11): 按 D-50 增加通用机制模板(战斗组)

This commit is contained in:
unanmed 2026-10-19 18:25:55 +08:00
parent 486784fdcf
commit a50210aec2
9 changed files with 553 additions and 125 deletions

View File

@ -514,4 +514,31 @@
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 边界:仅改 7 篇文档 + 本记录;未改 `config.ts` 与生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释);未创建 `10-11-SUMMARY.md`,未改 `STATE.md` / `ROADMAP.md`(按本次派发要求)。
---
## 用户循环 · batch 9 记录(D-50 通用模板)
**范围:** 用户修订循环第 1 轮 batch 9(D-50)——战斗组 8 篇(`enemy-aura`、`enemy-query`、`enemy-final`、`enemy-calculator`、`enemy-map-damage`、`enemy-combat-script`、`enemy-status-hooks`、`enemy-specials`)。按 `10-CONTEXT.md` D-50:①每类可扩展机制给**机制本身的通用骨架**(类 / 转换器 / 视图 / 效果 / 钩子),用 `[需改]` 标出作者要改的部分;②模板与示例**分开**,不把某个具体效果当作模板;③示例仅作短小的填空示范。D-49(改内置优先 + caveat)与 D-48(六步实操)保留。
### 逐条处置
| 文档 | 处置 |
| --- | --- |
| `enemy-aura.md` | 在「自己写光环类」新增「模板:复制什么、改什么」:转换器骨架(`shouldConvert` / `convert`)+ 光环视图骨架(`priority` / `couldApplyBase` / `couldApplySpecial` / `range` / `getRangeParam` / `apply` / `applySpecial`),全部 `[需改]` 标注,并附「复制后要改的六处」表。「减少金币经验的光环」「链式光环」改为短示例,明确「只是填空示范,不是模板本身」。 |
| `enemy-query.md` | 新增「模板:复制什么、改什么」:常规查询骨架(`priority` / `apply` / `query()`)+ 特殊查询骨架(效果 `for(ctx)` + 修饰器 `shouldQuery` / `add` / `delete` / `modify`),附注册写法与需改点表;抱团 / 同化 / 号角三个示例改为短小的填空示范。 |
| `enemy-final.md` | 在「自己写最终效果类」新增「模板」:`IEnemyFinalEffect` 骨架(`priority` / `apply`)+ 注册写法;「仿攻」改为独立短示例。 |
| `enemy-calculator.md` | 在「自己写计算器类」新增「模板」:`IDamageCalculator` 骨架(`calculate` / `getCriticalLimit`)+ `useCalculator` 注册;「等差数列」改为独立短示例;保留「计算器必须无副作用」warning,并在模板后再次强调。 |
| `enemy-map-damage.md` | 在「自己写地图伤害类」新增「模板」:伤害视图骨架(`getRange` / `getRangeParam` / `getDamageAt` / `getDamageWithoutCheck`)+ 转换器骨架(`convert`)+ `useConverter` 注册;「八方向激光」改为独立短示例。 |
| `enemy-combat-script.md` | 在「自己写脚本类」新增「模板」:`ICombatScript` 骨架(`priority` / `before` / `after`)+ `addCombatScript` 注册;战前拦下「无敌」与战后回血改为独立短示例。 |
| `enemy-status-hooks.md` | 在「自己写钩子类」新增「模板」:钩子类骨架(构造保存 `state` + 钩子方法)+ 挂载点注册;「蓄力(下次战斗伤害乘二)」改为独立短示例。 |
| `enemy-specials.md` | 在「改内置」新增「模板」:`special.ts` 风格的无参 / 有参两种属性定义骨架 + `[需改]`,并提示按「编辑器识别约定」补齐注释与 `ISpecial{code}Value`;「灼烧」改为独立短示例。内置属性编号表与系统讲解保留。 |
### 校验
- 源码核对:`src/data/enemy/aura.ts`(`IAuraConverter.shouldConvert` / `convert`、`IEnemyAuraView` 的 `priority` / `couldApplyBase` / `couldApplySpecial` / `range` / `getRangeParam` / `apply` / `applySpecial`)、`packages-user/data-system/src/combat/types.ts`(`IEnemyCommonQueryEffect` / `IEnemySpecialQueryEffect` / `IEnemySpecialQueryModifier` / `IEnemyFinalEffect` / `IMapDamageView` / `IMapDamageConverter` / `IDamageCalculator` / `ICombatScript` / `ICombatFlowHooks`)、`src/data/enemy/final.ts`、`src/data/enemy/calculator.ts`、`src/data/enemy/mapDamage.ts`、`src/data/enemy/script.ts`、`src/data/enemy/hook.ts`、`src/data/enemy/special.ts`。
- 结构门禁:8 篇均 CRLF、单 H1(`STRUCT-OK`)。
- 格式门禁:`pnpm exec prettier --write`(D-45,`endOfLine: crlf`)已跑,8 篇全部 `unchanged` 或已格式化。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 边界:仅改 `docs/intermediate/enemy-*.md` 与本记录;未改 `config.ts` 与生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释);未创建 `10-11-SUMMARY.md`,未改 `STATE.md` / `ROADMAP.md`(按本次派发要求)。

View File

@ -179,13 +179,103 @@ apply(
**如果你不怎么懂代码,建议直接改内置光环类(`src/data/enemy/aura.ts` 的 `CommonAura` / `GuardAura` / `SpecialAura`),而不是自己写新的类。** 上面「改内置」一节已经覆盖了绝大多数需求:调数值、调范围、调优先级、给目标加属性,都能在现有方法里完成。
自己写的做法是「转换器 + 视图」两个类:转换器只有 `shouldConvert` 与 `convert` 两个方法,负责认领特殊属性并构造视图;视图负责范围、优先级与 `apply` / `applySpecial`。下面两个示例用 `[模板]` 标出可原样照抄的行(多为接口签名、`shouldConvert`、范围对象的构建),用 `[需改]` 标出必须按需求改写的行(code、范围、优先级,以及 `apply` / `applySpecial` 里做的事)。
自己写的做法是「转换器 + 视图」两个类:转换器只有 `shouldConvert` 与 `convert` 两个方法,负责认领特殊属性并构造视图;视图负责范围、优先级与 `apply` / `applySpecial`。下面先给这两个类的**通用模板**,再用两个具体效果作示例。
两个示例都新建了自己的 code(30、31),不修改内置的 23 / 24 / 26。如果需求只是「让已有光环多做一点事」,用上一节的改内置写法更省事。
### 模板:复制什么、改什么
这是光环机制本身的骨架,不含任何具体效果。把它复制到 `src/data/enemy/` 下新建的文件里,再按 `[需改]` 改成你的效果。
先写转换器——它只负责「认领 code」和「造出视图」:
```ts
// [需改] 类名:换成你的转换器名
export class XxxAuraConverter implements IAuraConverter<IEnemyAttr, IHeroAttr> {
// [模板] 签名固定:引擎拿每条特殊属性挨个问「这条你管吗」
shouldConvert(
special: ISpecial<any>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): boolean {
// [需改] 认领条件:这个转换器负责哪个 code
return special.code === 30;
}
// [模板] 签名固定:把认领到的特殊属性翻译成视图
convert(
special: ISpecial<ISpecial30Value>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
context: IEnemyContext<IEnemyAttr, IHeroAttr>
): XxxAura {
// [需改] 构造视图;需要读上下文时把 context 也传进去
return new XxxAura(handler.enemy, special, handler.locator);
}
}
```
再写视图——它负责「优先级、范围、改什么」:
```ts
// [需改] 类名:换成你的视图名
export class XxxAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam | IManhattanRangeParam | void,
ISpecial30Value
> {
// [需改] 优先级:越大越先执行(内置是支援 5 / 普通 10 / 特殊 20)
readonly priority = 10;
// [需改] 改基础属性就把 couldApplyBase 设为 true;改特殊属性则设 couldApplySpecial
readonly couldApplyBase = true;
readonly couldApplySpecial = false;
// [需改] 范围对象:FullRange / RectRange / ManhattanRange / RayRange 之一
readonly range: IRange<IRectRangeParam | IManhattanRangeParam | void>;
// [模板] 构造函数保存来源怪物、特殊属性与位置
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<ISpecial30Value>,
readonly locator: ITileLocator
) {
// [需改] 按需求选一个范围对象
this.range = new FullRange();
}
// [模板] 签名固定:返回与 range 配套的范围参数
getRangeParam(): IRectRangeParam | IManhattanRangeParam | void {
// [需改] FullRange 返回 undefined;矩形返回 { x, y, w, h };
// 曼哈顿返回 { cx, cy, radius };射线返回 { cx, cy, dir }
return undefined;
}
// [模板] 签名固定:baseEnemy 是未修改的原始怪物
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
baseEnemy: IReadonlyEnemy<IEnemyAttr>
): void {
// [需改] 在这里改 handler.enemy 的基础属性
}
// [模板] 签名固定:不改特殊属性就返回 null
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return null;
}
}
```
复制后只需要改这几处:
| 位置 | 改成什么 |
| --------------------------------------- | ------------------------ |
| `shouldConvert` 里的 `30` | 你的特殊属性 code |
| 两个类名 `XxxAuraConverter` / `XxxAura` | 你的名字 |
| `priority` | 你的执行顺序 |
| `couldApplyBase` / `couldApplySpecial` | 改基础属性还是改特殊属性 |
| `range` 与 `getRangeParam` | 你的范围形态 |
| `apply` / `applySpecial` 的方法体 | 你的效果 |
`ISpecial30Value` 只是占位:有参数就换成你在 `special.ts` 里定义的 `ISpecial{code}Value`,无参数就换成 `void`。注册见文末「注册在哪里」。
### 示例:基础光环(减少周围怪物的金币与经验)
先从一个只改基础属性的例子开始:让范围内的怪物金币与经验下降。它用 code 30(示例自选的未占用号)定义一条带参数的特殊属性,再配一个转换器与视图。
这是一个只改基础属性的短示例:范围内的怪物金币与经验下降。它用 code 30(示例自选的未占用号)定义一条带参数的特殊属性,再按上面的模板配一个转换器与视图。
**目标**:站在带「削弱光环」怪物周围的怪物,金币与经验按参数下降。
@ -194,85 +284,71 @@ apply(
**复制什么**:先在 `special.ts` 登记属性——
```ts
// [需改] 在 src/data/enemy/special.ts 里新增这条属性定义(编号换成你没用过的)
/** 30 - 削弱光环 */
export interface ISpecial30Value {
range: number; // [需改] 光环范围值
square: boolean; // [需改] true 矩形 / false 十字
moneyBuff: number; // [需改] 金币变化百分比(负数表示下降)
expBuff: number; // [需改] 经验变化百分比
range: number;
square: boolean;
moneyBuff: number;
expBuff: number;
}
// 30 - 削弱光环
map.set(
30, // [需改] 编号:示例自选的未占用号
30,
defineCommonSerializableSpecial<ISpecial30Value>(
30,
{ range: 1, square: false, moneyBuff: -20, expBuff: -20 }, // [需改] 默认参数
{ range: 1, square: false, moneyBuff: -20, expBuff: -20 },
{
name: '削弱光环', // [需改] 显示名称
name: '削弱光环',
desc: special =>
`周围怪物金币与经验下降${Math.abs(special.value.moneyBuff)}%。`, // [需改] 悬停说明
color: '#9AC' // [需改] 名称颜色
`周围怪物金币与经验下降${Math.abs(special.value.moneyBuff)}%。`,
color: '#9AC'
}
)
);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写转换器与视图——
再按模板填两个类。与模板不同的只有 code、范围选择与 `apply`:
```ts
// [模板] 两个范围对象各建一次复用
// [模板] 两个范围对象在模块顶部各建一次
const MANHATTAN_RANGE = new ManhattanRange();
const RECT_RANGE = new RectRange();
// [需改] 类名:换成你的转换器名
export class WeakAuraConverter implements IAuraConverter<
IEnemyAttr,
IHeroAttr
> {
// [模板] shouldConvert:认领本转换器负责的 code,通常只需换掉这个判断
shouldConvert(special: ISpecial<any>): boolean {
return special.code === 30; // [需改] code
}
// [模板] convert 签名固定:把认领到的特殊属性翻译成视图
convert(
special: ISpecial<ISpecial30Value>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): WeakAura {
// [需改] 构造视图,并决定把哪些信息传给它(这里传了来源怪物、属性、位置)
return new WeakAura(handler.enemy, special, handler.locator);
}
}
// [需改] 类名:换成你的视图名
export class WeakAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam | IManhattanRangeParam,
ISpecial30Value
> {
// [需改] priority:越大越先执行;高优先级的结果能被低优先级看到
readonly priority = 8;
// [需改] 本光环改基础属性(走 apply)还是改特殊属性(走 applySpecial)
readonly priority = 8; // [需改] 优先级
readonly couldApplyBase = true;
readonly couldApplySpecial = false;
// [模板] 范围对象在构造函数里挑选,getRangeParam 负责给参数
readonly range: IRange<IRectRangeParam | IManhattanRangeParam>;
// [模板] 构造函数把来源怪物、特殊属性、位置存下来
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<ISpecial30Value>,
readonly locator: ITileLocator
) {
// [模板] 按参数里的 square 选矩形或曼哈顿范围
this.range = special.value.square ? RECT_RANGE : MANHATTAN_RANGE;
}
// [模板] 生成范围参数:矩形用左上角与宽高,曼哈顿用中心点与半径
getRangeParam(): IRectRangeParam | IManhattanRangeParam {
const { range, square } = this.special.value;
if (square) {
@ -283,55 +359,44 @@ export class WeakAura implements IEnemyAuraView<
h: range * 2 + 1
};
}
return {
cx: this.locator.x,
cy: this.locator.y,
radius: range
};
return { cx: this.locator.x, cy: this.locator.y, radius: range };
}
// [需改] apply:真正修改目标基础属性;baseEnemy 是未修改的原始怪物
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
baseEnemy: IReadonlyEnemy<IEnemyAttr>
): void {
// [需改] 从参数里取出要用的字段
// [需改] 用 baseEnemy 的原始值算百分比,避免多个光环互相叠乘
const { moneyBuff, expBuff } = this.special.value;
// [模板] handler.enemy 是被光环命中的目标怪物
const enemy = handler.enemy;
if (moneyBuff !== 0) {
// [需改] 以 baseEnemy 的原始值算百分比,避免多个光环互相叠乘
const money = (baseEnemy.getAttribute('money') * moneyBuff) / 100;
enemy.addAttribute('money', Math.floor(money));
}
if (expBuff !== 0) {
const exp = (baseEnemy.getAttribute('exp') * expBuff) / 100;
enemy.addAttribute('exp', Math.floor(exp));
}
const money = (baseEnemy.getAttribute('money') * moneyBuff) / 100;
const exp = (baseEnemy.getAttribute('exp') * expBuff) / 100;
handler.enemy.addAttribute('money', Math.floor(money));
handler.enemy.addAttribute('exp', Math.floor(exp));
}
// [模板] 本光环不改特殊属性,返回 null
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return null;
}
}
```
最后把注册行加进 `initializeEnemy`(`src/data/enemy/initialize.ts`,见文末「注册在哪里」):
最后注册:
```ts
state.enemyContext.registerAuraConverter(new WeakAuraConverter());
```
**改什么**:编号(`30`)、类名、`priority`、范围参数与 `apply` 里改的字段(本示例 `moneyBuff` / `expBuff`)换成你自己的。
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:`30`、类名、`priority`、`apply` 里改的属性。
**怎么验证**:把带该属性的怪物与另一只怪物放在范围内,看后者的金币 / 经验是否按参数下降。
**注意**:`apply` 里用 `baseEnemy` 的原始值做百分比,因此多个光环不会把彼此的结果继续乘进去。只改基础属性就实现 `apply`;要改目标的特殊属性才需要 `applySpecial`。
**注意**:`apply` 里用 `baseEnemy` 的原始值做百分比;只改基础属性就实现 `apply`,要改目标的特殊属性才需要 `applySpecial`。
### 示例:特殊光环(给周围怪物上光环)
如果光环要给目标「加一条特殊属性」,而不是直接改基础属性,就把逻辑放进 `applySpecial`。下面的例子给范围内没有普通光环的怪物加上 code 23 的普通光环。
如果光环要给目标「加一条特殊属性」,而不是直接改基础属性,就把逻辑放进 `applySpecial`:`couldApplySpecial` 设为 `true`,并返回一个实现 `IEnemySpecialModifier` 的对象,它有三个方法——`add`(返回要添加的属性)、`delete`(返回要删除的属性)、`modify`(改动已有属性并返回是否真的改了)。下面的例子给范围内没有普通光环的怪物加上 code 23 的普通光环。
**目标**:站在带「传染光环」怪物附近的怪物,若自己没有普通光环,就获得一层 code 23 的普通光环。
@ -340,36 +405,29 @@ state.enemyContext.registerAuraConverter(new WeakAuraConverter());
**复制什么**:先在 `special.ts` 登记属性——
```ts
// [需改] 这条无参属性定义同样加进 src/data/enemy/special.ts
// special.ts:31 - 传染光环
map.set(
31, // [需改] 编号
31,
defineNonePropertySpecial(31, {
name: '传染光环', // [需改] 显示名称
desc: '同楼层没有光环的怪物获得普通光环。', // [需改] 悬停说明
color: '#9F9' // [需改] 名称颜色
name: '传染光环',
desc: '同楼层没有光环的怪物获得普通光环。',
color: '#9F9'
})
);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
再写转换器与视图——
再按模板填两个类,这次 `applySpecial` 返回一个修饰器:
```ts
// [需改] 类名:换成你的修饰器名;它负责「给目标增删改特殊属性」
export class ChainAuraModifier implements IEnemySpecialModifier<IEnemyAttr> {
// [模板] 构造函数存下来源属性
constructor(readonly special: ISpecial<void>) {}
// [需改] add:返回要添加给目标的特殊属性数组
add(handler: IReadonlyEnemyHandler<IEnemyAttr, any>): ISpecial<any>[] {
// [需改] 已经有普通光环(code 23)就不再添加
if (handler.enemy.hasSpecial(23)) return [];
// [模板] 从怪物管理器取一条新的属性实例
const sp = handler.state.enemyManager.getSpecial(23)?.();
if (!sp) return [];
// [需改] 设置这条属性的参数(这里是普通光环的字段)
// [需改] 设置新属性的参数
sp.setValue({
range: 1,
square: false,
@ -377,74 +435,59 @@ export class ChainAuraModifier implements IEnemySpecialModifier<IEnemyAttr> {
atkBuff: 0,
defBuff: 0
});
// [模板] 返回要添加的属性
return [sp];
}
// [模板] 本修饰器不删除属性,返回空数组
delete(): ISpecial<any>[] {
return [];
}
// [模板] 本修饰器不修改已有属性,返回 false
modify(): boolean {
return false;
}
}
// [需改] 类名:换成你的转换器名
export class ChainAuraConverter implements IAuraConverter<
IEnemyAttr,
IHeroAttr
> {
// [模板] shouldConvert:认领 code,通常只换这个判断
shouldConvert(special: ISpecial<any>): boolean {
return special.code === 31; // [需改] code
}
// [模板] convert 签名固定
convert(
special: ISpecial<void>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): ChainAura {
// [需改] 构造视图
return new ChainAura(handler.enemy, special, handler.locator);
}
}
// [需改] 类名:换成你的视图名
export class ChainAura implements IEnemyAuraView<IEnemyAttr, void, void> {
// [需改] priority = 25 高于内置特殊光环的 20,故先执行
readonly priority = 25;
// [需改] 本光环只改特殊属性:couldApplyBase 为 false,couldApplySpecial 为 true
readonly priority = 25; // [需改] 高于内置特殊光环的 20,故先执行
readonly couldApplyBase = false;
readonly couldApplySpecial = true;
// [需改] 范围:这里用 FullRange 作用于全场
readonly range = new FullRange();
// [模板] 构造函数保存来源信息
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<void>,
readonly locator: ITileLocator
) {}
// [模板] 范围参数:FullRange 不需要参数
getRangeParam(): void {
return undefined;
}
// [模板] 本光环不改基础属性,apply 留空
apply(): void {}
// [需改] applySpecial:返回修饰器,由上下文按优先级应用到目标
applySpecial(): IEnemySpecialModifier<IEnemyAttr> | null {
return new ChainAuraModifier(this.special);
}
}
```
**改什么**:编号(`31`)、类名、`priority`、范围与 `add` 里设置的参数(本示例的 `range` / `hpBuff` 等光环字段)换成你自己的。
**改什么**:`31`、类名、`priority`、范围与 `add` 里设置的参数。
**怎么验证**:把带该属性的怪物与一只没有普通光环的怪物放在范围内,看后者是否被加上了普通光环(属性提升)。

View File

@ -117,12 +117,47 @@ calculate(
**如果你不怎么懂代码,建议直接改内置的 `MainDamageCalculator.calculate`(在 `src/data/enemy/calculator.ts` 里加分支),而不是自己写新的计算器。** 上面「改内置」一节已经覆盖了绝大多数需求;自写计算器意味着你要自己重新实现无敌、吸血、支援、连击、先攻、破甲、反击、净化、固伤、仇恨、自爆这些分支。
做法是实现 `IDamageCalculator` 接口,再用 `useCalculator` 替换:
做法是实现 `IDamageCalculator` 接口,再用 `useCalculator` 替换。先看接口模板,再看一个具体示例。
- **接口**:`calculate` 返回 `{ damage, turn }`,`getCriticalLimit` 返回临界上界;
- **替换**:`useCalculator` 是**替换**当前计算器,选了自己的就会把内置的整个换掉。
### 模板:复制什么、改什么
下面用「等差数列」演示自写类的最小形态(只保留本示例所需分支):
下面是计算器机制本身的骨架,不含任何具体公式。复制到新文件后按 `[需改]` 修改:
```ts
// [需改] 类名:换成你自己的计算器名
export class XxxCalculator implements IDamageCalculator<IEnemyAttr, IHeroAttr> {
// [模板] calculate 签名固定:读 handler 里的 enemy 与 hero,返回 { damage, turn }
calculate(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): IEnemyDamageInfoBase {
// [模板] 取出只读的怪物与勇士对象(只读,供读取,不可改写)
const { enemy, hero } = handler;
// [需改] 在这里写你的公式;只读输入,不改任何外部状态
return { damage: 0, turn: 0 };
}
// [模板] getCriticalLimit 签名固定:返回临界计算的上界
getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): number {
// [需改] 需要自定义临界上界时改这里;否则返回勇士当前最终值即可
return handler.hero.getFinalAttribute(attribute);
}
}
```
注册(**替换**当前计算器):
```ts
state.enemyContext.getDamageSystem()?.useCalculator(new XxxCalculator());
```
复制后需要改的只有:类名、`calculate` 里的公式、必要时 `getCriticalLimit` 的返回值。**计算器必须无副作用**(见上方 warning)——模板里的 `calculate` 只读输入,不要写 flag、不要改 `enemy` / `hero`。
### 示例:等差数列
下面用「等差数列」演示自写类的最小形态(只保留本示例所需分支)。它只是填空示范,不是模板本身:
```ts
// [需改] 类名:换成你自己的计算器名

View File

@ -139,7 +139,44 @@ async after(
**如果你不怎么懂代码,建议直接改内置的 `MainCombatScript`(在 `src/data/enemy/script.ts` 的 `before` / `after` 里加逻辑),而不是自己写新的脚本类。** 上面「改内置」一节已经覆盖了拦截与结算两类需求;只有确实需要独立优先级时才用下面的写法。
做法是实现 `ICombatScript` 接口,再用 `addCombatScript` 追加。`addCombatScript` 是**追加**而不是替换:内置脚本仍会执行,新脚本按 `priority` 与它一起排序;但同优先级的脚本不能重复注册。
做法是实现 `ICombatScript` 接口,再用 `addCombatScript` 追加。`addCombatScript` 是**追加**而不是替换:内置脚本仍会执行,新脚本按 `priority` 与它一起排序;但同优先级的脚本不能重复注册。先看接口模板,再看两个具体示例。
### 模板:复制什么、改什么
下面是战斗脚本机制本身的骨架,不含任何具体规则。复制到新文件后按 `[需改]` 修改:
```ts
// [需改] 类名:换成你的脚本名
export class XxxCombatScript implements ICombatScript<IEnemyAttr, IHeroAttr> {
// [需改] priority:越大越先执行;内置脚本是 0,自写时不要撞上 0
readonly priority = 100;
// [模板] before 签名固定:战前执行,返回 false 即放弃战斗
async before(
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
): Promise<boolean> {
// [需改] 你的战前判断;不拦截就返回 true
return true;
}
// [模板] after 签名固定:战后执行
async after(
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
): Promise<void> {
// [需改] 你的战后处理
}
}
```
注册(**追加**):
```ts
state.combat.addCombatScript(new XxxCombatScript());
```
复制后需要改的只有:类名、`priority`、`before` 里的拦截条件、`after` 里的处理。不用的方法保留空实现即可(`before` 返回 `true`、`after` 留空)。
### 示例:拥有某特殊属性时无法战斗

View File

@ -108,22 +108,54 @@ apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
**如果你不怎么懂代码,建议直接改内置的 `MainEnemyFinalEffect.apply`(在 `src/data/enemy/final.ts` 里加分支),而不是自己写新的类。** 上面「改内置」一节已经覆盖了绝大多数需求;只有确实需要独立优先级或独立类时,才用下面的写法。
做法是实现 `IEnemyFinalEffect` 接口,再用 `registerFinalEffect` 注册。它与内置效果进入同一条流水线,会按各自的 `priority` 一起执行。下面仍以「仿攻」为例,演示自写类的形态:
做法是实现 `IEnemyFinalEffect` 接口,再用 `registerFinalEffect` 注册。它与内置效果进入同一条流水线,会按各自的 `priority` 一起执行。先看接口模板,再看一个具体示例。
### 模板:复制什么、改什么
下面是最终效果机制本身的骨架,不含任何具体效果。复制到新文件后按 `[需改]` 修改:
```ts
// [需改] 类名:换成你自己的效果名
export class AttackCopyFinal implements IEnemyFinalEffect<
export class XxxFinalEffect implements IEnemyFinalEffect<
IEnemyAttr,
IHeroAttr
> {
// [需改] priority:数值越大越先执行;内置最终效果是 0
readonly priority = 0;
// [模板] apply 的签名是固定的,引擎在最终阶段调用它并传入 handler
// [模板] apply 签名固定:引擎在最终阶段调用它并传入 handler
apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
// [需改] 触发条件:只有带本效果(示例 code 32)的怪物才处理
// [需改] 触发条件:哪些怪物要执行这段收尾
if (!handler.enemy.hasSpecial(XX)) return;
// [需改] 收尾逻辑:只读 handler.enemy / handler.hero,
// 再改 handler.enemy 的基础属性
}
}
```
注册:
```ts
state.enemyContext.registerFinalEffect(new XxxFinalEffect());
```
复制后需要改的只有三处:类名、`priority`、`apply` 里「怎么判断」与「改什么属性」。
### 示例:仿攻
下面仍以「仿攻」为例,演示自写类的形态。它只是填空示范,不是模板本身:
```ts
export class AttackCopyFinal implements IEnemyFinalEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 0;
apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
// [需改] 只有带本效果(示例 code 32)的怪物才处理
if (!handler.enemy.hasSpecial(32)) return;
// [需改] 真正做的事:把怪物攻击设为勇士的最终攻击
// [需改] 把怪物攻击设为勇士的最终攻击
handler.enemy.setAttribute(
'atk',
handler.hero.getFinalAttribute('atk')

View File

@ -123,7 +123,91 @@ convert(
`useConverter` 是**替换**当前转换器:选了自己的就把内置的整个换掉,若要同时保留内置五类,需要在自己的转换器里把它们的视图也收集进去,或直接改内置转换器。
下面代码用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。
### 模板:复制什么、改什么
自己写地图伤害永远是「一个视图 + 一个转换器」两个类。下面是机制本身的骨架,不含任何具体伤害;复制到新文件后按 `[需改]` 修改。
先写视图——它负责「范围、某点的伤害与类型」:
```ts
// [需改] 类名:换成你的视图名
export class XxxDamageView implements IMapDamageView<TRangeParam> {
// [模板] 构造函数接收引擎传进来的上下文、位置与特殊属性
constructor(
private readonly context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: ITileLocator,
private readonly special: ISpecial<IXxxValue>
) {}
// [需改] 范围对象:RectRange / ManhattanRange / RayRange / FullRange 之一
getRange(): IRange<TRangeParam> {
// [需改] RANGE 换成你在模块顶部建好的范围对象
return RANGE;
}
// [需改] 与范围配套的参数(矩形 { x, y, w, h };曼哈顿 { cx, cy, radius };
// 射线 { cx, cy, dir };全场返回 undefined)
getRangeParam(): TRangeParam {
return undefined as TRangeParam;
}
// [模板] 先判范围,再取伤害;通常不需要改
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | null {
const range = this.getRange();
range.bindHost(this.context);
if (!range.inRange(locator.x, locator.y, this.getRangeParam())) {
return null;
}
return this.getDamageWithoutCheck(locator);
}
// [需改] 真正决定某点的伤害与类型;返回 null 表示该点不受伤
getDamageWithoutCheck(
locator: ITileLocator
): Readonly<IMapDamageInfo> | null {
return {
damage: 0,
type: MapDamageType.Unknown,
extra: { catch: new Set(), repulse: new Set() }
};
}
}
```
再写转换器——它负责「从怪物身上读出属性,造出视图」:
```ts
// [需改] 类名:换成你的转换器名
export class XxxConverter implements IMapDamageConverter<
IEnemyAttr,
IHeroAttr
> {
// [模板] convert 签名固定:把特殊属性翻译成一组视图
convert(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
context: IEnemyContext<IEnemyAttr, IHeroAttr>
): IMapDamageView<any>[] {
// [需改] 读取要转换的特殊属性
const special = handler.enemy.getSpecial<IXxxValue>(XX);
if (!special) return [];
// [需改] 造视图;一条属性也可以返回多个视图
return [new XxxDamageView(context, handler.locator, special)];
}
}
```
注册(**替换**当前转换器):
```ts
const mapDamage = state.enemyContext.getMapDamage();
mapDamage?.useConverter(new XxxConverter());
```
复制后需要改的只有:两个类名、范围对象与参数、`getDamageWithoutCheck` 的伤害 / 类型、转换器读取的 code。
### 示例:八方向激光
下面是一个具体效果,演示怎么把模板填满。它只是填空示范,不是模板本身。
**目标**:新增一个「八方向激光」特殊属性(示例 code 43),沿上下左右加四个斜向共八个方向发射,且不影响内置的四方向激光。

View File

@ -83,7 +83,119 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
这些内置类已经在 `initializeEnemy` 里注册好,直接改方法体即可,不需要新建类。只有「必须读取周围怪物、再修改自己」的逻辑,才需要按下面的示例自己写查询类。
下面代码用 `[模板]` 标出可原样照抄的行(接口签名、遍历骨架),用 `[需改]` 标出必须按需求改写的行(code、优先级、统计与修改逻辑)。
## 模板:复制什么、改什么
查询分两种,模板也分两种。下面是机制本身的骨架,不含任何具体效果;复制到 `src/data/enemy/` 下的新文件后,按 `[需改]` 改成你的查询。
### 常规查询模板
```ts
// [需改] 类名
export class XxxCommonQueryEffect implements IEnemyCommonQueryEffect<
IEnemyAttr,
IHeroAttr
> {
// [需改] 优先级:与光环共用,越大越先执行
readonly priority = 10;
// [模板] apply 签名固定
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>,
query: () => IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): void {
// [模板] 必须用 query() 取上下文,不能用 handler.context 代替
const ctx = query();
// [需改] 用 ctx 读外部状态(scanRange / iterateEnemy / indexer),
// 再改 handler.enemy 的基础属性
}
}
```
注册时把效果绑定到一个 code 上:
```ts
// [需改] code 换成触发这条查询的特殊属性编号
state.enemyContext.registerCommonQueryEffect(40, new XxxCommonQueryEffect());
```
### 特殊查询模板
特殊查询由「效果 + 修饰器」两个类组成:效果只负责造修饰器,修饰器负责真正增删改目标的特殊属性。
```ts
// [需改] 类名:修饰器
class XxxQueryModifier implements IEnemySpecialQueryModifier<
IEnemyAttr,
IHeroAttr
> {
// [模板] 保存只读查询上下文
constructor(
private readonly ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
) {}
// [需改] 这次查询作用于哪些怪物
shouldQuery(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): boolean {
return handler.enemy.hasSpecial(41);
}
// [需改] 要添加的特殊属性
add(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): ISpecial<any>[] {
return [];
}
// [需改] 要删除的特殊属性
delete(): ISpecial<any>[] {
return [];
}
// [需改] 要改写的特殊属性;真正改了才返回 true
modify(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>
): boolean {
return false;
}
}
// [需改] 类名:效果本身
export class XxxSpecialQueryEffect implements IEnemySpecialQueryEffect<
IEnemyAttr,
IHeroAttr
> {
// [需改] 优先级
readonly priority = 15;
// [模板] for 签名固定:返回修饰器
for(
ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): IEnemySpecialQueryModifier<IEnemyAttr, IHeroAttr> {
return new XxxQueryModifier(ctx);
}
}
```
注册时特殊查询不绑定 code,只传效果实例:
```ts
state.enemyContext.registerSpecialQueryEffect(new XxxSpecialQueryEffect());
```
复制后只需要改这几处:
| 位置 | 改成什么 |
| ---------------------------------------- | ------------------------ |
| `priority` | 你的执行顺序 |
| 常规查询的 `apply` 方法体与注册时的 code | 你的读法、改法与触发属性 |
| 特殊查询的 `shouldQuery` | 这次查询作用于哪些怪物 |
| 特殊查询的 `add` / `delete` / `modify` | 你要增删改哪些特殊属性 |
| 类名 `Xxx...` | 你的名字 |
下面三个示例都只是「填空示范」,不是模板本身。
## 示例:抱团
@ -96,49 +208,42 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
**复制什么**:
```ts
// [需改] 类名:这是一个常规查询效果
export class GroupQueryEffect implements IEnemyCommonQueryEffect<
IEnemyAttr,
IHeroAttr
> {
// [需改] priority:与光环共用一套优先级,越大越先执行
readonly priority = 10;
readonly priority = 10; // [需改] 优先级
// [模板] apply 签名固定:handler 是当前怪物,special 是触发查询的属性,query() 取上下文
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>,
query: () => IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): void {
// [需改] 读出本属性登记的参数(示例:半径与每只同类提供的攻击加成)
// [需改] 读出本属性登记的参数
const { radius, atk } = special.value as {
radius: number;
atk: number;
};
// [模板] 必须用 query() 取上下文(直接读 handler.context 不会被登记为「查询过」,见上文)
const ctx = query();
// [模板] 范围对象建一次即可
const range = new ManhattanRange();
let count = 0;
// [模板] 扫出范围内的怪物,逐项拿到 [位置, 视图]
for (const [locator, view] of ctx.scanRange(range, {
cx: handler.locator.x,
cy: handler.locator.y,
radius
})) {
// [需改] 跳过自己:只统计周围的其他怪物
// [需改] 跳过自己,统计拥有同种特殊属性的其他怪物
if (
locator.x === handler.locator.x &&
locator.y === handler.locator.y
) {
continue;
}
// [需改] 统计拥有同种特殊属性的怪物
if (view.getComputedEnemy().hasSpecial(special.code)) {
count++;
}
}
// [需改] 修改自身:每有一个同类,攻击加 atk 点
// [需改] 每有一个同类,攻击加 atk 点
handler.enemy.addAttribute('atk', count * atk);
}
}
@ -168,29 +273,26 @@ state.enemyContext.registerCommonQueryEffect(40, new GroupQueryEffect());
**复制什么**:
```ts
// [需改] 类名:特殊查询的修饰器,负责实际改动目标怪物的特殊属性
class AssimilationModifier implements IEnemySpecialQueryModifier<
IEnemyAttr,
IHeroAttr
> {
// [模板] 构造时保存只读查询上下文
constructor(
private readonly ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
) {}
// [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物)
// [需改] 这次查询作用于哪些怪物
shouldQuery(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): boolean {
return handler.enemy.hasSpecial(41);
}
// [需改] add:返回要添加给怪物的特殊属性
// [需改] 把别人有、自己没有的属性照抄过来(跳过同化属性自身)
add(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): ISpecial<any>[] {
const result: ISpecial<any>[] = [];
// [模板] 取全场其他怪物,并按位置索引从小到大排序
const others = [...this.ctx.iterateEnemy()]
.filter(([, view]) => view.getComputedEnemy() !== handler.enemy)
.sort(
@ -198,7 +300,6 @@ class AssimilationModifier implements IEnemySpecialQueryModifier<
this.ctx.indexer.index(a.x, a.y) -
this.ctx.indexer.index(b.x, b.y)
);
// [需改] 把别人有、自己没有的属性照抄过来(跳过同化属性自身)
for (const [, view] of others) {
for (const special of view.getComputedEnemy().iterateSpecials()) {
if (special.code === 41) continue;
@ -209,21 +310,18 @@ class AssimilationModifier implements IEnemySpecialQueryModifier<
return result;
}
// [模板] 本效果不删除属性,返回空数组
delete(): ISpecial<any>[] {
return [];
}
// [需改] modify:自己已有、需要叠加数值的属性在这里处理
// [需改] 自己已有、需要叠加数值的属性在这里处理
modify(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>
): boolean {
// [模板] 只处理数值型属性
if (typeof special.value !== 'number') return false;
let sum = special.value;
let found = false;
// [模板] 遍历全场,把同 code 的数值加算进来
for (const [, view] of this.ctx.iterateEnemy()) {
const other = view
.getComputedEnemy()
@ -233,20 +331,17 @@ class AssimilationModifier implements IEnemySpecialQueryModifier<
found = true;
}
if (!found) return false;
// [模板] 写回并返回 true,表示确实改动了
special.setValue(sum);
return true;
}
}
// [需改] 类名:特殊查询效果
export class AssimilationQueryEffect implements IEnemySpecialQueryEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 15; // [需改] 优先级
// [模板] for(ctx) 签名固定:返回一个修饰器;ctx 是只读查询上下文
for(
ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): IEnemySpecialQueryModifier<IEnemyAttr, IHeroAttr> {
@ -277,27 +372,24 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
**复制什么**:
```ts
// [需改] 类名:同样是常规查询效果
export class HornQueryEffect implements IEnemyCommonQueryEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 5; // [需改] 优先级
// [模板] apply 签名固定
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>,
query: () => IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): void {
// [模板] 必须用 query() 取上下文
const ctx = query();
let count = 0;
// [模板] 遍历全场怪物逐一计数
// [需改] 遍历全场怪物逐一计数
for (const _ of ctx.iterateEnemy()) {
count++;
}
// [需改] 修改自身:每有一只怪物,攻击加 special.value 点
// [需改] 每有一只怪物,攻击加 special.value 点
handler.enemy.addAttribute('atk', count * (special.value as number));
}
}

View File

@ -150,7 +150,55 @@ map.set(
本篇的主线是**改内置**。特殊属性没有「自己写一个类」的做法:这类数据本就登记在内置文件 `src/data/enemy/special.ts` 里,新增一条属性的方式就是**往这个文件里加一段 `map.set(...)`**(即下面六步)。真正让它生效的代码,则按类型写在各内置实现文件里(见下方「属性实现位置」表)——这也是本篇与光环 / 查询那两篇最大的不同:那两边需要新建类,这里只需要改内置文件。
需要新机制时,先看它能不能归入已有的分类(调参、坚固 / 模仿、地图伤害、光环、状态钩子)。能归入,就照下面六步加一条属性,再在对应的内置实现里加一个分支;这是绝大多数需求的做法。以「每回合给勇士造成固定伤害」为例(假设使用 code 50):
需要新机制时,先看它能不能归入已有的分类(调参、坚固 / 模仿、地图伤害、光环、状态钩子)。能归入,就照下面六步加一条属性,再在对应的内置实现里加一个分支;这是绝大多数需求的做法。
### 模板:复制什么、改什么
在 `special.ts` 里加属性只有两种写法。按你的属性需不需要参数选一种,复制后按 `[需改]` 修改。
无参数(纯机制,`value` 为 `void`):
```ts
// {code} - {name}
map.set(
XX, // [需改] 编号
defineNonePropertySpecial(XX, {
name: '{名称}', // [需改] 显示名称
desc: '{说明}', // [需改] 悬停说明
color: '#XXXXXX' // [需改] 名称颜色
})
);
```
有参数(参数会进入存档):
```ts
/** {code} - {name} */
export interface ISpecialXXValue {
value: number; // [需改] 你的参数字段
}
// {code} - {name}
map.set(
XX, // [需改] 编号
defineCommonSerializableSpecial<ISpecialXXValue>(
XX,
{ value: 0 }, // [需改] 默认参数
{
name: '{名称}', // [需改] 显示名称
// [需改] 悬停说明;需要按参数变化时写成函数
desc: special => `...${special.value.value}...`,
color: '#XXXXXX' // [需改] 名称颜色
}
)
);
```
复制后按「编辑器识别约定」补齐注释与接口命名:`// {code} - {name}`、`/** {code} - {name} */`、`ISpecial{code}Value`。真正的生效逻辑不在这个文件里,按下方「属性实现位置」表去找落点。
### 示例:灼烧(每回合固定伤害)
以「每回合给勇士造成固定伤害」为例(假设使用 code 50):
**目标**:新增一条「每回合对勇士造成固定伤害」的特殊属性(示例 code 50)。

View File

@ -174,7 +174,37 @@ if (double) {
- 战斗:`state.combat.addHook(new ...Hooks(state))`,实现 `ICombatFlowHooks`;
- 切层:`state.hero.location.addHook(new ...Hooks(state))`,实现 `IHeroLocationHooks`。
下面仍以「蓄力」为例,演示自写类的形态:一个钩子负责挂状态,一个脚本负责消费(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。
### 模板:复制什么、改什么
下面是状态钩子机制本身的骨架(以战斗钩子为例),不含任何具体状态。复制到新文件后按 `[需改]` 修改:
```ts
// [需改] 类名与接口:按上表换成你要挂的钩子接口
export class XxxHooks implements ICombatFlowHooks<IEnemyAttr, IHeroAttr> {
// [模板] 构造时保存数据端状态,供读写 flag、取勇士属性等
constructor(readonly state: ICoreState) {}
// [需改] 方法名:按上表换成 onStepEnd / onAfterTeleportTo / onAfterCombat / onSetFloor
async onAfterCombat(
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>
): Promise<void> {
// [需改] 你的状态逻辑:读状态、改 flag、挂修饰器等
}
}
```
注册到对应挂载点:
```ts
// [需改] 挂载点与方法签名要匹配(见上表)
state.combat.addHook(new XxxHooks(state));
```
复制后需要改的只有:类名、实现的接口、钩子方法名与内部逻辑、挂载点。一个状态要多个钩子时,可以写在同一个类里(实现多个接口),也可以拆成几个类分别挂载。
### 示例:蓄力(下次战斗伤害乘二)
下面仍以「蓄力」为例,演示自写类的形态。它只是填空示范,不是模板本身:一个钩子负责挂状态,一个脚本负责消费(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。
```ts
// [需改] 类名