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.
798 lines
29 KiB
Markdown
798 lines
29 KiB
Markdown
# 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 编译无报错
|