/** * 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; // 分诊日志:系统接管确认(elapsed===0 表示本组件首个有效帧,只输出一次) if (phase.elapsed === 0) { mLogger.log(DEBUG, "BossPhase", `${model.hero_name} 阶段系统已接管(方案 ${phase.plan_id},当前 P${phase.index + 1},hp ${Math.round(model.hp)}/${model.hp_max})`); } 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 { } }