diff --git a/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md index 2f64dcc..712210e 100644 --- a/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md +++ b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md @@ -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`(按本次派发要求)。 + diff --git a/docs/intermediate/enemy-aura.md b/docs/intermediate/enemy-aura.md index 3715da9..5c08056 100644 --- a/docs/intermediate/enemy-aura.md +++ b/docs/intermediate/enemy-aura.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 { + // [模板] 签名固定:引擎拿每条特殊属性挨个问「这条你管吗」 + shouldConvert( + special: ISpecial, + handler: IReadonlyEnemyHandler + ): boolean { + // [需改] 认领条件:这个转换器负责哪个 code + return special.code === 30; + } + + // [模板] 签名固定:把认领到的特殊属性翻译成视图 + convert( + special: ISpecial, + handler: IReadonlyEnemyHandler, + context: IEnemyContext + ): 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; + + // [模板] 构造函数保存来源怪物、特殊属性与位置 + constructor( + readonly enemy: IReadonlyEnemy, + readonly special: ISpecial, + 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, + baseEnemy: IReadonlyEnemy + ): void { + // [需改] 在这里改 handler.enemy 的基础属性 + } + + // [模板] 签名固定:不改特殊属性就返回 null + applySpecial(): IEnemySpecialModifier | 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( 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): boolean { return special.code === 30; // [需改] code } - // [模板] convert 签名固定:把认领到的特殊属性翻译成视图 convert( special: ISpecial, handler: IReadonlyEnemyHandler ): 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; - // [模板] 构造函数把来源怪物、特殊属性、位置存下来 constructor( readonly enemy: IReadonlyEnemy, readonly special: ISpecial, 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, baseEnemy: IReadonlyEnemy ): 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 | 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 { - // [模板] 构造函数存下来源属性 constructor(readonly special: ISpecial) {} - // [需改] add:返回要添加给目标的特殊属性数组 add(handler: IReadonlyEnemyHandler): ISpecial[] { // [需改] 已经有普通光环(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 { atkBuff: 0, defBuff: 0 }); - // [模板] 返回要添加的属性 return [sp]; } - // [模板] 本修饰器不删除属性,返回空数组 delete(): ISpecial[] { return []; } - // [模板] 本修饰器不修改已有属性,返回 false modify(): boolean { return false; } } -// [需改] 类名:换成你的转换器名 export class ChainAuraConverter implements IAuraConverter< IEnemyAttr, IHeroAttr > { - // [模板] shouldConvert:认领 code,通常只换这个判断 shouldConvert(special: ISpecial): boolean { return special.code === 31; // [需改] code } - // [模板] convert 签名固定 convert( special: ISpecial, handler: IReadonlyEnemyHandler ): ChainAura { - // [需改] 构造视图 return new ChainAura(handler.enemy, special, handler.locator); } } -// [需改] 类名:换成你的视图名 export class ChainAura implements IEnemyAuraView { - // [需改] 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, readonly special: ISpecial, readonly locator: ITileLocator ) {} - // [模板] 范围参数:FullRange 不需要参数 getRangeParam(): void { return undefined; } - // [模板] 本光环不改基础属性,apply 留空 apply(): void {} - // [需改] applySpecial:返回修饰器,由上下文按优先级应用到目标 applySpecial(): IEnemySpecialModifier | null { return new ChainAuraModifier(this.special); } } ``` -**改什么**:编号(`31`)、类名、`priority`、范围与 `add` 里设置的参数(本示例的 `range` / `hpBuff` 等光环字段)换成你自己的。 +**改什么**:`31`、类名、`priority`、范围与 `add` 里设置的参数。 **怎么验证**:把带该属性的怪物与一只没有普通光环的怪物放在范围内,看后者是否被加上了普通光环(属性提升)。 diff --git a/docs/intermediate/enemy-calculator.md b/docs/intermediate/enemy-calculator.md index 13caf15..00be7ce 100644 --- a/docs/intermediate/enemy-calculator.md +++ b/docs/intermediate/enemy-calculator.md @@ -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 { + // [模板] calculate 签名固定:读 handler 里的 enemy 与 hero,返回 { damage, turn } + calculate( + handler: IReadonlyEnemyHandler + ): IEnemyDamageInfoBase { + // [模板] 取出只读的怪物与勇士对象(只读,供读取,不可改写) + const { enemy, hero } = handler; + // [需改] 在这里写你的公式;只读输入,不改任何外部状态 + return { damage: 0, turn: 0 }; + } + + // [模板] getCriticalLimit 签名固定:返回临界计算的上界 + getCriticalLimit( + handler: IReadonlyEnemyHandler, + attribute: CriticalableHeroStatus + ): number { + // [需改] 需要自定义临界上界时改这里;否则返回勇士当前最终值即可 + return handler.hero.getFinalAttribute(attribute); + } +} +``` + +注册(**替换**当前计算器): + +```ts +state.enemyContext.getDamageSystem()?.useCalculator(new XxxCalculator()); +``` + +复制后需要改的只有:类名、`calculate` 里的公式、必要时 `getCriticalLimit` 的返回值。**计算器必须无副作用**(见上方 warning)——模板里的 `calculate` 只读输入,不要写 flag、不要改 `enemy` / `hero`。 + +### 示例:等差数列 + +下面用「等差数列」演示自写类的最小形态(只保留本示例所需分支)。它只是填空示范,不是模板本身: ```ts // [需改] 类名:换成你自己的计算器名 diff --git a/docs/intermediate/enemy-combat-script.md b/docs/intermediate/enemy-combat-script.md index 752770c..9984610 100644 --- a/docs/intermediate/enemy-combat-script.md +++ b/docs/intermediate/enemy-combat-script.md @@ -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 { + // [需改] priority:越大越先执行;内置脚本是 0,自写时不要撞上 0 + readonly priority = 100; + + // [模板] before 签名固定:战前执行,返回 false 即放弃战斗 + async before( + info: IEnemyDamageInfo, + handler: ICombatFlowHandler + ): Promise { + // [需改] 你的战前判断;不拦截就返回 true + return true; + } + + // [模板] after 签名固定:战后执行 + async after( + info: IEnemyDamageInfo, + handler: ICombatFlowHandler + ): Promise { + // [需改] 你的战后处理 + } +} +``` + +注册(**追加**): + +```ts +state.combat.addCombatScript(new XxxCombatScript()); +``` + +复制后需要改的只有:类名、`priority`、`before` 里的拦截条件、`after` 里的处理。不用的方法保留空实现即可(`before` 返回 `true`、`after` 留空)。 ### 示例:拥有某特殊属性时无法战斗 diff --git a/docs/intermediate/enemy-final.md b/docs/intermediate/enemy-final.md index aa7f5a2..2f8bf8c 100644 --- a/docs/intermediate/enemy-final.md +++ b/docs/intermediate/enemy-final.md @@ -108,22 +108,54 @@ apply(handler: IEnemyHandler): 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): 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): void { + // [需改] 只有带本效果(示例 code 32)的怪物才处理 if (!handler.enemy.hasSpecial(32)) return; - // [需改] 真正做的事:把怪物攻击设为勇士的最终攻击 + // [需改] 把怪物攻击设为勇士的最终攻击 handler.enemy.setAttribute( 'atk', handler.hero.getFinalAttribute('atk') diff --git a/docs/intermediate/enemy-map-damage.md b/docs/intermediate/enemy-map-damage.md index 0990176..9d97a71 100644 --- a/docs/intermediate/enemy-map-damage.md +++ b/docs/intermediate/enemy-map-damage.md @@ -123,7 +123,91 @@ convert( `useConverter` 是**替换**当前转换器:选了自己的就把内置的整个换掉,若要同时保留内置五类,需要在自己的转换器里把它们的视图也收集进去,或直接改内置转换器。 -下面代码用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求改的行。 +### 模板:复制什么、改什么 + +自己写地图伤害永远是「一个视图 + 一个转换器」两个类。下面是机制本身的骨架,不含任何具体伤害;复制到新文件后按 `[需改]` 修改。 + +先写视图——它负责「范围、某点的伤害与类型」: + +```ts +// [需改] 类名:换成你的视图名 +export class XxxDamageView implements IMapDamageView { + // [模板] 构造函数接收引擎传进来的上下文、位置与特殊属性 + constructor( + private readonly context: IEnemyContext, + private readonly locator: ITileLocator, + private readonly special: ISpecial + ) {} + + // [需改] 范围对象:RectRange / ManhattanRange / RayRange / FullRange 之一 + getRange(): IRange { + // [需改] RANGE 换成你在模块顶部建好的范围对象 + return RANGE; + } + + // [需改] 与范围配套的参数(矩形 { x, y, w, h };曼哈顿 { cx, cy, radius }; + // 射线 { cx, cy, dir };全场返回 undefined) + getRangeParam(): TRangeParam { + return undefined as TRangeParam; + } + + // [模板] 先判范围,再取伤害;通常不需要改 + getDamageAt(locator: ITileLocator): Readonly | 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 | 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, + context: IEnemyContext + ): IMapDamageView[] { + // [需改] 读取要转换的特殊属性 + const special = handler.enemy.getSpecial(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),沿上下左右加四个斜向共八个方向发射,且不影响内置的四方向激光。 diff --git a/docs/intermediate/enemy-query.md b/docs/intermediate/enemy-query.md index 6d5d17e..fc42570 100644 --- a/docs/intermediate/enemy-query.md +++ b/docs/intermediate/enemy-query.md @@ -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, + special: ISpecial, + query: () => IReadonlyEnemyContext + ): 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 + ) {} + + // [需改] 这次查询作用于哪些怪物 + shouldQuery( + handler: IReadonlyEnemyHandler + ): boolean { + return handler.enemy.hasSpecial(41); + } + + // [需改] 要添加的特殊属性 + add( + handler: IReadonlyEnemyHandler + ): ISpecial[] { + return []; + } + + // [需改] 要删除的特殊属性 + delete(): ISpecial[] { + return []; + } + + // [需改] 要改写的特殊属性;真正改了才返回 true + modify( + handler: IEnemyHandler, + special: ISpecial + ): boolean { + return false; + } +} + +// [需改] 类名:效果本身 +export class XxxSpecialQueryEffect implements IEnemySpecialQueryEffect< + IEnemyAttr, + IHeroAttr +> { + // [需改] 优先级 + readonly priority = 15; + + // [模板] for 签名固定:返回修饰器 + for( + ctx: IReadonlyEnemyContext + ): IEnemySpecialQueryModifier { + 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, special: ISpecial, query: () => IReadonlyEnemyContext ): 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 ) {} - // [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物) + // [需改] 这次查询作用于哪些怪物 shouldQuery( handler: IReadonlyEnemyHandler ): boolean { return handler.enemy.hasSpecial(41); } - // [需改] add:返回要添加给怪物的特殊属性 + // [需改] 把别人有、自己没有的属性照抄过来(跳过同化属性自身) add( handler: IReadonlyEnemyHandler ): ISpecial[] { const result: ISpecial[] = []; - // [模板] 取全场其他怪物,并按位置索引从小到大排序 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[] { return []; } - // [需改] modify:自己已有、需要叠加数值的属性在这里处理 + // [需改] 自己已有、需要叠加数值的属性在这里处理 modify( handler: IEnemyHandler, special: ISpecial ): 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 ): IEnemySpecialQueryModifier { @@ -277,27 +372,24 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect()); **复制什么**: ```ts -// [需改] 类名:同样是常规查询效果 export class HornQueryEffect implements IEnemyCommonQueryEffect< IEnemyAttr, IHeroAttr > { readonly priority = 5; // [需改] 优先级 - // [模板] apply 签名固定 apply( handler: IEnemyHandler, special: ISpecial, query: () => IReadonlyEnemyContext ): 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)); } } diff --git a/docs/intermediate/enemy-specials.md b/docs/intermediate/enemy-specials.md index 0003764..a10e503 100644 --- a/docs/intermediate/enemy-specials.md +++ b/docs/intermediate/enemy-specials.md @@ -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( + XX, + { value: 0 }, // [需改] 默认参数 + { + name: '{名称}', // [需改] 显示名称 + // [需改] 悬停说明;需要按参数变化时写成函数 + desc: special => `...${special.value.value}...`, + color: '#XXXXXX' // [需改] 名称颜色 + } + ) +); +``` + +复制后按「编辑器识别约定」补齐注释与接口命名:`// {code} - {name}`、`/** {code} - {name} */`、`ISpecial{code}Value`。真正的生效逻辑不在这个文件里,按下方「属性实现位置」表去找落点。 + +### 示例:灼烧(每回合固定伤害) + +以「每回合给勇士造成固定伤害」为例(假设使用 code 50): **目标**:新增一条「每回合对勇士造成固定伤害」的特殊属性(示例 code 50)。 diff --git a/docs/intermediate/enemy-status-hooks.md b/docs/intermediate/enemy-status-hooks.md index a962930..0f9c421 100644 --- a/docs/intermediate/enemy-status-hooks.md +++ b/docs/intermediate/enemy-status-hooks.md @@ -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 { + // [模板] 构造时保存数据端状态,供读写 flag、取勇士属性等 + constructor(readonly state: ICoreState) {} + + // [需改] 方法名:按上表换成 onStepEnd / onAfterTeleportTo / onAfterCombat / onSetFloor + async onAfterCombat( + info: IEnemyDamageInfo + ): Promise { + // [需改] 你的状态逻辑:读状态、改 flag、挂修饰器等 + } +} +``` + +注册到对应挂载点: + +```ts +// [需改] 挂载点与方法签名要匹配(见上表) +state.combat.addHook(new XxxHooks(state)); +``` + +复制后需要改的只有:类名、实现的接口、钩子方法名与内部逻辑、挂载点。一个状态要多个钩子时,可以写在同一个类里(实现多个接口),也可以拆成几个类分别挂载。 + +### 示例:蓄力(下次战斗伤害乘二) + +下面仍以「蓄力」为例,演示自写类的形态。它只是填空示范,不是模板本身:一个钩子负责挂状态,一个脚本负责消费(示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」)。 ```ts // [需改] 类名