HumanBreak/docs/guide/implements/special.md
2025-08-22 16:45:47 +08:00

9.4 KiB
Raw Blame History

怪物特殊属性

下面以特殊属性“勇士造成的伤害减少 10%”为例,展示如何自定义一个仅修改伤害计算的特殊属性。

:::warning 伤害计算将会在 2.C 中重构,不过逻辑并不会有大幅变动。 :::

编写属性定义

打开 packages-user/data-state/src/enemy/special.ts,在最后添加一个属性定义:

export const specials: SpecialDeclaration[] = [
    // ... 原有属性定义

    // 自定义属性
    {
        code: 30, // 特殊属性代码,用于 hasSpecial 判断 // [!code ++]
        name: '自定义特殊属性', // 特殊属性名称 // [!code ++]
        desc: '勇士对该怪物造成的伤害减少 10%', // 特殊属性描述 // [!code ++]
        color: '#ffd' // 特殊属性显示的颜色 // [!code ++]
    }
];

实现特殊属性

打开 packages-user/data-state/src/enemy/damage.ts,在文件最后的 calDamageWith 函数中编写:

export function calDamageWith(
    info: UserEnemyInfo,
    hero: Partial<HeroStatus>
): number | null {
    // ... 原有内容

    // 在需要降低勇士伤害的地方将勇士伤害乘以 0.9 即可
    // 注意需要再回合计算前乘,不然没有效果
    heroPerDamage *= 0.9; // [!code ++]

    // ... 原有内容
}

拓展-用函数声明属性

特殊属性的属性名和介绍可以使用函数来声明,这允许属性名和描述根据怪物属性变化。下面我们以特殊属性“勇士伤害减少n%”为例,展示如何声明这种类型的属性。

编辑表格

首先我们打开编辑器,选中任意一个怪物,在左侧属性栏上方找到编辑表格,然后点击它打开,找到【怪物】相关的表格配置,在 _data 属性下仿照攻击或其他属性新增一项,注意不要忘记了逗号:

"enemys": {
    "_data": {
        // 属性名为 myAttr
        "myAttr": {
            "_leaf": true,
            "_type": "textarea",
            // 属性说明
            "_docs": "伤害减免",
            "_data": "伤害减免"
        },
    }
}

类型声明

然后打开 src/types/declaration/event.d.ts,找到开头的 type PartialNumbericEnemyProperty =,在后面新增一行:

type PartialNumbericEnemyProperty =
    | 'value'
    // ... 其他属性声明

    // 新增自己的 myAttr 属性
    // 注意不要忘记删除前一行最后的分号
    | 'myAttr'; // [!code ++]

属性定义

最后在 special.ts 中新增属性定义即可:

export const specials: SpecialDeclaration[] = [
    // ... 原有属性定义

    // 自定义属性
    {
        code: 30, // 特殊属性代码,用于 hasSpecial 判断
        name: enemy => `${enemy.myAttr ?? 0}%减伤`, // 特殊属性名称 // [!code ++]
        desc: enemy => `勇士对该怪物造成的伤害减少${enemy.myAttr ?? 0}%`, // 特殊属性描述 // [!code ++]
        color: '#ffd' // 特殊属性显示的颜色
    }
];

此时,如果给怪物的 myAttr 栏填写 10,那么特殊属性名称就会显示 10%减伤,属性描述会显示 勇士对该怪物造成的伤害减少10%

属性实现

修改 damage.ts calDamageWith 中的实现:

export function calDamageWith(
    info: UserEnemyInfo,
    hero: Partial<HeroStatus>
): number | null {
    // ... 原有内容

    // 在乘以 1 - (myAttr / 100),除以 100 是因为 myAttr 是百分制
    heroPerDamage *= 1 - (info.myAttr ?? 0) / 100; // [!code ++]

    // ... 原有内容
}

拓展-地图伤害

同样在 damage.ts,找到 DamageEnemy.calMapDamage 方法,直接 ctrl+F 搜索 calMapDamage 即可找到,然后在其中编写地图伤害即可。以领域为例,它是这么写的:

class DamageEnemy {
    calMapDamage(
        damage: Record<string, MapDamage> = {},
        hero: Partial<HeroStatus> = getHeroStatusOn(realStatus)
    ) {
        // 判断是否包含领域属性
        if (this.info.special.has(15)) {
            // 计算领域范围
            const range = enemy.range ?? 1;
            const startX = Math.max(0, this.x - range);
            const startY = Math.max(0, this.y - range);
            const endX = Math.min(floor.width - 1, this.x + range);
            const endY = Math.min(floor.height - 1, this.y + range);
            // 伤害量
            const dam = enemy.value ?? 0;
            const objs = core.getMapBlocksObj(this.floorId);

            for (let x = startX; x <= endX; x++) {
                for (let y = startY; y <= endY; y++) {
                    if (
                        !enemy.zoneSquare &&
                        // 判断非九宫格领域,使用曼哈顿距离判断
                        manhattan(x, y, this.x, this.y) > range
                    ) {
                        continue;
                    }
                    const loc = `${x},${y}` as LocString;
                    if (objs[loc]?.event.noPass) continue;
                    // 存储地图伤害
                    this.setMapDamage(damage, loc, dam, '领域');
                }
            }
        }
    }
}

