Files
pixelheros/docs/superpowers/plans/2026-10-06-boss-phase-system.md
panFD 71db1c24b0 docs: add boss phase system implementation plan
Nine bite-sized tasks covering config schema, marker component, phase switch system, generation wiring, stale test-skill fix, and editor-based runtime verification. Full code in every step, no placeholders.
2026-10-07 15:06:32 +08:00

798 lines
29 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 多阶段战斗系统实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 Boss 实现固定 3 阶段(P1/P2/P3)战斗模式:混合触发(血量阈值 OR 在场时长)、增量技能轮换(adds/removes)、属性修正,表现层留空钩子。
**Architecture:** 增量驱动式——配置层 `BossPhaseSet`(10000 段 id)声明切换点,`heroInfo.phase_ids` 声明 Boss 允许的方案,`RogueConfig.makeBoss` 随机取一注入 `GeneratedMonster.phase_plan_id`,`Mon.load` 按需挂载 `BossPhaseComp` 标记组件,`BossPhaseSystem`(独立 ECS 系统)检测条件并增量改写 `model.skills`。`SCastSystem`/`HeroAtkSystem` 零改动。
**Tech Stack:** Cocos Creator 3.x + TypeScript 严格模式 + oops-framework ECS + `oops.message` 事件总线
**规格文档:** `docs/superpowers/specs/2026-10-06-boss-phase-system-design.md`
**验证方式:** 项目无单元测试框架,每任务完成后在 Cocos Creator 编辑器中触发编译检查控制台无红色报错;Task 9 做运行时验证。
**已知约束(来自审查,实施时必须遵守):**
- `ComblockSystem.update` 真实签名为 `update(e: ecs.Entity)`,dt 从 `this.dt` 取,框架自动遍历
- removes/adds 绝不操作普攻首键(`buildSkillCandidates` 硬假设首键=普攻)
- cd 强制兜底 8s(防 `ccd = Math.min(undefined,...) = NaN` 导致大招永不释放)
- 血量阈值每帧实时算 `hp/hp_max*100`,禁止缓存绝对值(hp_max 在 load 后被 MissionMonComp 覆盖)
- `heroSet.ts` 工作区有用户未提交修改,只做精准行内插入,勿动其他行
---
### Task 1: 新建 BossPhaseSet.ts(配置 + 类型 + 校验)
**Files:**
- Create: `assets/script/game/common/config/BossPhaseSet.ts`
- [ ] **Step 1: 创建配置文件**
```ts
/**
* @file BossPhaseSet.ts
* @description Boss 多阶段战斗方案配置(固定 3 阶段:P1 基线 / P2 / P3)
*
* 数据流:heroInfo.phase_ids(允许方案)→ RogueConfig.makeBoss 随机取一
* → Mon.load 第 8 参 → BossPhaseComp → BossPhaseSystem 检测切换
*
* 设计约束:
* - 方案 id 使用 10000 段(9100 段已被 TalentSet 9101-9199 / ICardSet 9101~9110 占用)
* - 增量驱动:切换 = removes 移除 + adds 注入,P1 = 基线技能(heroInfo.skills + RogueConfig 大招)无需配置
* - removes/adds 禁止操作普攻首键(SCastSystem.buildSkillCandidates 硬假设首键=普攻垫底;
* JS delete+set 会把键移到末尾,破坏普攻垫底契约)
* - 混合触发:同一切换点 hp_pct / time 任一满足即切换;
* hp_pct 切换点必须构成前缀(time-only 点之后禁止再配 hp_pct,防乱序连切)
*/
import { SkillSet, SkillOverrides } from "./SkillSet";
/** 阶段切换触发条件(混合触发:任一条件满足即切换) */
export interface PhaseTrigger {
/** 血量百分比阈值(1~99):Boss 血量比例 ≤ 该值时触发 */
hp_pct?: number;
/** Boss 在场战斗秒数:超过该时长触发 */
time?: number;
}
/** 切换瞬间一次性属性修正(基于当前值链式乘算,P3 基于 P2 修正后的值) */
export interface PhaseAttrMod {
/** 攻击 ±% */
ap_pct?: number;
/** 防御 ±%(applyDefense 除法递减模型,收益非线性,配值按公式校验) */
defense_pct?: number;
}
/** 注入技能条目(与 RogueConfig 大招条目同构,cd 缺省由系统兜底 8s) */
export interface PhaseSkillEntry {
s_uuid: number;
cd?: number;
overrides?: SkillOverrides;
}
/** 单个切换点定义(switches[0] = P1→P2,switches[1] = 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];
}
/**
* 阶段方案表(id 段 10000)
* 示例方案 10001 挂在 6101(兽人首领-双刀dps型)的 phase_ids 上;
* 大招 s_uuid 引用 RogueConfig.BossUltPools 近战池(6100~6104)保证可达
*/
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, overrides: { ap: 180 } }], attr_mod: { ap_pct: 20 } },
],
},
};
/**
* 校验阶段方案表合法性(供 validateRogueConfig 追加调用,建议启动期执行一次)
* @returns 错误信息数组,空数组表示校验通过
*/
export function validateBossPhaseSet(): string[] {
const errors: string[] = [];
for (const key in BossPhaseSet) {
const plan = BossPhaseSet[key];
const tag = `BossPhaseSet[${key}](${plan.name})`;
if (plan.switches.length !== 2) {
errors.push(`${tag} 切换点必须恰为 2 个(固定 3 阶段),实际 ${plan.switches.length}`);
continue;
}
let seenTimeOnly = false;
let lastHpPct = 101;
let lastTime = 0;
plan.switches.forEach((def, i) => {
const t = def.trigger;
const stag = `${tag} switches[${i}]`;
// 条件完备性与取值范围
if (t.hp_pct === undefined && t.time === undefined) {
errors.push(`${stag} trigger 至少需要 hp_pct 或 time 之一`);
return;
}
if (t.hp_pct !== undefined && (t.hp_pct <= 0 || t.hp_pct >= 100)) {
errors.push(`${stag} hp_pct 必须在 (0,100) 区间,实际 ${t.hp_pct}`);
}
if (t.time !== undefined && t.time <= 0) {
errors.push(`${stag} time 必须 > 0,实际 ${t.time}`);
}
// hp_pct 前缀规则:time-only 点之后禁止再配 hp_pct(防"P2 time / P3 hp_pct"乱序连切)
if (seenTimeOnly && t.hp_pct !== undefined) {
errors.push(`${stag} hp_pct 切换点必须构成前缀:time-only 切换点之后禁止再配置 hp_pct`);
}
if (t.hp_pct === undefined && t.time !== undefined) {
seenTimeOnly = true;
}
// hp_pct 严格递减 / time 严格递增
if (t.hp_pct !== undefined) {
if (t.hp_pct >= lastHpPct) {
errors.push(`${stag} hp_pct=${t.hp_pct} 未严格小于前一档 ${lastHpPct}`);
}
lastHpPct = t.hp_pct;
}
if (t.time !== undefined) {
if (t.time <= lastTime) {
errors.push(`${stag} time=${t.time} 未严格大于前一档 ${lastTime}`);
}
lastTime = t.time;
}
// 技能引用存在性(error 级)
for (const add of def.adds ?? []) {
if (!add.s_uuid || !SkillSet[add.s_uuid]) {
errors.push(`${stag} adds 技能 ${add.s_uuid} 未在 SkillSet 中定义`);
}
}
for (const s_uuid of def.removes ?? []) {
if (!SkillSet[s_uuid]) {
errors.push(`${stag} removes 技能 ${s_uuid} 未在 SkillSet 中定义`);
}
}
});
}
return errors;
}
```
- [ ] **Step 2: 编译验证**
在 Cocos Creator 编辑器中保存文件触发编译(或等编辑器自动编译),检查控制台无红色报错。
重点确认:`SkillSet` 与 `SkillOverrides` 均从 `./SkillSet` 导出(RogueConfig.ts L24 已有同款 import 先例)。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/common/config/BossPhaseSet.ts
git commit -m "feat: add BossPhaseSet config with phase plan schema"
```
---
### Task 2: heroSet.ts 加 phase_ids 字段与示例配置
**Files:**
- Modify: `assets/script/game/common/config/heroSet.ts:432`(接口字段,accuracy 之后)
- Modify: `assets/script/game/common/config/heroSet.ts:1095`(6101 配置行尾)
- [ ] **Step 1: heroInfo 接口新增字段**
在 L432 `accuracy?: number;` 行之后、L433 `skills: Record<number, HSkillInfo>;` 之前插入:
```ts
phase_ids?: number[]; // Boss 专属:允许的阶段方案 id 列表(引用 BossPhaseSet,生成时随机取一)
```
- [ ] **Step 2: 6101 配置挂载示例方案**
在 L1095(`uuid: 6101, ... speed: 120,` 行)的 `speed: 120,` 之后、行尾前追加:
```ts
phase_ids: [10001],
```
即该行变为(仅尾部变化,其余内容原样保留):
```ts
uuid: 6101, name: "兽人首领-双刀dps型", path: "mb1", fac: FacSet.MON, lv: 6, rarity: 4, type: HType.Melee, grow_type: HGrowType.Warrior, role: HRole.Warrior, monType: MonType.MeleeBoss, hp: HERO_ROLE_BASE[HRole.Warrior].hp, ap: HERO_ROLE_BASE[HRole.Warrior].ap, def: HERO_ROLE_BASE[HRole.Warrior].def, speed: 120, phase_ids: [10001],
```
注意:该文件工作区有用户未提交修改,只做上述两处精准插入,不改动任何其他行。
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/common/config/heroSet.ts
git commit -m "feat: add phase_ids field to boss hero config"
```
---
### Task 3: GameEvent.ts 加 BossPhaseChange 枚举
**Files:**
- Modify: `assets/script/game/common/config/GameEvent.ts:118`(RestartMission 之后)
- [ ] **Step 1: 新增枚举项**
在 L118 `RestartMission = "RestartMission",` 之后、枚举闭合 `}` 之前插入:
```ts
/** Boss 阶段切换(payload: { eid, planId, fromPhase, toPhase },表现层监听此事件实现演出;eid 定位实体,勿持裸引用) */
BossPhaseChange = "BossPhaseChange",
```
- [ ] **Step 2: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/common/config/GameEvent.ts
git commit -m "feat: add BossPhaseChange game event"
```
---
### Task 4: 新建 BossPhaseComp.ts(标记组件)
**Files:**
- Create: `assets/script/game/hero/BossPhaseComp.ts`
- [ ] **Step 1: 创建组件文件**
```ts
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
/**
* Boss 阶段标记组件:挂载即表示该实体参与多阶段状态机
* 由 Mon.load 按 phase_plan_id 条件挂载,Monster.destroy 显式 remove(触发 reset + 回组件池)
* 计时字段 elapsed 随 reset 天然归零,无全局战斗计时状态
*/
@ecs.register('BossPhase')
export class BossPhaseComp extends ecs.Comp {
/** 生效的阶段方案 id(BossPhaseSet key) */
plan_id: number = 0;
/** 当前阶段索引:0=P1 / 1=P2 / 2=P3(P3 后系统不再检测) */
index: number = 0;
/** Boss 在场战斗累计时长(秒),time 触发条件基准 */
elapsed: number = 0;
reset() {
this.plan_id = 0;
this.index = 0;
this.elapsed = 0;
}
}
```
- [ ] **Step 2: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。重点确认 `ecs.Comp` 的 `reset()` 为抽象方法必须实现(与 HeroAttrsComp 同款模式)。
- [ ] **Step 3: Commit**
```bash
git add assets/script/game/hero/BossPhaseComp.ts
git commit -m "feat: add BossPhaseComp marker component"
```
---
### Task 5: Mon.ts 接入(load 第 8 参 + 组件挂载/移除)
**Files:**
- Modify: `assets/script/game/hero/Mon.ts:1`(import 区)
- Modify: `assets/script/game/hero/Mon.ts:137`(load 签名)
- Modify: `assets/script/game/hero/Mon.ts:265`(挂载点,updateSkillDistanceCache 之后)
- Modify: `assets/script/game/hero/Mon.ts:126`(destroy 移除)
- Modify: `assets/script/game/hero/Mon.ts:358`(reset 对齐)
- [ ] **Step 1: 新增 import**
在 L12 `import { SkillTriggerHelper } from "./SkillTriggerHelper";` 之后插入:
```ts
import { BossPhaseSet } from "../common/config/BossPhaseSet";
import { BossPhaseComp } from "./BossPhaseComp";
```
- [ ] **Step 2: load 签名追加第 8 参**
L137 改为(仅尾部追加 `phase_plan_id: number = 0`):
```ts
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, is_boss: boolean = false, dropToY: number = pos.y, mon_lv: number = 1, laneIndex: number = 0, phase_plan_id: number = 0) {
```
- [ ] **Step 3: 技能表构建完成后挂载组件**
在 L265 `model.updateSkillDistanceCache();` 之后、L267 `// 注册视图组件并重置对象池复用状态` 注释之前插入:
```ts
// Boss 阶段初始化:P1 = 基线技能,仅挂标记组件并记录方案(方案缺失则退化为单阶段 Boss)
if (is_boss && phase_plan_id > 0) {
const plan = BossPhaseSet[phase_plan_id];
if (plan) {
const phase = this.add(BossPhaseComp);
phase.plan_id = phase_plan_id;
phase.index = 0;
} else {
// 配置兜底:引用了不存在的方案,退化为单阶段并告警(不阻断战斗)
oops.log.warn(`[BossPhase] 阶段方案 ${phase_plan_id} 未在 BossPhaseSet 中定义,Boss 按单阶段战斗`);
}
}
```
- [ ] **Step 4: destroy 显式移除组件(触发 reset + 回组件池)**
在 L126 `this.remove(HeroViewComp);` 之前插入(放 HeroAttrsComp 移除前,便于将来清理逻辑读属性):
```ts
// 阶段组件显式移除:触发 reset 清空状态并回组件池(ECS 组件无自动销毁钩子)
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
```
- [ ] **Step 5: reset 与 destroy 对齐(防旁路残留)**
L358-360 改为:
```ts
/** 重置入口:复用 destroy 的释放流程 */
reset() {
if (this.has(BossPhaseComp)) {
this.remove(BossPhaseComp);
}
super.destroy();
}
```
- [ ] **Step 6: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。此时 BossPhaseSystem 尚未创建,组件挂载后无消费方,属预期中间态。
- [ ] **Step 7: Commit**
```bash
git add assets/script/game/hero/Mon.ts
git commit -m "feat: wire BossPhaseComp into monster load/destroy"
```
---
### Task 6: 新建 BossPhaseSystem.ts(检测与切换系统)
**Files:**
- Create: `assets/script/game/hero/BossPhaseSystem.ts`
- [ ] **Step 1: 创建系统文件**
```ts
/**
* BossPhaseSystem — Boss 多阶段战斗状态机。
*
* 检测混合触发条件(血量阈值 OR 在场时长),命中后执行阶段切换:
* removes 移除旧技能 → adds 注入新技能(cd 兜底 + 资源预热)→ attr_mod 属性修正
* → 空钩子 onPhaseExit/onPhaseEnter(演出后续迭代)→ 广播 GameEvent.BossPhaseChange
*
* Why: 独立系统符合 M/V/S 分层;filter 按 BossPhaseComp 过滤,每局 Boss 唯一,开销可忽略。
* 计时在组件 elapsed(随 reset 归零),避免系统级计数在 per-entity update 中重复累加。
*/
import { Prefab } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { smc } from "../common/SingletonModuleComp";
import { GameEvent } from "../common/config/GameEvent";
import { BossPhaseSet, PhaseSwitchDef } from "../common/config/BossPhaseSet";
import { SkillSet } from "../common/config/SkillSet";
import { mLogger } from "../common/Logger";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { BossPhaseComp } from "./BossPhaseComp";
/** 阶段模块调试日志开关 */
const DEBUG = true;
/** 新注入技能缺省 CD(秒),与 Mon.ts 大招注入兜底一致 */
const DEFAULT_ADD_CD = 8;
@ecs.register('BossPhaseSystem')
export class BossPhaseSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
filter(): ecs.IMatcher {
return ecs.allOf(BossPhaseComp, HeroAttrsComp);
}
update(e: ecs.Entity): void {
const phase = e.get(BossPhaseComp);
const model = e.get(HeroAttrsComp);
if (!phase || !model) return;
// 战斗状态守卫(与 MoveComp/SCastSystem 同款模式):暂停/未开战不计时
if (!smc.mission.play || smc.mission.pause) return;
// 死亡同帧守卫:防死尸换阶段
if (model.is_dead) return;
// P3 后不再检测(固定 3 阶段)
if (phase.index >= 2) return;
const plan = BossPhaseSet[phase.plan_id];
if (!plan) return;
phase.elapsed += this.dt;
const def = plan.switches[phase.index];
// 混合触发:任一条件满足即切换(血量实时计算,禁止缓存绝对值——hp_max 在 load 后被外部覆盖)
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);
}
}
/**
* 执行阶段切换:技能增量改写 + 属性修正 + 钩子与事件广播
* Why 改 skills 而非施法系统过滤:与 _testSkills/RogueConfig.ults 动态注入同模式,
* SCastSystem 零改动;施法闭包捕获 SkillSet 静态配置,进行中施法不受影响
*/
private executePhaseSwitch(e: ecs.Entity, def: PhaseSwitchDef): void {
const phase = e.get(BossPhaseComp)!;
const model = e.get(HeroAttrsComp)!;
const from = phase.index;
this.onPhaseExit(e);
// 1. 移除旧技能(仅大招键;普攻首键受键序契约保护,见 BossPhaseSet 文件头注释)
if (def.removes) {
for (const s_uuid of def.removes) {
delete model.skills[s_uuid];
}
}
// 2. 注入新技能(cd 强制兜底,防 undefined → Math.min 结果 NaN 导致大招永不释放)
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 ?? DEFAULT_ADD_CD,
ccd: 0,
overrides: add.overrides,
};
// 资源预热:新技能 prefab 首次异步加载会丢一发,切换时预载(路径与 Skill.load 对齐)
oops.res.load(`game/skill/atk/${SkillSet[add.s_uuid].sp_name}`, 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);
mLogger.log(DEBUG, "BossPhase", `${model.hero_name} 阶段切换 P${from + 1}→P${phase.index + 1}(方案 ${phase.plan_id})`);
// 广播阶段切换(payload 用 eid 定位实体,防监听方持有已回收实体裸引用)
oops.message.dispatchEvent(GameEvent.BossPhaseChange, {
eid: e.eid,
planId: phase.plan_id,
fromPhase: from,
toPhase: phase.index,
});
}
/** TODO: 阶段退场表现(震屏/变暗等),后续迭代实现 */
private onPhaseExit(e: ecs.Entity): void { }
/** TODO: 阶段入场表现(怒吼/无敌帧/技能横幅等),后续迭代实现 */
private onPhaseEnter(e: ecs.Entity): void { }
}
```
- [ ] **Step 2: 确认系统注册链可达**
`@ecs.register` 装饰器在模块加载时自动注册系统,**唯一要求是模块被 import**。
验证:Task 5 已在 `Mon.ts` import `BossPhaseComp`,但那是组件文件;系统文件需单独确认——
在 `Mon.ts` 的 import 区追加一行(放在 `import { BossPhaseComp } from "./BossPhaseComp";` 之后):
```ts
import "./BossPhaseSystem";
```
(副作用 import 确保系统模块被加载注册;与框架"装饰器注册即生效"约定一致。)
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
重点确认:`update(e: ecs.Entity)` 签名(框架无 dt 参数)、`this.dt` 可用、`mLogger.log(DEBUG, tag, msg)` 签名与 BuffSystem.ts L39 一致、`e.eid` 属性存在(Mon.ts L120 已有先例)。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/hero/BossPhaseSystem.ts assets/script/game/hero/Mon.ts
git commit -m "feat: add BossPhaseSystem with phase switch logic"
```
---
### Task 7: RogueConfig.ts 消费阶段方案
**Files:**
- Modify: `assets/script/game/map/RogueConfig.ts:23`(import 区)
- Modify: `assets/script/game/map/RogueConfig.ts:207`(GeneratedMonster 接口)
- Modify: `assets/script/game/map/RogueConfig.ts:390-412`(makeBoss)
- Modify: `assets/script/game/map/RogueConfig.ts:167-189`(validateRogueConfig)
- Modify: `assets/script/game/map/RogueConfig.ts:231-246`(TestModeConfig)
- Modify: `assets/script/game/map/RogueConfig.ts:359-384`(generateTestWave)
- [ ] **Step 1: 新增 import**
在 L24 `import { SkillOverrides, SkillSet } from "../common/config/SkillSet";` 之后插入:
```ts
import { BossPhaseSet, validateBossPhaseSet } from "../common/config/BossPhaseSet";
```
- [ ] **Step 2: GeneratedMonster 接口加字段**
在 L208 `delay?: number;` 之后、L210 `/** 本怪所属批次...` 注释之前插入:
```ts
/** Boss 阶段方案 id(引用 BossPhaseSet,由 makeBoss 从 heroInfo.phase_ids 随机取一;未配置为 undefined) */
phase_plan_id?: number;
```
- [ ] **Step 3: makeBoss 读取消费**
L399-411 的 return 对象,在 `skills: { ults: this.pickBossUlts(range, stage) },` 之后追加一行属性:
```ts
phase_plan_id: this.pickPhasePlan(baseInfo),
```
并在 `pickBossUuid` 方法(L428-434)之后新增私有方法:
```ts
/** 从 Boss 允许的阶段方案中随机取一(未配置返回 undefined,Boss 退化为单阶段) */
private pickPhasePlan(baseInfo?: { phase_ids?: number[] }): number | undefined {
const ids = baseInfo?.phase_ids;
if (!ids || ids.length === 0) return undefined;
return ids[Math.floor(Math.random() * ids.length)];
}
```
- [ ] **Step 4: validateRogueConfig 追加校验**
在 L186 `return errors;` 之前插入:
```ts
// 校验 Boss 阶段方案表合法性(恰 2 切换点 / trigger 条件 / hp_pct 前缀与递减 / 技能引用)
for (const err of validateBossPhaseSet()) {
errors.push(err);
}
// 校验 heroInfo.phase_ids 引用的方案存在
for (const key in HeroInfo) {
const info = HeroInfo[key];
if (!info.phase_ids) continue;
for (const pid of info.phase_ids) {
if (!BossPhaseSet[pid]) {
errors.push(`HeroInfo[${key}] phase_ids 引用的方案 ${pid} 未在 BossPhaseSet 中定义`);
}
}
}
```
- [ ] **Step 5: TestModeConfig 加调试开关**
在 L244 `fstart: undefined as TriggerGrouped | undefined,` 之后追加:
```ts
/** 测试模式:Boss 阶段方案 id(0 = 不启用阶段;需配合 isBoss: true 才会挂载组件) */
phasePlanId: 0 as number,
/** 测试模式:是否以 Boss 身份刷出(影响层级/金币/阶段组件挂载) */
isBoss: false,
```
- [ ] **Step 6: generateTestWave 接通调试开关**
L364-381 的 monsters.push 对象改为(isBoss 与 phase_plan_id 两处变化,其余原样):
```ts
monsters.push({
uuid: TestModeConfig.monUuid,
type: TestModeConfig.monType,
hp: TestModeConfig.baseHp,
ap: TestModeConfig.baseAp,
isBoss: TestModeConfig.isBoss,
lv: 1,
spawnIndex: i,
batch: 0,
phase_plan_id: TestModeConfig.phasePlanId || undefined,
skills: {
skill: TestModeConfig.skill,
ults: TestModeConfig.ults,
atking: TestModeConfig.atking,
atked: TestModeConfig.atked,
dead: TestModeConfig.dead,
fstart: TestModeConfig.fstart,
},
});
```
- [ ] **Step 7: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 8: Commit**
```bash
git add assets/script/game/map/RogueConfig.ts
git commit -m "feat: consume phase plans in boss generation"
```
---
### Task 8: MissionMonComp.ts 接线 + _testSkills 残留修复
**Files:**
- Modify: `assets/script/game/map/MissionMonComp.ts:219-222`(_testSkills 赋值)
- Modify: `assets/script/game/map/MissionMonComp.ts:224`(load 调用)
- [ ] **Step 1: 修复 _testSkills 池化残留(现存 bug)**
L219-222 原代码:
```ts
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
if (monData.skills) {
(mon as any)._testSkills = monData.skills;
}
```
改为(无条件赋值,Boss 死亡回池复用为小怪时不再继承上一个 Boss 的大招):
```ts
// 技能套装注入:通过 _testSkills 通道传递给 Mon.load()
// Why 无条件赋值:实体池复用同一 JS 对象,条件赋值会让复用为小怪的实体残留上一个 Boss 的技能
(mon as any)._testSkills = monData.skills ?? undefined;
```
- [ ] **Step 2: load 调用追加第 8 实参**
L224 改为:
```ts
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monData.lv, posIndex, monData.phase_plan_id ?? 0);
```
- [ ] **Step 3: 编译验证**
Cocos Creator 编辑器编译,控制台无红色报错。
- [ ] **Step 4: Commit**
```bash
git add assets/script/game/map/MissionMonComp.ts
git commit -m "fix: pass phase plan id and clear stale test skills on reuse"
```
---
### Task 9: 运行时验证(编辑器内)
**Files:**
- 无代码改动(临时改 TestModeConfig 验证后还原)
- [ ] **Step 1: 配置测试模式**
`RogueConfig.ts` TestModeConfig 临时改为:
```ts
export const TestModeConfig = {
enable: true,
baseHp: 1000,
baseAp: 5,
monType: MonType.MeleeBoss,
monUuid: 6101,
spawnCount: 1,
phasePlanId: 10001,
isBoss: true,
// 其余字段保持原样(skill/ults/atking/atked/dead/fstart 均为 undefined)
```
(baseAp 调低避免过快击杀 Boss;baseHp 适中便于打到 70%/40% 阈值。)
- [ ] **Step 2: 运行验证阶段切换**
Cocos Creator 编辑器运行游戏进入战斗,验证:
1. 控制台出现 `[BossPhase] 兽人首领-双刀dps型 阶段切换 P1→P2(方案 10001)`(血量打到 70% 以下时)
2. 继续输出 `P2→P3`(血量 40% 以下或 60 秒后)
3. P3 后无论血量多低不再出现切换日志
4. 切换后 Boss 普攻(6004)仍正常施放(普攻首键未被破坏)
5. 拖过 60 秒验证 time 条件独立生效(用高生存阵容)
6. P2 起 Boss 施放新注入大招 6102(裂气斩)、P3 起 6103(闪电斩),P2 后 6100(寒冰斩,若 RogueConfig 注入过)消失
- [ ] **Step 3: 验证池化复用修复**
击败 Boss 后继续推进关卡刷出普通小怪,验证小怪不再施放 Boss 大招(控制台无异常大招横幅/弹道)。
- [ ] **Step 4: 验证乱序配置拦截(规格 §7.4)**
临时在 BossPhaseSet.ts 中加一个乱序方案:
```ts
99999: {
name: "乱序测试",
switches: [
{ trigger: { time: 30 } },
{ trigger: { hp_pct: 80 } },
],
},
```
在浏览器/编辑器控制台执行 `validateBossPhaseSet()`,确认返回包含
`hp_pct 切换点必须构成前缀` 的错误信息;验证后删除该临时方案。
- [ ] **Step 5: 验证单阶段回归**
TestModeConfig 改回 `phasePlanId: 0` 或换未配置 phase_ids 的 Boss(如 monUuid: 6102),验证行为与现状一致(无阶段切换日志)。
- [ ] **Step 6: 还原测试模式并提交**
TestModeConfig 还原为 `enable: false` 及原始默认值(phasePlanId: 0, isBoss: false 保留新增字段),提交:
```bash
git add assets/script/game/map/RogueConfig.ts
git commit -m "test: verify boss phase switching and pool reuse fix"
```
若验证中修复了任何问题,将修复与验证一并提交并在 commit body 说明。
---
## 完成标准
- [ ] 6101 Boss 在 70%/40% 血量阈值或 60s 时间阈值处依次切换 P1→P2→P3
- [ ] 切换时技能增量生效(removes 消失、adds 按 CD 轮转、attr_mod 攻防修正生效)
- [ ] 普攻全程不受影响(首键契约)
- [ ] 未配置 phase_ids 的 Boss 行为与现状完全一致
- [ ] 对象池复用无状态残留(Boss→小怪不继承技能)
- [ ] 全部任务提交完成,Cocos Creator 编译无报错