战斗系统
ADVMaker 的战斗系统是一套完整的回合制战斗框架,支持自定义敌方、技能、行动顺序和胜负条件。你可以通过组件 DSL(<ABattle>)或 JS API(Adv.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
| 属性 | 类型 | 说明 |
|---|---|---|
setting | ADVUserBattle | 战斗配置对象 |
插槽
| 插槽 | 条件 | 说明 |
|---|---|---|
#success | isFinish() 返回 true | 战斗胜利后展示 |
#fail | isFinish() 返回 false | 战斗失败后展示 |
#flee | 玩家逃跑 | 逃离战斗后展示 |
执行流程
ABattle挂载 →ABattleBase向父级ADialog注册异步脚本项。- 脚本执行到该项 → 调用
Adv.startBattle(setting)。 battleStore.isBattle = true→Message隐藏,Battle.vue显示。initiativeOrder()生成先攻序列,存入queue。- 调用
battleStore.next(true)启动回合循环:- 首次(
first=true):直接执行第一个行动者,跳过isFinish和队列轮转 - 后续:先检查
isFinish()→ 若结束则emit('battle-over', res);否则轮转队列 → 执行下一个 - 玩家回合(
-1):state = 'player',显示行动按钮 - 敌方回合(
>=0):state = 'enemy',调用enemies[n].move(),执行完后递归next()
- 首次(
- 战斗结束:
isFinish返回true→emit('battle-over', true)isFinish返回false→emit('battle-over', false)battleStore.flee()被调用 →state = 'flee'→emit('battle-over', 'flee')
- 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();逃跑后流程:
battleStore.state设为'flee'- 下次
next()检测到state === 'flee'→ 立即emit('battle-over', 'flee') startBattle的 Promise 以'flee'兑现ABattle渲染#flee插槽
战斗 UI 组件
Battle.vue — 主战斗界面
位于 components/battle/Battle.vue,battleStore.isBattle = true 时替换 Message。
┌──────────────────────────────────────┐
│ [敌人1卡片] [敌人2卡片] │ ← 点击查看详情
├──────────────────────────────────────┤
│ 📜 战斗日志区域(battleStore.log[]) │
├──────────────────────────────────────┤
│ [攻击] [技能] [特殊行动] [逃跑] │
└──────────────────────────────────────┘Enemy.vue — 敌人详情弹窗
点击敌人卡片弹出,显示完整属性(名称、描述、HP、攻防、技能列表)。
Action.vue — 行动选择弹窗
点击行动按钮后弹出可用技能列表:
- 若
targetNum > 0,触发choose-object信号让玩家选目标 - 选中后执行
skill.onUse(targets)→ 调用battleStore.next()
信号参考
| 信号 | 触发方 | 参数 | 返回 | 说明 |
|---|---|---|---|---|
battle-over | battleStore.next() | (res: boolean | 'flee') | void | 战斗结束,传递结果 |
choose-object | Action.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>