Files
zeling_v2/Assets/_Game/Scripts/Enemies/Boss/BossBase.cs

368 lines
15 KiB
C#
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.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Boss;
using BaseGames.Combat;
using BaseGames.Core.Events;
using BaseGames.Parry;
namespace BaseGames.Enemies
{
/// <summary>
/// Boss 敌人基类。扩展 <see cref="EnemyBase"/> 以支持多阶段切换、技能执行与战斗结束广播。
/// 具体 Boss 继承此类并重写 <see cref="EnterPhase"/>。
/// </summary>
public class BossBase : EnemyBase
{
[Header("Boss 配置")]
[SerializeField] private string _bossId;
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
[SerializeField] private BossPhaseEventChannelSO _onBossPhaseChanged;
[Header("技能执行器")]
[SerializeField] private BossSkillExecutor _skillExecutor;
[Header("资源组件(可选)")]
[SerializeField] private BossResource _bossResource;
[Header("玩家反制事件(可选)")]
[Tooltip("订阅此频道以响应玩家弹反成功事件")]
[SerializeField] private ParryInfoEventChannelSO _onParrySuccess;
public string BossId => _bossId;
/// <summary>当前是否有 Boss 技能正在执行BD_UseBossSkill 轮询此值)。</summary>
public bool IsBossSkillExecuting => _skillExecutor != null && _skillExecutor.IsExecuting;
protected int _currentPhase = 0;
/// <summary>当前 Boss 阶段索引BD Task 可直接查询)。</summary>
public int CurrentPhase => _currentPhase;
private Coroutine _counterStaggerCoroutine;
// 缓存加权候选列表,避免 UseBossSkillWeighted() 每次 new List → GC 分配
private readonly List<(BossSkillSO skill, float w)> _weightedCandidates = new(8);
// 单元素缓冲数组,供 ApplyCounterResponse 缓存当前技能,避免 new[] 分配
private readonly BossSkillSO[] _singleSkillBuf = new BossSkillSO[1];
protected override void Awake()
{
base.Awake();
// includeInactive:true 确保禁用状态的子组件也能被发现(如分阶段按需启用的执行器)
if (_skillExecutor == null) _skillExecutor = GetComponentInChildren<BossSkillExecutor>(true);
if (_bossResource == null) _bossResource = GetComponentInChildren<BossResource>(true);
}
protected override void OnEnable()
{
base.OnEnable();
_onParrySuccess?.Subscribe(HandleParrySuccess).AddTo(_subs);
}
/// <summary>
/// 阶段过渡期间完全无敌(<see cref="EnemyBase.TakeDamage"/> 的 IsInvincible 检查由此路由)。
/// </summary>
public override bool IsInvincible => IsPhaseTransitioning || base.IsInvincible;
/// <summary>
/// 上一次成功执行的技能 ID。<see cref="UseBossSkillWeighted"/> 对其施加权重惩罚,防止相同技能连续重复。
/// </summary>
public string LastUsedSkillId { get; private set; }
// ── 技能执行BD Task 调用入口)─────────────────────────────────────
/// <summary>
/// 通过技能 ID 执行 Boss 技能。
/// 若技能未找到、执行器忙或冷却中则返回 false否则返回 true。
/// </summary>
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;
}
/// <summary>
/// 在当前阶段可用且冷却就绪的技能中,按 <see cref="BossSkillSO.weight"/> 加权随机选择一个并执行。
/// 若上一次已使用某技能,则对该技能施加 0.3× 权重惩罚,降低连续重复的概率。
/// 若无可用技能或执行器忙则返回 false。
/// </summary>
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();
float totalWeight = 0f;
foreach (var s in skills)
{
if (s == null || s.weight <= 0f) continue;
if (!_skillExecutor.CanUseSkill(s.skillId)) continue;
if (!IsSkillAvailableInPhase(s)) continue;
// 防重复:上一个技能权重打折
float w = s.skillId == LastUsedSkillId ? s.weight * 0.3f : s.weight;
_weightedCandidates.Add((s, w));
totalWeight += w;
}
if (_weightedCandidates.Count == 0 || totalWeight <= 0f) return false;
// 加权随机抽取
float roll = UnityEngine.Random.Range(0f, totalWeight);
BossSkillSO selected = null;
float accum = 0f;
foreach (var (skill, w) in _weightedCandidates)
{
accum += w;
if (roll <= accum) { selected = skill; break; }
}
selected ??= _weightedCandidates[_weightedCandidates.Count - 1].skill;
if (!CheckResourceCost(selected)) return false;
_skillExecutor.ExecuteSkill(selected);
LastUsedSkillId = selected.skillId;
_bossResource?.OnBossUseSkill();
return true;
}
/// <summary>检查技能的 availablePhaseIndices 是否包含当前阶段(空数组 = 全阶段可用)。</summary>
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;
}
/// <summary>
/// 检查 Boss 资源是否满足技能的 minRequired 门槛。
/// 未配置资源组件或 minRequired &lt;= 0 时视为通过。
/// </summary>
private bool CheckResourceCost(BossSkillSO skill)
{
if (_bossResource == null) return true;
float min = skill.resourceCost.minRequired;
if (min <= 0f) return true;
return _bossResource.CurrentValue >= min;
}
// ── 阶段 ──────────────────────────────────────────────────────────────
/// <summary>当前是否处于阶段过渡(无敌帧 + 过渡演出)期间。</summary>
public bool IsPhaseTransitioning { get; private set; }
private Coroutine _phaseTransitionCoroutine;
/// <summary>
/// 进入指定阶段。自动打断当前执行中的技能,广播 <see cref="BossPhaseEvent"/> 供 UI / 音乐系统响应。
/// 子类可重写以添加额外过渡逻辑(动画、无敌帧等)。
/// </summary>
public virtual void EnterPhase(int phase)
{
// 阶段切换必须先打断正在执行的技能,确保原子性
_skillExecutor?.InterruptCurrentSkill();
_currentPhase = phase;
LastUsedSkillId = null; // 新阶段重置权重惩罚,防止跨阶段漂移
_onBossPhaseChanged?.Raise(new BossPhaseEvent
{
BossId = _bossId,
Phase = phase,
});
}
/// <summary>
/// 启动阶段过渡演出:无敌帧 + 可选定格时间,结束后自动调用 <see cref="EnterPhase"/>。
/// BD_BossPhaseTransition 检查 <see cref="IsPhaseTransitioning"/> 来等待过渡完成。
/// </summary>
/// <param name="targetPhase">过渡目标阶段索引。</param>
/// <param name="invincibleDuration">无敌帧持续时间(秒)。</param>
public void BeginPhaseTransition(int targetPhase, float invincibleDuration = 1.5f)
{
if (IsPhaseTransitioning)
{
Debug.LogWarning(
$"[BossBase] '{_bossId}' 已在阶段过渡中(当前阶段 {_currentPhase}" +
$"忽略跳转至阶段 {targetPhase} 的请求。请检查行为树逻辑是否重复触发阶段切换。",
this);
return;
}
if (_phaseTransitionCoroutine != null) StopCoroutine(_phaseTransitionCoroutine);
_phaseTransitionCoroutine = StartCoroutine(PhaseTransitionCoroutine(targetPhase, invincibleDuration));
}
private IEnumerator PhaseTransitionCoroutine(int targetPhase, float duration)
{
IsPhaseTransitioning = true;
OnBeginPhaseTransition(targetPhase);
// 打断技能 + 停止移动
_skillExecutor?.InterruptCurrentSkill();
StopMovement();
// 无敌帧期间接受的伤害由 IsInvincible 属性屏蔽(子类重写 IsInvincible 或在此处理)
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
yield return null;
}
EnterPhase(targetPhase);
IsPhaseTransitioning = false;
_phaseTransitionCoroutine = null;
}
/// <summary>立即终止阶段过渡协程并清除标志位。
/// 死亡时调用,防止 IsPhaseTransitioning 永久为 true 影响对象池复用。
/// </summary>
private void AbortPhaseTransition()
{
if (_phaseTransitionCoroutine != null)
{
StopCoroutine(_phaseTransitionCoroutine);
_phaseTransitionCoroutine = null;
}
IsPhaseTransitioning = false;
}
/// <summary>
/// 阶段过渡开始时回调(子类可重写以触发演出动画或特殊逻辑)。
/// 在无敌帧等待之前调用。
/// </summary>
protected virtual void OnBeginPhaseTransition(int targetPhase) { }
/// <summary>检查当前 HP 是否低于指定百分比0~1。</summary>
public bool IsHPBelow(float ratio)
{
if (_stats == null || _stats.MaxHP <= 0) return false;
return (float)_stats.CurrentHP / _stats.MaxHP < ratio;
}
protected override void OnDamageTaken(DamageInfo info)
{
_bossResource?.OnBossTakeDamage();
}
protected override void Die()
{
// 死亡时立即中止阶段过渡,防止 IsPhaseTransitioning 标志永久锁死(影响对象池复用)
AbortPhaseTransition();
base.Die();
_onBossFightEnded?.Raise(true);
}
// ── 玩家反制响应 ──────────────────────────────────────────────────────
private void HandleParrySuccess(ParryInfo info)
{
if (!IsAlive) return;
var counterType = info.IsPerfect ? CounterType.PerfectParry : CounterType.Parry;
ApplyCounterResponse(counterType, string.Empty);
}
/// <summary>
/// 根据 counterType 查找当前技能(或所有技能)的 PlayerCounterResponse 并应用效果。
/// 可由外部系统(闪避穿越、弱点命中等)直接调用。
/// </summary>
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;
_skillExecutor?.ResetAllCooldowns();
}
}
}