diff --git a/Assets/_Game/Scripts/Enemies/Boss/BossBase.cs b/Assets/_Game/Scripts/Enemies/Boss/BossBase.cs index 5905b3ad..c2e4340e 100644 --- a/Assets/_Game/Scripts/Enemies/Boss/BossBase.cs +++ b/Assets/_Game/Scripts/Enemies/Boss/BossBase.cs @@ -1,16 +1,16 @@ using System.Collections; -using System.Collections.Generic; using UnityEngine; using BaseGames.Boss; using BaseGames.Combat; using BaseGames.Core.Events; -using BaseGames.Parry; +using BaseGames.Enemies.Abilities; namespace BaseGames.Enemies { /// - /// Boss 敌人基类。扩展 以支持多阶段切换、技能执行与战斗结束广播。 - /// 具体 Boss 继承此类并重写 。 + /// Boss 敌人基类。扩展 以支持多阶段切换与战斗结束广播。 + /// 招式的选取与执行走小怪同轨( + 能力组件), + /// 本类不再持有独立的技能执行器。具体 Boss 继承此类并重写 。 /// public class BossBase : EnemyBase, BaseGames.AI.IBossControl { @@ -19,9 +19,6 @@ namespace BaseGames.Enemies [SerializeField] private BoolEventChannelSO _onBossFightEnded; [SerializeField] private BossPhaseEventChannelSO _onBossPhaseChanged; - [Header("技能执行器")] - [SerializeField] private BossSkillExecutor _skillExecutor; - [Header("资源组件(可选)")] [SerializeField] private BossResource _bossResource; @@ -33,15 +30,8 @@ namespace BaseGames.Enemies [Tooltip("定点走位用的锚点集合;AI 图未用到锚点片段时可留空")] [SerializeField] private BossArenaAnchors _arenaAnchors; - [Header("玩家反制事件(可选)")] - [Tooltip("订阅此频道以响应玩家弹反成功事件")] - [SerializeField] private ParryInfoEventChannelSO _onParrySuccess; - public string BossId => _bossId; - /// 当前是否有 Boss 技能正在执行。 - public bool IsBossSkillExecuting => _skillExecutor != null && _skillExecutor.IsExecuting; - protected int _currentPhase = 0; /// 当前 Boss 阶段索引。IBossControl 实现。 public int CurrentPhase => _currentPhase; @@ -67,20 +57,10 @@ namespace BaseGames.Enemies $"[BossBase] '{name}' 的 AI 图使用了竞技场锚点,但未挂 BossArenaAnchors 组件。" + "请挂上该组件并配好锚点,或从 AI 图里移除锚点片段。"); - private Coroutine _counterStaggerCoroutine; - - // 缓存加权候选与其有效权重(两者等长、下标对应),避免 UseBossSkillWeighted() 每次 new List → GC 分配 - private readonly List _weightedCandidates = new(8); - private readonly List _candidateWeights = new(8); - - // 单元素缓冲数组,供 ApplyCounterResponse 缓存当前技能,避免 new[] 分配 - private readonly BossSkillSO[] _singleSkillBuf = new BossSkillSO[1]; - protected override void Awake() { base.Awake(); - // includeInactive:true 确保禁用状态的子组件也能被发现(如分阶段按需启用的执行器) - if (_skillExecutor == null) _skillExecutor = GetComponentInChildren(true); + // includeInactive:true 确保禁用状态的子组件也能被发现(如阶段门按阶段停用的组件) if (_bossResource == null) _bossResource = GetComponentInChildren(true); if (_phaseGate == null) _phaseGate = GetComponentInChildren(true); if (_arenaAnchors == null) _arenaAnchors = GetComponentInChildren(true); @@ -96,110 +76,11 @@ namespace BaseGames.Enemies _phaseGate?.ApplyPhase(_currentPhase); } - protected override void OnEnable() - { - base.OnEnable(); - _onParrySuccess?.Subscribe(HandleParrySuccess).AddTo(_subs); - } - /// /// 阶段过渡期间完全无敌( 的 IsInvincible 检查由此路由)。 /// public override bool IsInvincible => IsPhaseTransitioning || base.IsInvincible; - /// - /// 上一次成功执行的技能 ID。 对其施加权重惩罚,防止相同技能连续重复。 - /// - public string LastUsedSkillId { get; private set; } - - // ── 技能执行(决策层调用入口)─────────────────────────────────────── - - /// - /// 通过技能 ID 执行 Boss 技能。 - /// 若技能未找到、执行器忙或冷却中则返回 false,否则返回 true。 - /// - public bool UseBossSkill(string skillId) - { - if (_skillExecutor == null || string.IsNullOrEmpty(skillId)) return false; - if (IsPhaseTransitioning) return false; - var skill = _skillExecutor.FindSkill(skillId); - if (skill == null) - { - Debug.LogWarning($"[BossBase] 未找到技能 '{skillId}'(Boss: {_bossId})", this); - return false; - } - if (!_skillExecutor.CanUseSkill(skillId)) - return false; - if (!CheckResourceCost(skill)) - return false; - - _skillExecutor.ExecuteSkill(skill); - _bossResource?.OnBossUseSkill(); - return true; - } - - /// - /// 在当前阶段可用且冷却就绪的技能中,按 加权随机选择一个并执行。 - /// 若上一次已使用某技能,则对该技能施加 0.3× 权重惩罚,降低连续重复的概率。 - /// 若无可用技能或执行器忙则返回 false。 - /// - public bool UseBossSkillWeighted() - { - if (_skillExecutor == null || _skillExecutor.IsExecuting) return false; - if (IsPhaseTransitioning) return false; - - var skills = _skillExecutor.Skills; - if (skills == null || skills.Length == 0) return false; - - // 筛选:在当前阶段可用 + 冷却就绪 + weight > 0 - _weightedCandidates.Clear(); - _candidateWeights.Clear(); - foreach (var s in skills) - { - if (s == null || s.weight <= 0f) continue; - if (!_skillExecutor.CanUseSkill(s.skillId)) continue; - if (!IsSkillAvailableInPhase(s)) continue; - - _weightedCandidates.Add(s); - // 防重复:上一个技能权重打折 - _candidateWeights.Add(s.skillId == LastUsedSkillId ? s.weight * 0.3f : s.weight); - } - - // 加权随机抽取(共享原语:无正权重/无候选时返回 -1) - int idx = BaseGames.Core.WeightedPick.Index(_candidateWeights); - if (idx < 0) return false; - BossSkillSO selected = _weightedCandidates[idx]; - - if (!CheckResourceCost(selected)) return false; - - _skillExecutor.ExecuteSkill(selected); - LastUsedSkillId = selected.skillId; - _bossResource?.OnBossUseSkill(); - return true; - } - - /// 检查技能的 availablePhaseIndices 是否包含当前阶段(空数组 = 全阶段可用)。 - private bool IsSkillAvailableInPhase(BossSkillSO skill) - { - if (skill.availablePhaseIndices == null || skill.availablePhaseIndices.Length == 0) - return true; - foreach (int p in skill.availablePhaseIndices) - if (p == _currentPhase) return true; - return false; - } - - /// - /// 检查 Boss 资源是否满足技能的 minRequired 门槛。 - /// 未配置资源组件或 minRequired <= 0 时视为通过。 - /// - private bool CheckResourceCost(BossSkillSO skill) - { - if (_bossResource == null) return true; - float min = skill.resourceCost.minRequired; - if (min <= 0f) return true; - return _bossResource.CurrentValue >= min; - } - // ── 阶段 ────────────────────────────────────────────────────────────── /// 当前是否处于阶段过渡(无敌帧 + 过渡演出)期间。 @@ -208,16 +89,15 @@ namespace BaseGames.Enemies private Coroutine _phaseTransitionCoroutine; /// - /// 进入指定阶段。自动打断当前执行中的技能,广播 供 UI / 音乐系统响应。 + /// 进入指定阶段。自动打断在跑的招,广播 供 UI / 音乐系统响应。 /// 子类可重写以添加额外过渡逻辑(动画、无敌帧等)。 /// public virtual void EnterPhase(int phase) { - // 阶段切换必须先打断正在执行的技能,确保原子性 - _skillExecutor?.InterruptCurrentSkill(); + // 阶段切换必须先打断在跑的招,确保原子性 + Abilities.InterruptAll(InterruptReason.ExternalRequest); _currentPhase = phase; - LastUsedSkillId = null; // 新阶段重置权重惩罚,防止跨阶段漂移 // 新阶段换招池,上一阶段的防重复记忆不应跨阶段影响选招 AttackSelector?.ResetRepeatMemory(); // 阶段 = 换招池:先换池,再广播阶段事件,保证订阅方看到的是新池 @@ -254,8 +134,8 @@ namespace BaseGames.Enemies IsPhaseTransitioning = true; OnBeginPhaseTransition(targetPhase); - // 打断技能 + 停止移动 - _skillExecutor?.InterruptCurrentSkill(); + // 打断在跑的招 + 停止移动 + Abilities.InterruptAll(InterruptReason.ExternalRequest); StopMovement(); // 无敌帧期间接受的伤害由 IsInvincible 属性屏蔽(子类重写 IsInvincible 或在此处理) @@ -310,97 +190,11 @@ namespace BaseGames.Enemies _onBossFightEnded?.Raise(true); } - // ── 玩家反制响应 ────────────────────────────────────────────────────── - - private void HandleParrySuccess(ParryInfo info) - { - if (!IsAlive) return; - var counterType = info.IsPerfect ? CounterType.PerfectParry : CounterType.Parry; - ApplyCounterResponse(counterType, string.Empty); - } - - /// - /// 根据 counterType 查找当前技能(或所有技能)的 PlayerCounterResponse 并应用效果。 - /// 可由外部系统(闪避穿越、弱点命中等)直接调用。 - /// - public void ApplyCounterResponse(CounterType counterType, string requiredSkillId) - { - if (_skillExecutor == null) return; - - // 优先检查当前正在执行的技能的反制规则 - BossSkillSO activeSkill = _skillExecutor.IsExecuting - ? _skillExecutor.FindCurrentSkill() - : null; - - BossSkillSO[] candidates; - if (activeSkill != null) - { - _singleSkillBuf[0] = activeSkill; - candidates = _singleSkillBuf; - } - else - { - candidates = _skillExecutor.Skills; - } - - if (candidates == null) return; - - foreach (var skill in candidates) - { - if (skill?.counterResponses == null) continue; - foreach (var resp in skill.counterResponses) - { - if (resp.counterType != counterType) continue; - if (!string.IsNullOrEmpty(resp.requiredSkillId) && - !string.IsNullOrEmpty(requiredSkillId) && - resp.requiredSkillId != requiredSkillId) - continue; - - ExecuteCounterEffect(resp); - return; // 每次反制只触发第一条匹配规则 - } - } - } - - private void ExecuteCounterEffect(in PlayerCounterResponse resp) - { - if (resp.interruptSkill) - _skillExecutor?.InterruptCurrentSkill(); - - if (resp.bossStaggerDuration > 0f) - { - if (_counterStaggerCoroutine != null) - StopCoroutine(_counterStaggerCoroutine); - _counterStaggerCoroutine = StartCoroutine(CounterStaggerCoroutine(resp.bossStaggerDuration)); - } - - if (resp.openVulnWindow) - { - float duration = Mathf.Max(resp.bossStaggerDuration, 1f); - float multiplier = 1f + resp.bossDamageBonus; - _skillExecutor?.OpenVulnerabilityWindow(duration, multiplier); - } - - resp.counterFeedback?.Play(); - } - - private IEnumerator CounterStaggerCoroutine(float duration) - { - ForceState(EnemyStateType.Stagger); - // 时长固定且较短,直接 new WFY 即可;若需优化可接入 WFS 缓存 - yield return new WaitForSeconds(duration); - if (IsAlive && CurrentState == EnemyStateType.Stagger) - ForceState(EnemyStateType.Controlled); - _counterStaggerCoroutine = null; - } - public override void OnSpawn() { base.OnSpawn(); - LastUsedSkillId = null; - _currentPhase = 0; + _currentPhase = 0; _phaseGate?.ApplyPhase(0); // 对象池复用:回到阶段 0 的启用集 - _skillExecutor?.ResetAllCooldowns(); } } } diff --git a/Assets/_Game/Scripts/Enemies/EnemyDebugOverlay.cs b/Assets/_Game/Scripts/Enemies/EnemyDebugOverlay.cs index 89f52dfd..f1478caa 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyDebugOverlay.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyDebugOverlay.cs @@ -11,7 +11,7 @@ namespace BaseGames.Enemies /// - AI 决策状态 / EnemyStateType /// - 当前激活的能力名称及运行状态 /// - LOS 状态 / 到玩家距离 - /// - Boss:技能执行状态 + 各技能冷却倒计时 + /// - Boss:当前阶段与阶段过渡状态 /// [AddComponentMenu("BaseGames/Debug/Enemy Debug Overlay")] public sealed class EnemyDebugOverlay : MonoBehaviour @@ -110,7 +110,9 @@ namespace BaseGames.Enemies // Boss 信息 if (_boss != null) { - list.Add($"BossSkillExec: {_boss.IsBossSkillExecuting}"); + // 上面第 2 行已用 "Phase:" 表示 AI 决策状态名,这里用 "BossPhase:" 区分,避免同名两行 + list.Add($"BossPhase: {_boss.CurrentPhase}" + + (_boss.IsPhaseTransitioning ? " (transitioning)" : "")); } // 状态效果