Skip to content

战斗系统

ADVMaker 的战斗系统是一套完整的回合制战斗框架,支持自定义敌方、技能、行动顺序和胜负条件。你可以通过组件 DSL<ABattle>)或 JS APIAdv.startBattle())两种方式来触发战斗。


整体架构

[故事脚本]                  [战斗子系统]
    │                            │
    ├─ <ABattle> ───→ ABattleBase ──→ Adv.startBattle()
    │                     │              │
    │                     │      ┌───────┴────────┐
    │                     │      │  battleStore    │
    │                     │      │  ├─ isBattle    │
    │                     │      │  ├─ setting     │
    │                     │      │  ├─ queue       │
    │                     │      │  ├─ log[]       │
    │                     │      │  └─ state       │
    │                     │      └───────┬────────┘
    │                     │              │
    │                     │      ┌───────┴────────┐
    │                     │      │   Battle.vue   │
    │                     │      │  (覆盖 Message) │
    │                     │      └───────┬────────┘
    │                     │              │
    │              ┌──────┴──────┐       │
    │              │ Promise<boolean|'flee'>
    │              │  true  = win │       │
    │              │  false = lose│       │
    │              │  'flee'=逃跑  │       │
    │              └──────┬──────┘       │
    │                     │              │
    ├─ #success ←── true ─┘              │
    ├─ #fail    ←── false ───────────────┘
    ├─ #flee    ←── 'flee' ──────────────┘

关键设计点

  • 战斗期间 Message 被隐藏,Battle.vue 取而代之
  • 战斗结果有三种:胜利、失败、逃跑
  • 回合循环通过递归调用 next() 驱动

核心类型

ADVUserEnemy — 敌方配置

定义敌方的基础属性、技能和 AI 行为。

ts
interface ADVUserEnemy {
    name: string; // 名称
    desc?: string; // 描述
    hp: number; // 当前生命值
    maxhp?: number; // 最大生命值(默认 Infinity)
    atk?: number; // 攻击力(默认 0)
    def?: number; // 防御力(默认 0)
    dex?: number; // 敏捷(默认 0,用于先攻排序)
    skill: ADVUserSkill<ADVEnemy>[]; // 技能列表
    /**
     * 敌方回合的 AI 行为
     * @param enemies 所有敌人实例数组
     * 执行完毕后,系统会自动推进到下一个行动者
     */
    move: (enemies: ADVEnemy[]) => Promise<void>;
}

ADVUserSkill — 技能定义

定义攻击、技能或特殊行动。

ts
interface ADVUserSkill<T = ADVEnemy[]> {
    name: string; // 技能名称
    desc: string; // 详细描述
    summary?: string; // 简短摘要
    targetNum?: number; // 目标选择数量(Infinity = 全体)
    /**
     * 技能执行回调。
     *
     * ⚠️ T 的类型取决于触发者:
     * - **玩家**触发(ATKActions / SPActions)
     *   → T = ADVEnemy[],object = 弹窗选中的所有敌人
     * - **敌人 A**触发(enemy.skill)
     *   → T = ADVEnemy,object = 敌人 A 自身(用于读取 atk 等属性计算伤害)
     */
    onUse?: (object: T) => void;
}

ADVUserBattle — 战斗配置

组装一场完整战斗所需的所有配置。

ts
interface ADVUserBattle {
    /** 敌方阵容 */
    enemies: ADVUserEnemy[];
    /** 普通攻击选项(点击"攻击"时弹出) */
    ATKActions: ADVUserSkill[];
    /** 技能选项(点击"技能"时弹出,可选) */
    SPActions?: ADVUserSkill[];
    /** 特殊行动选项(可选) */
    otherActions?: ADVUserSkill[];

    /**
     * 决定先攻序列
     * @param enemies 运行时敌方实例数组
     * @returns 行动顺序数组,元素为敌方在数组中的下标,
     *          -1 代表玩家。越靠前越先行动。
     */
    initiativeOrder: (enemies: ADVEnemy[]) => number[];

    /**
     * 判断战斗是否结束
     * @param enemies 当前所有敌方实例
     * @returns null 继续 | true 胜利 | false 失败
     */
    isFinish: (enemies: ADVEnemy[]) => boolean | null;
}

组件方式:ABattle