拓展-光环属性

光环计算目前分为两个优先级,高优先级的可以影响低优先级的,这意味着你可以做出来加光环的光环属性。不过高级光环的逻辑比较绕,而且需求不高,这里不再介绍。如果需要的话可以自行理解这部分逻辑或在造塔群里询问。这里以攻击光环为例,展示如何制作一个普通光环。

我们假设使用 atkHalo 作为光环增幅,haloRange 作为光环范围,属性代码为 30,九宫格光环。我们在 damage.ts 中找到 DamageEnemy.provideHalo 方法,直接 ctrl+F 搜索 provideHalo 就能找到。

光环逻辑

我们直接调用 applyHalo 即可,如下编写代码:

class DamageEnemy {
    provideHalo() {
        // ... 原有逻辑

        // 施加光环
        col.applyHalo(
            // 光环形状为正方形。目前支持 square 矩形和 rect 矩形
            'square',
            // 正方形形状参数
            {
                x: this.x, // 中心横坐标
                y: this.y, // 中心纵坐标
                d: this.info.haloRange * 2 + 1 // 边长
            },
            this, // 填 this 即可
            (e, enemy) => {
                // 这里的 e 是指被加成的怪物enemy 是当前施加光环的怪物
                // 直接加到 atkBuff_ 属性上即可
                e.atkBuff_ += enemy.atkHalo;
            }
        );

        // 在地图上显示光环,这部分可选,如果不想显示也可以不写
        col.haloList.push({
            // 光环形状
            type: 'square',
            // 形状参数
            data: { x: this.x, y: this.y, d: this.info.haloRange * 2 + 1 },
            // 特殊属性代码
            special: 30,
            // 施加的怪物
            from: this
        });
    }
}

自定义形状

如果想要自定义光环形状,我们打开 packages-user/data-utils/src/range.ts,拉到最后可以看到形状定义,目前包含两个:

  • square: 中心点+边长的正方形
  • rect: 左上角坐标+宽高的矩形

我们以曼哈顿距离为例,展示如何自定义形状。

首先在开头找到 interface RangeTypeData,在其中添加必要的参数类型:

interface RangeTypeData {
    // ... 原有内容

    // 自定义的曼哈顿范围参数,包含中心坐标和半径
    manhattan: { x: number; y: number; dis: number }; // [!code ++]
}

然后在文件最后定义形状即可:

// 这里的第一个参数就是我们的形状名称,填 manhattan 即可
// 第二个参数是一个函数,目的是判断 item 是否在范围内
Range.register('manhattan', (item, { x, y, dis }) => {
    // 如果 item 连坐标属性都不存在,那么直接判定不在范围内
    if (isNil(item.x) || isNil(item.y)) return false;
    // 计算与中心的坐标差
    const dx = Math.abs(item.x - x);
    const dy = Math.abs(item.y - y);
    // 坐标差之和小于半径则在范围内,否则在范围外
    return dx + dy < dis;
});

在光环中,我们就可以直接使用这种形状了:

col.applyHalo(
    // 使用自定义形状
    'manhattan',
    // 自定义形状的参数
    {
        x: this.x, // 中心横坐标
        y: this.y, // 中心纵坐标
        dis: this.info.haloRange // 半径
    },
    this,
    (e, enemy) => {
        e.atkBuff_ += enemy.atkHalo;
    }
);

拓展-输出回合数

样板默认的 calDamageWith 函数只允许输出伤害值,而有时候我们可能会需要战斗的回合数,这时候我们需要修改一下这部分内容,将伤害计算逻辑单独提出来,然后在 calDamageWith 中调用它。在需要回合数的时候,我们调用提出了的函数即可,如下例所示:

/** 包含回合数的伤害计算 */
export function calDamageWithTurn(
    info: UserEnemyInfo,
    hero: Partial<HeroStatus>
) {
    // ... 原本 calDamageWith 的计算逻辑,记得删除最后返回伤害的那一行返回值

    // 返回回合数和伤害
    return { turn, damage };
}

export function calDamageWith(info: UserEnemyInfo, hero: Partial<HeroStatus>) {
    // 调用单独提出的函数计算伤害值
    const damageInfo = calDamageWithTurn(info, hero);
    // 如果伤害不存在,那么返回无穷大
    return damageInfo?.damage ?? Infinity;
}