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

9.0 KiB
Raw Blame History

主动技能

在数据端新增一个文件定义技能开启与关闭的行为,然后在数据端处理录像,最后处理交互。

对于键盘,在 packages-user/client-modules/src/action/hotkey.ts 中自定义技能按键,并在此处实现。

对于触屏和鼠标,在 packages-user/client-modules/src/render/ui/statusBar.tsx 中提供技能按钮。

下面以技能“二倍斩”为例,展示如何实现主动技能。

技能开启关闭行为

packages-user/data-state/src/mechainism 文件夹下新增一个文件 skill.ts,然后打开同一文件夹下的 index.ts,写入 export * from './skill.ts';。回到 skill.ts,开始编写技能的开启与关闭行为。

由于二倍斩技能本质上是修改战斗函数,因此我们只需要一个变量来存储当前是否开启了技能即可。因此写出如下内容:

/** 二倍斩技能是否已经开启 */
let skill1 = false;

/** 开启二倍斩技能 */
export function enableSkill1() {
    // 将变量设为 true
    skill1 = true;
    // 更新状态栏
    core.updateStatusBar();
}

/** 关闭二倍斩技能 */
export function disableSkill1() {
    skill1 = false;
    core.updateStatusBar();
}

/** 获取二倍斩技能是否已经开启 */
export function getSkill1Enabled() {
    return skill1;
}

/** 切换二倍斩技能,如果开启则关闭,否则开启 */
export function toggleSkill1() {
    if (skill1) disableSkill1();
    else enableSkill1();
}

修改伤害计算

打开 packages-user/data-state/src/enemy/damage.ts,在最后找到 calDamageWith 函数,在里面修改勇士的 heroPerDamage 即可:

// 文件开头引入刚刚编写的 skill.ts可以使用自动补全自动引入
import { getSkill1Enabled } from '../machanism/skill'; // [!code ++]

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

    // 在特定位置将勇士伤害乘以 2
    // 注意需要再回合计算前乘,不然没有效果
    if (getSkill1Enabled()) {
        heroPerDamage *= 2;
    }

    // ... 原有内容
}

录像处理

录像处理其实很简单,我们只需要简单修改我们刚刚编写的几个函数,并注册一个新录像即可。

我们先在 skill.ts 中编写一个 createSkill 函数,注册录像行为:

export function createSkill() {
    // 样板接口,注册录像行为
    core.registerReplayAction('skill1', action => {
        // action 可能是 skill1:1 或者 skill1:0
        // 前者表示开启技能,后者表示关闭
        if (!action.startsWith('skill1:')) return;
        // 获取应该开启还是关闭
        const [, param] = action.split(':');
        const enable = parseInt(param) === 1;
        // 执行开启或关闭行为
        // 由于是在同一个文件,因此是不需要引入的
        if (enable) enableSkill1();
        else disableSkill1();

        // 这一句不能少
        core.replay();
    });
}

然后我们再次进入 index.ts,在 createMechanism 函数中调用 createSkill

import { createSkill } from './skill'; // [!code ++]

export function createMechanism() {
    // ... 原有内容
    createSkill(); // [!code ++]
}

最后简单修改一下 enableSkill1disableSkill1 即可:

/** 开启二倍斩技能 */
export function enableSkill1() {
    skill1 = true;
    core.updateStatusBar();
    // 将开启技能行为计入录像
    core.status.route.push('skill1:1'); // [!code ++]
}

/** 关闭二倍斩技能 */
export function disableSkill1() {
    skill1 = false;
    core.updateStatusBar();
    // 将关闭技能行为计入录像
    core.status.route.push('skill1:0'); // [!code ++]
}

按键交互与点击交互

按键交互参考此文档

点击交互参考此文档

最终实现参考(按键和点击):

:::code-group

// 引入刚刚编写的函数
import { toggleSkill1 } from '@user/data-state';

gameKey
    // 按键分组
    .group('skill', '主动技能')
    // 按键注册
    .register({
        id: 'skill1',
        name: '二倍斩',
        defaults: KeyCode.Digit1
    });

// 按键实现
gameKey.realize('skill1', toggleSkill1);
// 引入刚刚编写的函数
import { toggleSkill1 } from '@user/data-state'; // [!code ++]