<ABattle> 使用 AIf/AElif/AElse 实现三分支(胜利/失败/逃跑)。

基本用法

html
<ADialog>
    <ALine>一只哥布林挡住了去路!</ALine>

    <ABattle :setting="battleConfig">
        <template #success>
            <ALine>🎉 你击败了哥布林!</ALine>
        </template>
        <template #fail>
            <ALine>💀 你被哥布林打倒了...</ALine>
            <AEnding desc="败北" />
        </template>
        <template #flee>
            <ALine>🏃 你成功逃离了战斗。</ALine>
        </template>
    </ABattle>
</ADialog>

<script setup lang="ts">
    import { ADVUserBattle, ADVUserSkill } from '@advmaker/core';

    const battleConfig = new ADVUserBattle({
        enemies: [
            {
                name: '哥布林',
                desc: '森林中常见的小怪物',
                hp: 30,
                maxhp: 30,
                atk: 8,
                def: 4,
                dex: 10,
                skill: [],
                move: async (enemies) => {
                    Adv.print(`${enemies[0].name} 向你发动攻击!`);
                },
            },
        ],
        ATKActions: [
            new ADVUserSkill({
                name: '斩击',
                desc: '用武器劈向敌人',
                summary: '基础攻击',
                targetNum: 1,
                onUse: (targets) => {
                    targets[0].hp -= 10;
                    Adv.print(`对 ${targets[0].name} 造成 10 点伤害!`);
                },
            }),
        ],
        initiativeOrder: () => [-1, 0],
        isFinish: (enemies) => (enemies[0].hp <= 0 ? true : null),
    });
</script>

Props

属性类型说明
settingADVUserBattle战斗配置对象

插槽

插槽条件说明
#successisFinish() 返回 true战斗胜利后展示
#failisFinish() 返回 false战斗失败后展示
#flee玩家逃跑逃离战斗后展示

执行流程

  1. ABattle 挂载 → ABattleBase 向父级 ADialog 注册异步脚本项。
  2. 脚本执行到该项 → 调用 Adv.startBattle(setting)
  3. battleStore.isBattle = trueMessage 隐藏,Battle.vue 显示。
  4. initiativeOrder() 生成先攻序列,存入 queue
  5. 调用 battleStore.next(true) 启动回合循环:
    • 首次(first=true:直接执行第一个行动者,跳过 isFinish 和队列轮转
    • 后续:先检查 isFinish() → 若结束则 emit('battle-over', res);否则轮转队列 → 执行下一个
    • 玩家回合-1):state = 'player',显示行动按钮
    • 敌方回合>=0):state = 'enemy',调用 enemies[n].move(),执行完后递归 next()
  6. 战斗结束:
    • isFinish 返回 trueemit('battle-over', true)
    • isFinish 返回 falseemit('battle-over', false)
    • battleStore.flee() 被调用 → state = 'flee'emit('battle-over', 'flee')
  7. Promise 兑现,ABattle 渲染对应插槽。

JS API 方式:Adv.startBattle()

ts
import { ADVBattle, ADVUserSkill } from '@advmaker/core/data/model';

Adv.appendDialog('battle_scene')
    .say('一只哥布林挡住了去路!')
    .say(async () => {
        const setting = new ADVBattle({
            enemies: [
                {
                    name: '哥布林',
                    hp: 30,
                    maxhp: 30,
                    atk: 8,
                    def: 4,
                    dex: 10,
                    skill: [],
                    move: async () => {},
                },
            ],
            ATKActions: [
                new ADVUserSkill({
                    name: '斩击',
                    desc: '',
                    targetNum: 1,
                    onUse: (targets) => {
                        targets[0].hp -= 10;
                    },
                }),
            ],
            initiativeOrder: () => [-1, 0],
            isFinish: (e) => (e[0].hp <= 0 ? true : null),
        });
        const result = await Adv.startBattle(setting);
        if (result === true) Adv.print('🎉 胜利!');
        else if (result === false) Adv.print('💀 失败...');
        else Adv.print('🏃 逃跑了');
    });

函数签名

ts
Adv.startBattle(setting: ADVBattle): Promise<boolean | 'flee'>
返回值含义
true玩家胜利
false玩家失败
'flee'玩家逃跑

该方法会阻塞直到战斗结束。


