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

16 KiB
Raw Blame History

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 双重占用,禁用)。

/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
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? 风格一致):
/** Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一) */
phase_ids?: number[];
  1. 在 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

/** 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. 在技能表构建完成后:
// 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;
}
  1. destroy() 中、remove(HeroAttrsComp) 之前显式 this.remove(BossPhaseComp) (触发 reset + 回组件池;ECS 组件无自动销毁钩子,必须手动 remove)
  2. reset() 与 destroy() 对齐,同样补 remove(防旁路)

4.3 新文件 assets/script/game/hero/BossPhaseSystem.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 惯例)

/** 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 整表重建保证首键契约