template/docs/intermediate/enemy-specials.md
unanmed 5528edc70d docs(10-04): write combat overview, specials, and aura docs
- add combat flow overview with mermaid and doc map
- document special attribute definitions and implementation locations
- document aura priority, built-in auras, and custom examples
2026-10-17 18:43:06 +08:00

6.5 KiB
Raw Blame History

特殊属性

特殊属性是挂在怪物身上的机制:一个数字 code 加一份可选参数 value。怪物的基础属性决定「有多少血、多少攻防」,特殊属性决定「它还会做什么」,例如先攻、连击、坚固、光环、中毒。访问怪物特殊属性的入口见战斗总览。

在数据里的样子

数据端的怪物对象(IReadonlyEnemy)提供三个读取接口:

  • getSpecial<T>(code):取指定 code 的特殊属性对象,没有则返回 null;
  • hasSpecial(code):判断是否拥有指定特殊属性;
  • iterateSpecials():迭代全部特殊属性。

每个特殊属性对象实现 ISpecial<T>,常用成员是 code、value 与 getDescription():

const multiHit = enemy.getSpecial<number>(4);
if (multiHit) {
    // 连击的 value 是每回合攻击次数
    console.log(multiHit.value);
}

if (enemy.hasSpecial(3)) {
    // 坚固:只看有没有,不读参数
}

ISpecial 还提供 setValue / getValue、getSpecialName / getNameColor、clone 与 deepEqualsTo。需要修改怪物时用 IEnemy 的 addSpecial / deleteSpecial,不要直接改 value 后忘记刷新上下文。

两类定义

内置特殊属性在 src/data/enemy/special.ts 的 createSpecials(state) 中定义,使用两个工厂函数:

  • defineNonePropertySpecial(code, config):无参数,value 为 void,适合先攻、魔攻、坚固、模仿这类纯机制;
  • defineCommonSerializableSpecial<T>(code, defaultValue, config):有参数,参数会进入存档,适合连击、破甲、光环这类需要数值描述的效果。

config 的 name、desc、color 都可以写成字符串,也可以写成 (special) => string 的函数:

// 无参:坚固
map.set(
    3,
    defineNonePropertySpecial(3, {
        name: '坚固',
        desc: '怪物防御不小于勇士攻击-1。',
        color: '#C0B088'
    })
);

// 有参:n连击,默认 2 次
map.set(
    4,
    defineCommonSerializableSpecial(4, 2, {
        name: special => `${special.value}连击`,
        desc: special => `怪物每回合攻击${special.value}次。`,
        color: '#FE7'
    })
);

参数可以不只是数字。比如吸血用对象保存比例与是否加到自身:

/** 9 - 吸血 */
export interface ISpecial9Value {
    /** 吸血比例 */
    vampire: number;
    /** 是否加到自身生命值上 */
    add: boolean;
}

map.set(
    9,
    defineCommonSerializableSpecial<ISpecial9Value>(
        9,
        { vampire: 0, add: false },
        {
            name: '吸血',
            desc: special => `战斗前吸取勇士${special.value.vampire}%生命。`,
            color: '#DD4448'
        }
    )
);

自定义一个新特殊属性

以新增一个「每回合给勇士造成固定伤害」的属性为例(假设使用 code 50):

  1. 选一个未使用的 code。先搜一遍 special.ts 中已有的 map.set(...),确认没有冲突。
  2. 有参数时定义参数接口。在文件顶部 //#region 复合属性值类型 内声明 ISpecial50Value,并在接口前写 /** 50 - 属性名 */。
  3. 在 //#region 属性定义 内注册。每条定义前写 // 50 - 属性名,再用 map.set(50, define...) 添加。
  4. 实现效果。按下方「属性实现位置」表,把效果写进对应的计算器、光环、地图伤害或钩子;如果不属于现有分类,再考虑新增对应的转换器 / 效果 / 钩子。
  5. 刷新上下文。修改怪物模板或新增属性后,调用 state.enemyContext.markDirty(view) 标记脏数据,必要时调用 state.enemyContext.buildup() 全量重建计算后怪物。
// 50 - 灼烧
map.set(
    50,
    defineCommonSerializableSpecial(50, 0, {
        name: '灼烧',
        desc: special => `每回合对勇士造成${special.value}点伤害。`,
        color: '#F80'
    })
);

createSpecials(state) 返回 Map<number, SpecialCreation<any>>。initializeEnemy 会遍历这个 Map,逐个调用 state.enemyManager.registerSpecial(code, cons),所以新增属性只要放进这个 Map 就会完成注册。

属性实现位置

special.ts 顶部的注释给出了内置属性的实现分工。新增属性时按这张表找落点:

属性类型 实现位置 说明
调参类(先攻、魔攻、连击、破甲、反击、净化、固伤、仇恨、自爆等) src/data/enemy/calculator.ts 的 MainDamageCalculator.calculate 只改变伤害公式的结果
坚固、模仿 src/data/enemy/final.ts 的 MainEnemyFinalEffect.apply 只修改怪物自身基础属性
领域、夹击、阻击、激光、伏击 src/data/enemy/mapDamage.ts 伤害视图 + 转换器
光环、支援、特殊光环 src/data/enemy/aura.ts 光环视图 + 转换器
中毒的每步效果 src/data/enemy/hook.ts 的 EnemyMoveHooks.handlePoison 移动钩子
中毒的瞬移效果 src/data/enemy/hook.ts 的 EnemyPathfindingHooks.handlePoison 寻路钩子
衰弱 src/data/enemy/hook.ts 的 EnemyCombatHooks.onAfterCombat 战后钩子 + 修饰器
重生 src/data/enemy/hook.ts 的 EnemyCombatHooks.onAfterCombat 与 EnemyChangeFloorHooks.onSetFloor 两个钩子协同

具体实现方式分别见怪物光环、伤害计算、地图伤害与状态钩子。

编辑器识别约定

special.ts 里的部分注释是编辑器的识别模式,删除后编辑器可能无法识别属性。新增属性时保持以下写法:

  • 属性定义前写 // {code} - {name},例如 // 50 - 灼烧;
  • 参数接口前写 /** {code} - {name} */,例如 /** 50 - 灼烧 */;
  • 参数接口名必须是 ISpecial{code}Value,例如 ISpecial50Value;
  • //#region 复合属性值类型 与 //#region 属性定义 这两行标记不能删除。

:::warning special.ts 顶部的注释明确说明这些注释会被编辑器读取。修改属性定义时不要顺手删掉 #region 标记或 // {code} - {name} 注释。 :::

怪物比较

MainEnemyComparer.compare 负责判断两个怪物是否相同:先比较 hp、atk、def、money、exp、point,再逐个比较特殊属性的 deepEqualsTo。它用于怪物模板的脏数据判断。一般不需要改;只有当你希望「两个怪物是否视为同一模板」的判定标准发生变化时,才替换比较器。