Files
pixelheros/docs/superpowers/specs/2026-10-06-boss-phase-system-design.md
panFD b149e6517b docs: add boss phase system design spec
Add design for WoW-style 3-phase boss combat: mixed triggers (hp threshold OR elapsed time), incremental skill adds/removes, BossPhaseComp marker component, and BossPhaseSystem with stub performance hooks. Plan ids use the 10000 id range. Reviewed by three sub-agents against ECS framework and combat chain sources.
2026-10-07 15:00:57 +08:00

331 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Boss 多阶段战斗系统设计(Boss Phase System)
> 日期:2026-10-06
> 状态:已评审(3 个子智能体审查通过,硬伤已修订)
> 关联文件:`heroSet.ts` / `RogueConfig.ts` / `Mon.ts` / `HeroAttrsComp.ts` / `GameEvent.ts`
## 1. 背景与目标
为 Boss 添加类似魔兽世界的多阶段战斗模式:
- **固定 3 个阶段**(P1/P2/P3),Boss 出生即 P1
- **混合触发**切换:血量阈值为主,战斗时长辅助(同一切换点内 OR 语义,任一满足即切)
- **混合式技能**:核心技能(普攻 + RogueConfig 注入的大招基线)贯穿全程,阶段技能增量轮换(adds/removes)
- 阶段切换的**表现层(演出/无敌/清场)留空钩子**,本次只搭骨架,后续迭代填充
## 2. 架构总览与数据流
方案选型:**增量驱动式**(方案 1)。每个切换点声明触发条件 + adds/removes + 属性修正,
切换 = BossPhaseSystem 增量改写 `model.skills`,与现有 `_testSkills` / `RogueConfig.ults`
动态注入模式完全一致,`SCastSystem` / `HeroAtkSystem` 零改动。
```
heroSet.ts (heroInfo) 【配置声明层】
└─ phase_ids: [10001, 10002] ← Boss 允许的阶段方案
↓ 生成时读取
RogueConfig.ts (StageSpawningEngine.makeBoss) 【消费层】
└─ 从 phase_ids 随机选 1 个 → GeneratedMonster.phase_plan_id
↓ 传递(唯一接线点 MissionMonComp.ts:224)
Mon.ts (load) 【初始化层】
└─ is_boss 且方案存在 → this.add(BossPhaseComp),index = 0
(P1 = 基线技能,无需注入)
↓ 每帧检测
BossPhaseSystem (新 ECS 系统) 【运行时层】
└─ filter: allOf(BossPhaseComp, HeroAttrsComp)
→ removes 移除 + adds 注入 + attr_mod 修正 + 资源预热
→ 空钩子 onPhaseExit/onPhaseEnter (TODO)
→ 广播 GameEvent.BossPhaseChange
```
关键决策:
- 新增轻量标记组件 `BossPhaseComp`:ECS matcher 按 allOf 过滤,只遍历带阶段方案的 Boss
- `HeroAttrsComp` 零新增字段(阶段状态全在 BossPhaseComp;属性修正直接改 ap/defense 现有字段)
- 计时下沉到组件(`BossPhaseComp.elapsed`),随 reset 天然归零,无全局状态
## 3. 配置层设计
### 3.1 新文件 `assets/script/game/common/config/BossPhaseSet.ts`
方案 id 使用 **10000 段**(已验证全项目空闲;9100 段被 TalentSet 9101-9199 与
ICardSet 9101~9110 双重占用,禁用)。
```ts
/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
export interface PhaseTrigger {
/** 血量百分比阈值(1~99):Boss 血量比例 ≤ 该值时触发 */
hp_pct?: number;
/** Boss 在场战斗秒数:超过该时长触发 */
time?: number;
}
/** 切换瞬间一次性属性修正(基于当前值链式乘算,(1 + pct/100)) */
export interface PhaseAttrMod {
ap_pct?: number; // 攻击 ±%
defense_pct?: number; // 防御 ±%(注意 applyDefense 除法递减收益非线性)
}
/** 注入技能条目(与 RogueConfig 大招条目同构) */
export interface PhaseSkillEntry {
s_uuid: number;
cd?: number; // 缺省 8s(强制兜底,防 NaN 污染 CD)
overrides?: SkillOverrides;
}
/** 单个切换点定义(P1→P2 或 P2→P3) */
export interface PhaseSwitchDef {
trigger: PhaseTrigger;
/** 注入的技能(追加进 skills 表,SCastSystem 按 CD 轮转)。禁止操作普攻首键 */
adds?: PhaseSkillEntry[];
/** 移除的技能 s_uuid 列表(含 RogueConfig 注入的大招)。禁止操作普攻首键 */
removes?: number[];
attr_mod?: PhaseAttrMod;
}
/** 阶段方案:固定 2 个切换点 = 3 阶段,P1 为基线 */
export interface BossPhasePlan {
name: string;
switches: [PhaseSwitchDef, PhaseSwitchDef];
}
/** 方案表:key 为方案 id(10000 段) */
export const BossPhaseSet: Record<number, BossPhasePlan> = {
10001: {
name: "狂暴进化",
switches: [
{ trigger: { hp_pct: 70 }, adds: [{ s_uuid: 6102, overrides: { ap: 180 } }], removes: [6100] },
{ trigger: { hp_pct: 40, time: 60 }, adds: [{ s_uuid: 6103 }], attr_mod: { ap_pct: 20 } },
],
},
};
```
`validateBossPhaseSet(): string[]` 校验规则:
1. 恰好 2 个切换点;每个 trigger 至少含一个条件
2. `hp_pct ∈ (0, 100)`;`time > 0`;多个 time 严格递增
3. **hp_pct 前缀规则**:hp_pct 切换点必须构成前缀——一旦出现 time-only 切换点,
其后的切换点禁止再携带 hp_pct(拦截"P2 time / P3 hp_pct=80"乱序连切)
4. 存在 hp_pct 的相邻切换点严格递减(70 > 40)
5. adds/removes 的 s_uuid 存在于 SkillSet(error 级)
6. removes 的 s_uuid 应在该 Boss 可达技能集内(heroInfo.skills ∪ 大招池 ∪ 前置 adds,warn 级)
### 3.2 `heroSet.ts` 改动(2 处)
1. `heroInfo` 接口新增可选字段(与 `dis?`/`bonus?` 风格一致):
```ts
/** Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一) */
phase_ids?: number[];
```
2. 在 6101~6106 真正的 Boss 配置上加示例:`phase_ids: [10001]`
(注意 6006 骷髅领主也在 BossPools 内但未配置 phase_ids,走兜底路径)
### 3.3 `RogueConfig.ts` 改动(4 处)
1. `GeneratedMonster` 接口加 `phase_plan_id?: number`
2. `makeBoss()`:`baseInfo.phase_ids` 非空数组时随机选 1 个写入,
否则 `undefined`(防 NaN:`arr?.length ? arr[rand] : undefined`)
3. `validateRogueConfig()` 追加调用 `validateBossPhaseSet()` + 校验
heroInfo.phase_ids 引用的方案存在
4. **顺带修复现存 bug**:`_testSkills` 池化残留(Boss 回池复用为小怪会继承大招)——
`MissionMonComp.spawnMonster` 改为无条件赋值 `(mon as any)._testSkills = monData.skills ?? undefined`
依赖方向:RogueConfig → BossPhaseSet → SkillSet,无循环导入。
## 4. 运行时层设计
### 4.1 新文件 `assets/script/game/hero/BossPhaseComp.ts`
```ts
/** Boss 阶段标记组件:挂载即表示该实体参与阶段状态机 */
@ecs.register('BossPhase')
export class BossPhaseComp extends ecs.Comp {
/** 生效的阶段方案 id(BossPhaseSet key) */
plan_id: number = 0;
/** 当前阶段索引:0=P1 / 1=P2 / 2=P3 */
index: number = 0;
/** Boss 在场战斗累计时长(秒),time 触发条件基准 */
elapsed: number = 0;
reset() { this.plan_id = 0; this.index = 0; this.elapsed = 0; }
}
```
**禁止缓存绝对血量阈值**:hp/ap 在 Mon.load 后被 MissionMonComp 外部覆盖
(MissionMonComp.ts L239-246),阶段系统每帧实时用 `model.hp / model.hp_max * 100` 比对。
### 4.2 `Mon.ts` 改动
1. `load()` 增加第 **8** 个可选参数 `phase_plan_id: number = 0`
(当前签名 7 参;唯一调用点 `MissionMonComp.ts:224` 同步追加实参)
2. 在技能表构建完成后:
```ts
// Boss 阶段初始化:P1 = 基线技能,仅挂标记组件并记录方案
if (is_boss && phase_plan_id > 0 && BossPhaseSet[phase_plan_id]) {
const phase = this.add(BossPhaseComp);
phase.plan_id = phase_plan_id;
phase.index = 0;
}
```
3. `destroy()` 中、`remove(HeroAttrsComp)` **之前**显式 `this.remove(BossPhaseComp)`
(触发 reset + 回组件池;ECS 组件无自动销毁钩子,必须手动 remove)
4. `reset()` 与 `destroy()` 对齐,同样补 remove(防旁路)
### 4.3 新文件 `assets/script/game/hero/BossPhaseSystem.ts`
```ts
/**
* Boss 阶段系统:检测混合触发条件(血量阈值 OR 在场时长)并执行阶段切换
* Why: 独立系统符合 M/V/S 分层;filter 按 BossPhaseComp 过滤,每局 Boss 唯一,开销可忽略
*/
@ecs.register('BossPhaseSystem')
export class BossPhaseSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
filter() { return ecs.allOf(BossPhaseComp, HeroAttrsComp); }
/** 框架自动遍历匹配实体逐个回调;dt 从 this.dt 取(秒) */
update(e: ecs.Entity): void {
const phase = e.get(BossPhaseComp);
const model = e.get(HeroAttrsComp);
// 守卫:非战斗/暂停/已死亡跳过(死亡同帧不切阶段,防死尸换阶段)
if (!smc.mission.in_fight || smc.mission.pause) return;
if (model.is_dead) return;
// P3 后不再检测
if (phase.index >= 2) return;
phase.elapsed += this.dt;
const plan = BossPhaseSet[phase.plan_id];
if (!plan) return;
const def = plan.switches[phase.index];
// 混合触发:任一条件满足即切换(血量实时计算,不缓存绝对值)
const hpPct = model.hp / model.hp_max * 100;
const hpHit = def.trigger.hp_pct !== undefined && hpPct <= def.trigger.hp_pct;
const timeHit = def.trigger.time !== undefined && phase.elapsed >= def.trigger.time;
if (hpHit || timeHit) {
this.executePhaseSwitch(e, def);
}
}
private executePhaseSwitch(e: ecs.Entity, def: PhaseSwitchDef) {
const phase = e.get(BossPhaseComp)!;
const model = e.get(HeroAttrsComp)!;
const from = phase.index;
this.onPhaseExit(e); // 空钩子:TODO 阶段退场演出
// 1. 移除旧技能(仅大招键,绝不操作普攻首键——键序契约见 §5)
if (def.removes) {
for (const s_uuid of def.removes) delete model.skills[s_uuid];
}
// 2. 注入新技能(cd 强制兜底 8s,防 undefined → NaN 污染 CD)
if (def.adds) {
for (const add of def.adds) {
if (!add || !add.s_uuid || !SkillSet[add.s_uuid]) continue;
model.skills[add.s_uuid] = {
uuid: add.s_uuid,
lv: 1,
cd: add.cd ?? 8,
ccd: 0,
overrides: add.overrides,
};
// 资源预热:新技能 prefab 首次异步加载会"丢一发",切换时预加载
// 路径格式与 Skill.ts L112 对齐(game/skill/atk/{sp_name},Prefab 类型)
const path = `game/skill/atk/${SkillSet[add.s_uuid].sp_name}`;
oops.res.load(path, Prefab);
}
}
// 3. 属性修正:基于当前值链式乘算(P3 基于 P2 后的值)
const mod = def.attr_mod;
if (mod) {
if (mod.ap_pct) model.ap = Math.round(model.ap * (1 + mod.ap_pct / 100));
if (mod.defense_pct) model.defense = Math.round(model.defense * (1 + mod.defense_pct / 100));
}
phase.index++;
this.onPhaseEnter(e); // 空钩子:TODO 阶段入场演出(无敌/清场/横幅后续在此实现)
// 广播:oops.message.dispatchEvent(GameEvent.BossPhaseChange, payload)
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity) { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity) { }
}
```
要点:
- `@ecs.register` 装饰器在模块加载时自动注册系统(App 级单例),
**唯一要求:BossPhaseSystem.ts 被 import**(从 Mon.ts import BossPhaseComp 的同文件链自然带动,
实施时确认 import 链可达)
- 不使用系统级 battleTime(per-entity update 中累加会乘 N 倍),计时在组件 elapsed
### 4.4 `GameEvent.ts` 新增枚举(近期 PascalCase 惯例)
```ts
/** Boss 阶段切换(data: { eid: number, planId: number, fromPhase: number, toPhase: number }),表现层监听此事件实现演出 */
BossPhaseChange = "BossPhaseChange",
```
payload 用 eid 定位实体(防裸引用已回收实体);监听方按项目规范在 onDestroy 中注销。
### 4.5 TestMode 调试通道
`TestModeConfig` 加 `phasePlanId: number = 0` 与 `isBoss: boolean = false`;
`generateTestWave()` 的 isBoss 硬编码 false(RogueConfig.ts L369)同步改为读配置,
并将 phasePlanId 传入 GeneratedMonster——否则调试开关配了也不生效。
## 5. 明示的设计决策(避免 QA 争议)
| 决策 | 说明 |
|------|------|
| ccd: 0 语义 | 切换后需等满一个完整 CD 才首次施放新技能(非立即轰) |
| attr_mod 口径 | 链式复利:P3 基于 P2 修正后的当前值乘算,配置时按此口径 |
| 前摇交叉语义 | 切换瞬间已在空的弹道按出手快照结算(SDataCom 值拷贝机制天然保证),可能出现"P1 技能携带 P2 攻击力落地",属可接受行为 |
| skills 键序契约 | JS `delete+set` 会把键移到对象末尾,而 `buildSkillCandidates`(SCastSystem.ts L627-631)硬假设首键=普攻垫底。**removes/adds 绝不允许操作普攻首键**(校验 + 注释双重约束);若未来需换普攻,必须整体重建 skills 表保证首键 |
| speed 不修正 | `HeroAttrsComp.speed` 是移速(仅 MoveComp 消费),攻速走 BuffComp 聚合通道。attr_mod 不含 speed_pct;攻速强化留给后续 buff 通道迭代 |
| hp_max 不修正 | 避免 hp 同步策略(保百分比/保绝对值/回满)问题,后续需要时单独设计 |
| 触发技不阶段化 | atking/atked/dead 等存于 runtime_* 缓存(独立于 model.skills),本次不覆盖,列为后续迭代 |
| 施法不打断 | 切换只改 model.skills;施法闭包捕获 SkillSet 静态配置 + 伤害走值拷贝快照,进行中的施法/飞行弹道不受影响 |
## 6. 错误处理与校验
- **运行时兜底**:BossPhaseSet 查不到方案 → 不挂组件,Boss 退化为单阶段(现状行为),`oops.log.warn`
- **校验**:`validateBossPhaseSet()` 规则见 §3.1;`validateRogueConfig()` 追加调用(启动期暴露配置错误)
- **对象池复用安全**:BossPhaseComp 随 destroy 手动 remove → reset 归零 → 回组件池;
`_testSkills` 残留 bug 顺带修复(§3.3 第 4 条)
## 7. 测试方案
1. **单阶段回归**:不配 phase_ids 的 Boss(如 6006)行为与现状完全一致
2. **阶段切换**:TestMode 配 `isBoss: true` + `phasePlanId: 10001` + 低血量阈值,
打 Boss 观察切换调试日志(`phase P1→P2`,受 debugMode 控制)
3. **time 触发**:高防阵容拖时间验证 time 条件独立生效
4. **乱序拦截**:手工配"P2 time-only / P3 hp_pct"方案,确认 validate 报错
5. **池化复用**:Boss 死亡后连续刷普通怪,确认小怪不继承 Boss 大招(_testSkills 修复验证)
6. **键序**:Boss 切换后普攻仍垫底、大招按 CD 轮转正常
## 8. 文件清单
| 文件 | 操作 | 内容 |
|------|------|------|
| `assets/script/game/common/config/BossPhaseSet.ts` | 新建 | 方案表 + 类型 + validate |
| `assets/script/game/hero/BossPhaseComp.ts` | 新建 | 标记组件 |
| `assets/script/game/hero/BossPhaseSystem.ts` | 新建 | 检测与切换系统 + 空钩子 |
| `assets/script/game/common/config/heroSet.ts` | 修改 | heroInfo 加 phase_ids 字段 + Boss 示例配置 |
| `assets/script/game/map/RogueConfig.ts` | 修改 | GeneratedMonster.phase_plan_id + makeBoss 消费 + validate 追加 + TestMode 通道 |
| `assets/script/game/map/MissionMonComp.ts` | 修改 | load 传参接线 + _testSkills 残留修复 |
| `assets/script/game/hero/Mon.ts` | 修改 | 第 8 参 + BossPhaseComp 挂载/移除 |
| `assets/script/game/common/config/GameEvent.ts` | 修改 | BossPhaseChange 枚举 |
## 9. 后续迭代(本次不做)
- 阶段切换演出:onPhaseExit/onPhaseEnter 钩子填充(无敌帧、清场、怒吼、横幅)
- 触发技(atking/atked/dead/call)阶段化:runtime_* 缓存重建通道
- 攻速强化:走 buffManager timed buff(Attrs.speed 修饰器)通道
- 阶段专属索敌/移动行为(如 P3 召唤小怪、冲锋走位)
- 换普攻支持:skills 整表重建保证首键契约