docs(10-04): write query, final, and calculator docs

- document query effects as new, not built into the engine
- add final effect comparison and attack-copy example
- explain damage formula, critical conventions, and arithmetic example
This commit is contained in:
unanmed 2026-10-17 18:44:16 +08:00
parent 5528edc70d
commit e13ebf71ed
3 changed files with 368 additions and 0 deletions

View File

@ -0,0 +1,113 @@
# 伤害计算
伤害计算器决定一次战斗的伤害与回合数。它实现的是 `f(enemy, hero)`:输入计算后的怪物与勇士属性,输出 `{ damage, turn }`。本篇导读内置公式,并说明要改公式时应该动哪里。整体流程见[战斗总览](./combat-overview.md)。
## 接口
`IDamageCalculator` 有两个方法:
- `calculate(handler)`:返回 `IEnemyDamageInfoBase`,即 `{ damage, turn }`;
- `getCriticalLimit(handler, attribute)`:返回指定勇士属性进行临界计算时的上界。
`handler` 是 `IReadonlyDamageHandler`,包含:
- `enemy`:计算后的怪物对象;
- `hero`:勇士属性对象;
- `context`:怪物上下文;
- `locator`:怪物位置;
- `state`:数据端状态对象。
注册方式:
```ts
state.enemyContext.getDamageSystem()?.useCalculator(new MainDamageCalculator());
```
`initializeEnemy` 在创建 `DamageSystem` 后就调用了 `useCalculator(new MainDamageCalculator())`,因此默认使用内置公式。
## 内置公式逐段导读
`MainDamageCalculator.calculate`(`src/data/enemy/calculator.ts`)按以下顺序计算:
1. **读取属性**:从 `hero` 读 `hp`、`atk`、`def`、`mdef`,从 `enemy` 读 `atk`、`def`、`hp`。
2. **无敌分支**:满足条件时直接返回 `{ damage: Infinity, turn: 0 }`,表示无法击败。
3. **勇士每回合伤害**:`atk - monDef`。如果小于等于 0,同样返回 `Infinity`。
4. **吸血**:按勇士生命值比例增加伤害,必要时把伤害加到怪物生命上。
5. **怪物每回合伤害**:`monAtk - def`;魔攻时忽略勇士防御;连击则乘以攻击次数。
6. **回合数**:`turn = ceil(monHp / heroPerDamage)`。
7. **支援**:怪物带有支援者时,递归计算每个支援者的伤害与回合数,并把它们累加到当前怪物上。
8. **先攻、破甲、反击、净化**:按各自规则增加伤害或每回合伤害。
9. **总伤害**:`damage += (turn - 1) * enemyPerDamage`,再减去魔防;未开启负伤时,负伤害归零。
10. **固伤、仇恨、自爆**:固伤与仇恨不受魔防影响;自爆把伤害抬到「勇士生命 - 1」。
11. **返回**:`damage` 向下取整,连同 `turn` 一起返回。
「加自己的属性加在哪一步」取决于你想改什么:
- 只是让怪物某项属性更高:不要改计算器,放进光环、查询或最终效果,让 `enemy.getAttribute(...)` 读到新值即可;
- 要让属性按回合数累积、按条件改变倍率、或增加新的伤害分支:在 `calculate` 的对应段落里改公式;
- 只是给勇士一侧加临时数值:用[勇士属性](./hero-attribute.md)里的修饰器,计算器会自动读到最终属性。
## 支援怪
支援怪部分的递归计算较绕:计算当前怪物时,会把每个支援者的回合数与伤害也加进来。初次阅读可以暂时跳过这一段,不影响自定义普通怪物的公式;等需要处理支援组合时再回来看。
## 示例:等差数列属性
「等差数列属性」指怪物攻击每回合按固定值递增。属性本身在一次计算中是常量,所以不能靠给 `atk` 加值实现;要在 `calculate` 里把「每回合伤害」按等差数列求和:
```ts
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
CriticalableHeroStatus,
IDamageCalculator,
IEnemyDamageInfoBase,
IReadonlyEnemyHandler
} from '@user/data-system';
export class ArithmeticCalculator implements IDamageCalculator<
IEnemyAttr,
IHeroAttr
> {
calculate(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): IEnemyDamageInfoBase {
const { enemy, hero } = handler;
const heroPerDamage = hero.getFinalAttribute('atk') - enemy.getAttribute('def');
if (heroPerDamage <= 0) {
return { damage: Infinity, turn: 0 };
}
const turn = Math.ceil(enemy.getAttribute('hp') / heroPerDamage);
const base = Math.max(
0,
enemy.getAttribute('atk') - hero.getFinalAttribute('def')
);
const step = 5;
// 首项 base,公差 step,共 turn 项
const damage = turn * base + (step * turn * (turn - 1)) / 2;
return { damage: Math.floor(damage), turn };
}
getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): number {
return handler.hero.getFinalAttribute(attribute);
}
}
```
这是只保留本示例所需分支的最小计算器;内置公式里的无敌、吸血、支援等分支需要你按上文顺序自行保留。
## 临界
临界计算回答「勇士某项属性提高到多少时,战斗结果会发生变化」。相关接口:
- `getCriticalLimit(handler, attribute)`:返回该属性的计算上界;
- `calculateViewCritical(enemy, attribute, precision?)` 与 `calculateEnemyCritical(...)`:伤害系统上的临界计算方法,返回 `IEnemyCritical` 的生成器;
- `CriticalableHeroStatus<THero>`:只允许数值类型的勇士属性,例如 `atk`、`def`、`hp`。
内置 `getCriticalLimit` 对 `atk` 的处理是:怪物有坚固时返回 `Infinity`,否则返回 `def + hp`;其他属性直接返回勇士当前最终值。
现有约定是**伤害值随属性提高单调递减**。临界值唯一依赖「最后的伤害值」,因此公式不单调也能运行,只是算出的临界点不准。写自定义公式时尽量保持单调,临界结果才有意义。

