From ad50e11052dc9bc246be5e2a796624a6433f5988 Mon Sep 17 00:00:00 2001 From: panFD Date: Wed, 7 Oct 2026 15:43:17 +0800 Subject: [PATCH] feat: add BossPhaseSystem with phase switch logic --- assets/script/game/hero/BossPhaseSystem.ts | 125 +++++++++++++++++++++ assets/script/game/hero/Mon.ts | 1 + 2 files changed, 126 insertions(+) create mode 100644 assets/script/game/hero/BossPhaseSystem.ts diff --git a/assets/script/game/hero/BossPhaseSystem.ts b/assets/script/game/hero/BossPhaseSystem.ts new file mode 100644 index 00000000..e4eb69a1 --- /dev/null +++ b/assets/script/game/hero/BossPhaseSystem.ts @@ -0,0 +1,125 @@ +/** + * 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 { } +} diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 09feb028..c8eefc93 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -12,6 +12,7 @@ import { SkillTriggerType, HType } from "../common/config/heroSet"; import { SkillTriggerHelper } from "./SkillTriggerHelper"; import { BossPhaseSet } from "../common/config/BossPhaseSet"; import { BossPhaseComp } from "./BossPhaseComp"; +import "./BossPhaseSystem"; import { mLogger } from "../common/Logger"; /** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */ @ecs.register(`Monster`)