mirror of
https://github.com/motajs/template.git
synced 2026-10-20 13:50:18 +08:00
docs(10-11): 按 D-50 增加通用机制模板(战斗组)
This commit is contained in:
parent
486784fdcf
commit
a50210aec2
@ -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`(按本次派发要求)。
|
||||
|
||||
|
||||
|
||||
@ -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` 里设置的参数。
|
||||
|
||||
**怎么验证**:把带该属性的怪物与一只没有普通光环的怪物放在范围内,看后者是否被加上了普通光环(属性提升)。
|
||||
|
||||
|
||||
@ -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
|
||||
// [需改] 类名:换成你自己的计算器名
|
||||
|
||||
@ -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` 留空)。
|
||||
|
||||
### 示例:拥有某特殊属性时无法战斗
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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),沿上下左右加四个斜向共八个方向发射,且不影响内置的四方向激光。
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)。
|
||||
|
||||
|
||||
@ -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
|
||||
// [需改] 类名
|
||||
|
||||
Loading…
Reference in New Issue
Block a user