View File

@ -0,0 +1,79 @@
# 最终属性效果
最终效果(final effect)是构建计算后怪物的最后一个阶段:它只对怪物自身的基础属性做收尾调整,不查询怪物上下文,也不影响其他怪物。它适合实现「只看自己(和勇士)就能决定」的转换,例如坚固、模仿。整体流程见[战斗总览](./combat-overview.md)。
## 接口
`IEnemyFinalEffect` 只有两个成员:
- `priority`:优先级,越高越先执行;
- `apply(handler)`:读取 `handler.enemy` 与 `handler.hero`,修改 `handler.enemy` 的基础属性。
注册方式:
```ts
state.enemyContext.registerFinalEffect(effect);
```
`initializeEnemy` 已经注册了内置的 `MainEnemyFinalEffect`(`src/data/enemy/final.ts`)。它处理两个属性:
- **坚固(code 3)**:把怪物防御提升到不低于「勇士攻击 - 1」;
- **模仿(code 8)**:把怪物攻击与防御设置为勇士的最终攻击与最终防御。
## 三方对比
光环、查询、最终效果都在构建计算后怪物时执行,但读取与影响的范围不同:
| 效果 | 读取其他怪物/上下文 | 影响其他怪物 | 典型用途 |
|------|---------------------|--------------|----------|
| 光环 | 否(只按范围命中) | 是 | 给周围怪物加属性或特殊属性 |
| 查询 | 是 | 否(只改自身) | 抱团、同化、号角 |
| 最终 | 否 | 否 | 坚固、模仿、仿攻 |
最终效果可以读取勇士属性(模仿与仿攻都需要),但不会扫描周围怪物,也不会改变其他怪物。
## 示例:仿攻
「仿攻」让怪物的攻击力等于勇士的最终攻击力。仿攻与内置的模仿类似,区别是只改攻击、不改防御:
```ts
// special.ts:32 - 仿攻
map.set(
32,
defineNonePropertySpecial(32, {
name: '仿攻',
desc: '怪物攻击等于勇士攻击。',
color: '#B0C0DD'
})
);
```
```ts
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemyFinalEffect, IEnemyHandler } from '@user/data-system';
export class AttackCopyFinal implements IEnemyFinalEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 0;
apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
if (!handler.enemy.hasSpecial(32)) return;
handler.enemy.setAttribute(
'atk',
handler.hero.getFinalAttribute('atk')
);
}
}
state.enemyContext.registerFinalEffect(new AttackCopyFinal());
```
`apply` 里直接 `setAttribute`,因为最终阶段拿到的 `handler.enemy` 就是可修改的怪物对象。最终阶段之后不再有别的属性修正,所以这里写进去的值会直接进入伤害计算。
`priority` 只需要记住一句:数值越大越先执行;内置最终效果的 `priority` 是 `0`。
## 与伤害计算的分工
最终效果改的是怪物的基础属性,伤害计算器负责决定「这些属性如何变成伤害与回合数」。如果某个效果只是让怪物攻击更高,优先考虑写成光环、查询或最终效果;只有需要改变公式本身时,才去改[伤害计算](./enemy-calculator.md)。

View File