逃跑机制

敌方 AI 中或按钮处理中调用 battleStore.flee() 即可触发逃跑:

ts
// Battle.vue 中逃跑按钮
battleStore.flee();

逃跑后流程:

  1. battleStore.state 设为 'flee'
  2. 下次 next() 检测到 state === 'flee' → 立即 emit('battle-over', 'flee')
  3. startBattle 的 Promise 以 'flee' 兑现
  4. ABattle 渲染 #flee 插槽

战斗 UI 组件

Battle.vue — 主战斗界面

位于 components/battle/Battle.vuebattleStore.isBattle = true 时替换 Message

┌──────────────────────────────────────┐
│  [敌人1卡片]  [敌人2卡片]             │  ← 点击查看详情
├──────────────────────────────────────┤
│  📜 战斗日志区域(battleStore.log[])  │
├──────────────────────────────────────┤
│  [攻击]  [技能]  [特殊行动]  [逃跑]    │
└──────────────────────────────────────┘

Enemy.vue — 敌人详情弹窗

点击敌人卡片弹出,显示完整属性(名称、描述、HP、攻防、技能列表)。

Action.vue — 行动选择弹窗

点击行动按钮后弹出可用技能列表:

  1. targetNum > 0,触发 choose-object 信号让玩家选目标
  2. 选中后执行 skill.onUse(targets) → 调用 battleStore.next()

信号参考

信号触发方参数返回说明
battle-overbattleStore.next()(res: boolean | 'flee')void战斗结束,传递结果
choose-objectAction.vue(num: number)Promise<ADVEnemy[]>玩家选择目标

完整示例

Boss 战(含多技能 + 逃跑):

html
<AShell>
    <AScene id="boss_fight" name="Boss 战">
        <ADialog>
            <ALine>暗影领主出现了!</ALine>

            <ABattle :setting="bossBattle">
                <template #success>
                    <ALine>🏆 击败了暗影领主!</ALine>
                    <ARun :run="() => Adv.bag.legendarySword += 1" />
                </template>
                <template #fail>
                    <ALine>💀 暗影领主太强大了...</ALine>
                    <AEnding desc="陨落" />
                </template>
                <template #flee>
                    <ALine>🏃 你逃出了 Boss 房间。</ALine>
                </template>
            </ABattle>
        </ADialog>
    </AScene>
</AShell>

<script setup lang="ts">
    import { ADVUserBattle, ADVUserSkill, Adv } from '@advmaker/core';

    const bossBattle = new ADVUserBattle({
        enemies: [
            {
                name: '暗影领主',
                desc: '掌控黑暗力量的强大存在',
                hp: 100,
                maxhp: 100,
                atk: 15,
                def: 10,
                dex: 12,
                skill: [
                    new ADVUserSkill({
                        name: '暗影箭',
                        desc: '发射黑暗能量',
                        summary: '造成大量伤害',
                        // 敌人技能 → object: ADVEnemy(敌人自身)
                        onUse: (self) => {
                            Adv.print(`${self.name} 释放了暗影箭!`);
                        },
                    }),
                ],
                move: async (enemies) => {
                    const boss = enemies[0];
                    if (Math.random() > 0.5 && boss.skill[0]) {
                        boss.skill[0].onUse(boss);
                    } else {
                        Adv.print(`${boss.name} 发动了普通攻击!`);
                    }
                },
            },
        ],
        ATKActions: [
            new ADVUserSkill({
                name: '圣光斩',
                desc: '用圣光之力劈砍',
                summary: '攻击力 × 1.5',
                targetNum: 1,
                onUse: (targets) => {
                    const dmg = 15 - targets[0].def * 0.5;
                    targets[0].hp -= dmg;
                    Adv.print(`造成 ${Math.round(dmg)} 点伤害!`);
                },
            }),
        ],
        SPActions: [
            new ADVUserSkill({
                name: '治疗术',
                desc: '恢复自身生命值',
                summary: '回复 20 HP',
                targetNum: 0,
                onUse: () => Adv.print('回复了 20 点生命值。'),
            }),
        ],
        initiativeOrder: (enemies) => [-1, ...enemies.keys()],
        isFinish: (enemies) => (enemies[0].hp <= 0 ? true : null),
    });
</script>