From b149e6517b4601404d2dcd99019b03f281eb39b0 Mon Sep 17 00:00:00 2001 From: panFD Date: Wed, 7 Oct 2026 15:00:57 +0800 Subject: [PATCH] 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-06-boss-phase-system-design.md | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-06-boss-phase-system-design.md diff --git a/docs/superpowers/specs/2026-10-06-boss-phase-system-design.md b/docs/superpowers/specs/2026-10-06-boss-phase-system-design.md new file mode 100644 index 00000000..40c5731b --- /dev/null +++ b/docs/superpowers/specs/2026-10-06-boss-phase-system-design.md @@ -0,0 +1,330 @@ +# 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 = { + 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 整表重建保证首键契约