@ -0,0 +1,176 @@
# 怪物查询
查询(query)让特殊属性在构建计算后怪物时读取上下文:周围有哪些怪物、它们带什么特殊属性、勇士在哪里。读取之后,查询可以修改怪物自身的基础属性,或增删、修改怪物的特殊属性。基础概念见[战斗总览](./combat-overview.md)。
:::warning
查询效果是**需要自己注册的新增内容**。当前 `src/data/enemy/initialize.ts` 只注册了光环转换器、最终效果、战斗脚本与钩子,没有注册任何查询效果。下面的接口与示例在你自己调用注册方法之前不会生效。
:::
## 常规查询(CommonQuery)
常规查询面向「某个特殊属性要读外部状态,然后改自己」。注册方式:
```ts
state.enemyContext.registerCommonQueryEffect(code, effect);
```
`IEnemyCommonQueryEffect` 只有两个成员:
- `priority`:优先级;
- `apply(handler, special, query)`:处理逻辑。`handler.enemy` 是正在计算的怪物,`special` 是触发查询的特殊属性,`query()` 返回怪物上下文。
`query()` 不只是取上下文,它还告诉上下文「这个怪物依赖外部状态」。调用后,周围怪物或位置变化时,上下文会把这个怪物标记为需要重新查询。建议始终通过 `query()` 取上下文,而不是直接读 `handler.context`。
## 特殊查询(SpecialQuery)
特殊查询面向「修改目标怪物拥有的特殊属性」,例如给范围内的怪物添加、删除或改写特殊属性。注册方式:
```ts
state.enemyContext.registerSpecialQueryEffect(effect);
```
`IEnemySpecialQueryEffect` 有:
- `priority`:优先级;
- `for(ctx)`:返回一个 `IEnemySpecialQueryModifier`。
修饰器包含四个方法:
- `shouldQuery(handler)`:判断当前怪物是否要执行这次查询;
- `add(handler)`:返回要添加给怪物的特殊属性数组;
- `delete(handler)`:返回要删除的特殊属性数组;
- `modify(handler, special)`:修改怪物的某个特殊属性,真正修改了返回 `true`。
`add` 与 `delete` 不应同时返回非空结果:上下文遇到这种组合会告警并放弃这次修改。
## 优先级
常规查询与特殊查询的 `priority` 和光环共用同一套优先级,从高到低处理。高优先级的效果先执行,低优先级能看到它留下的结果;反过来不行。查询新增的光环只能影响之后的阶段,规则与[怪物光环](./enemy-aura.md)一致。
## 示例:抱团
「抱团」的规则是:范围内每有一个带同种特殊属性的怪物,自身攻击力就增加固定值。用常规查询实现:
```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: 10,
apply(handler, special, query) {
const { radius, atk } = special.value as {
radius: number;
atk: number;
};
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++;
}
}
handler.enemy.addAttribute('atk', count * atk);
}
};
state.enemyContext.registerCommonQueryEffect(40, effect);
```
这个需求如果改用光环实现会麻烦得多:光环只能逐个目标施加减益或增益,很难先统计「范围内有多少个同类」再一次性加攻。查询可以在一次 `apply` 里完成扫描与累加。
## 示例:同化
「同化」的规则是:把范围内其他怪物的特殊属性复制到自己身上;数值型属性加算叠加,非数值型属性取位置索引最小的那个。用特殊查询实现:
```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,
for(ctx) {
return {
shouldQuery: handler => handler.enemy.hasSpecial(41),
add(handler) {
const result: ISpecial<any>[] = [];
const others = [...ctx.iterateEnemy()]
.filter(([, view]) => view.getComputedEnemy() !== handler.enemy)
.sort(
([a], [b]) =>
ctx.indexer.index(a.x, a.y) -
ctx.indexer.index(b.x, b.y)
);
for (const [, view] of others) {
for (const special of view.getComputedEnemy().iterateSpecials()) {
if (special.code === 41) continue;
if (handler.enemy.hasSpecial(special.code)) continue;
result.push(special.clone());
}
}
return result;
},
delete() {
return [];
},
modify(handler, special) {
if (typeof special.value !== 'number') return false;
let sum = special.value;
let found = false;
for (const [, view] of ctx.iterateEnemy()) {
const other = view
.getComputedEnemy()
.getSpecial<number>(special.code);
if (!other || typeof other.value !== 'number') continue;
sum += other.value;
found = true;
}
if (!found) return false;
special.setValue(sum);
return true;
}
};
}
};
state.enemyContext.registerSpecialQueryEffect(effect);
```
`add` 负责处理「自己还没有的属性」,`modify` 负责处理「自己已经有、需要叠加数值」的属性。两者配合,就能把范围内怪物的特殊属性合并到自身。
## 示例:号角
「号角」的规则是:场上每有一只怪物,自身就获得一份加成。它不关心范围与同类,用常规查询遍历全场即可:
```ts
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import { IEnemyCommonQueryEffect } from '@user/data-system';
const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
priority: 5,
apply(handler, special, query) {
const ctx = query();
let count = 0;
for (const _ of ctx.iterateEnemy()) {
count++;
}
handler.enemy.addAttribute('atk', count * (special.value as number));
}
};
state.enemyContext.registerCommonQueryEffect(42, effect);
```
三个示例分别覆盖了「按范围统计同类」「合并特殊属性」「遍历全场计数」三种典型用法。把注册调用放进 `initializeEnemy`,查询就会参与构建计算后怪物。