// 在状态栏新增
export const LeftStatusBar = defineComponent<StatusBarProps<ILeftHeroStatus>>(
    p => {
        return () => (
            <container>
                {/* ... 原有内容 */}

                {/* 新增一个 text 标签,点击时执行 toggleSkill1 切换技能 */}
                <text // [!code ++]
                    text="切换二倍斩" // [!code ++]
                    cursor="pointer" // [!code ++]
                    onClick={toggleSkill1} // [!code ++]
                />
            </container>
        );
    }
);

:::

拓展-多技能设计思路

很多时候我们可能会有多个技能,且多个技能间互斥,即每次只能开启一个技能,这时候如果我们给每个技能都单独编写一套 enable disable toggle 会显得很啰嗦,也不易维护。

枚举定义

此时我们可以考虑使用枚举方式来定义:

export const enum SkillType {
    None, // 未开启技能
    DoubleAttack, // 二倍斩
    TripleAttack // 三倍斩
    // ... 其他技能
}

修改开启关闭行为

然后给 enable 系列函数添加一个参数,来指定开启某个技能:

/** 当前开启了什么技能 */
let enabled: SkillType = SkillType.None;

export function enableSkill(skill: SkillType) {
    // 如果要开启的和当前技能一致,则不做任何事
    if (enabled === skill) return;
    // 否则切换当前技能
    enabled = skill;
    // 更新状态栏
    core.updateStatusBar();
    // 计入录像,直接计入当前开启了什么技能
    core.status.route.push(`skill:${skill}`);
}

export function disableSkill() {
    // 关闭技能相当于切换至无技能
    enableSkill(SkillType.None);
}

export function toggleSkill(skill: SkillType) {
    // 改为判断是否与当前技能一致,一致则关闭,否则切换至目标技能
    if (enabled === skill) disableSkill();
    else enableSkill(skill);
}

export function getEnabledSkill() {
    return enabled;
}

技能判断

在其他地方直接判断当前技能,可以使用 ifswitch

:::code-group

import { getEnabledSkill, SkillType } from './skill';

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

    // 获取当前开启了什么技能
    const enabled = getEnabledSkill();
    // 使用 if 判断
    if (enabled === SkillType.DoubleAttack) heroPerDamage *= 2;
    else if (enabled === SkillType.TripleAttack) heroPerDamage *= 3;

    // ... 原有内容
}
import { getEnabledSkill, SkillType } from './skill';

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

    // 获取当前开启了什么技能
    const enabled = getEnabledSkill();
    // 使用 switch 判断
    switch (enabled) {
        case SkillType.DoubleAttack:
            heroPerDamage *= 2;
            break;
        case SkillType.TripleAttack:
            heroPerDamage *= 3;
            break;
    }

    // ... 原有内容
}

:::

录像处理

录像直接改为开启目标技能即可:

export function createSkill() {
    // 样板接口,注册录像行为
    core.registerReplayAction('skill', action => {
        if (!action.startsWith('skill:')) return;
        // 获取应该开启的技能
        const [, param] = action.split(':');
        const skill = parseInt(param);
        // 开启目标技能,由于关闭技能就是开启 SkillType.None因此这里直接这么写就行
        enableSkill(skill);

        // 这一句不能少
        core.replay();
    });
}

拓展-战后自动关闭技能

可以使用战后的钩子实现,写在 createSkill 函数中,具体实现方式如下:

import { hook } from '@user/data-base';

export function createSkill() {
    // ... 原有内容

    // 战后钩子,会在战后自动执行
    hook.on('afterBattle', () => {
        // 战后直接关闭技能即可
        disableSkill();
    });
}

拓展-在开启或关闭技能时执行内容

直接在 enableSkill 里面编写即可,如果是单技能,那么直接编写内容,否则需要判断:

export function enableSkill(skill: SkillType) {
    // ... 原有内容

    // 使用 switch 判断
    switch (skill) {
        case SkillType.None:
            // 显示提示
            core.drawTip('已关闭技能!');
            break;
        case SkillType.DoubleAttack:
            // 显示提示
            core.drawTip('已开启二倍斩!');
            break;
        // ... 其他判断
    }
}