技能选取/执行归 EnemyAttackSelector + EnemyAbilityBase(阶段一已就位), BossBase 不再持有 BossSkillExecutor。阶段切换改用能力注册表打断在跑的招。 弹反反制整套删除:它订阅的是全局弹反成功频道,无来源过滤—— 玩家弹反任意小怪都会让场上 Boss 硬直。正确形态是 EnemyBase.ReceiveParry (谁被弹反谁硬直),其接线为独立后续任务。 调试面板的 BossSkillExec 行换为阶段与过渡状态。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
201 lines
9.2 KiB
C#
201 lines
9.2 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using BaseGames.Boss;
|
|
using BaseGames.Combat;
|
|
using BaseGames.Core.Events;
|
|
using BaseGames.Enemies.Abilities;
|
|
|
|
namespace BaseGames.Enemies
|
|
{
|
|
/// <summary>
|
|
/// Boss 敌人基类。扩展 <see cref="EnemyBase"/> 以支持多阶段切换与战斗结束广播。
|
|
/// 招式的选取与执行走小怪同轨(<see cref="EnemyAttackSelector"/> + 能力组件),
|
|
/// 本类不再持有独立的技能执行器。具体 Boss 继承此类并重写 <see cref="EnterPhase"/>。
|
|
/// </summary>
|
|
public class BossBase : EnemyBase, BaseGames.AI.IBossControl
|
|
{
|
|
[Header("Boss 配置")]
|
|
[SerializeField] private string _bossId;
|
|
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
|
|
[SerializeField] private BossPhaseEventChannelSO _onBossPhaseChanged;
|
|
|
|
[Header("资源组件(可选)")]
|
|
[SerializeField] private BossResource _bossResource;
|
|
|
|
[Header("阶段招池门(可选)")]
|
|
[Tooltip("按阶段启用 / 禁用能力组件;未挂载则所有能力全阶段可用")]
|
|
[SerializeField] private BossPhaseAbilityGate _phaseGate;
|
|
|
|
[Header("竞技场锚点(可选)")]
|
|
[Tooltip("定点走位用的锚点集合;AI 图未用到锚点片段时可留空")]
|
|
[SerializeField] private BossArenaAnchors _arenaAnchors;
|
|
|
|
public string BossId => _bossId;
|
|
|
|
protected int _currentPhase = 0;
|
|
/// <summary>当前 Boss 阶段索引。IBossControl 实现。</summary>
|
|
public int CurrentPhase => _currentPhase;
|
|
|
|
/// <summary>Boss 资源是否已满。IBossControl 实现。
|
|
/// 未挂 BossResource 即抛——AI 图问了资源却没配资源组件是配置错误,必须暴露。</summary>
|
|
public bool ResourceFull => _bossResource != null
|
|
? _bossResource.IsFull
|
|
: throw new System.InvalidOperationException(
|
|
$"[BossBase] '{name}' 的 AI 图查询了资源满值,但未挂 BossResource 组件。" +
|
|
"请挂上该组件,或从 AI 图里移除资源相关的边。");
|
|
|
|
/// <summary>第 index 个竞技场锚点坐标。IBossControl 实现。未挂锚点组件即抛——
|
|
/// AI 图用了锚点片段却没配锚点是配置错误,必须暴露而非回退到自身位置。</summary>
|
|
public Vector2 AnchorAt(int index) => RequireAnchors().At(index);
|
|
|
|
/// <summary>当前位置到第 index 个锚点的距离。IBossControl 实现。</summary>
|
|
public float DistanceToAnchor(int index)
|
|
=> Vector2.Distance(transform.position, RequireAnchors().At(index));
|
|
|
|
private BossArenaAnchors RequireAnchors()
|
|
=> _arenaAnchors != null ? _arenaAnchors : throw new System.InvalidOperationException(
|
|
$"[BossBase] '{name}' 的 AI 图使用了竞技场锚点,但未挂 BossArenaAnchors 组件。" +
|
|
"请挂上该组件并配好锚点,或从 AI 图里移除锚点片段。");
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
// includeInactive:true 确保禁用状态的子组件也能被发现(如阶段门按阶段停用的组件)
|
|
if (_bossResource == null) _bossResource = GetComponentInChildren<BossResource>(true);
|
|
if (_phaseGate == null) _phaseGate = GetComponentInChildren<BossPhaseAbilityGate>(true);
|
|
if (_arenaAnchors == null) _arenaAnchors = GetComponentInChildren<BossArenaAnchors>(true);
|
|
}
|
|
|
|
// 初始阶段的招池必须在第一次选招前就位。
|
|
// ApplyPhase 此前只在 EnterPhase(阶段切换)与 OnSpawn(对象池复用)里调用,
|
|
// 而直接摆在场景里的 Boss 两条路径都不走——开局时后续阶段的招式仍是 enabled,
|
|
// 会混进阶段 0 的候选池。放在 Start:此时各能力组件的 Awake 均已执行完毕。
|
|
protected override void Start()
|
|
{
|
|
base.Start();
|
|
_phaseGate?.ApplyPhase(_currentPhase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 阶段过渡期间完全无敌(<see cref="EnemyBase.TakeDamage"/> 的 IsInvincible 检查由此路由)。
|
|
/// </summary>
|
|
public override bool IsInvincible => IsPhaseTransitioning || base.IsInvincible;
|
|
|
|
// ── 阶段 ──────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>当前是否处于阶段过渡(无敌帧 + 过渡演出)期间。</summary>
|
|
public bool IsPhaseTransitioning { get; private set; }
|
|
|
|
private Coroutine _phaseTransitionCoroutine;
|
|
|
|
/// <summary>
|
|
/// 进入指定阶段。自动打断在跑的招,广播 <see cref="BossPhaseEvent"/> 供 UI / 音乐系统响应。
|
|
/// 子类可重写以添加额外过渡逻辑(动画、无敌帧等)。
|
|
/// </summary>
|
|
public virtual void EnterPhase(int phase)
|
|
{
|
|
// 阶段切换必须先打断在跑的招,确保原子性
|
|
Abilities.InterruptAll(InterruptReason.ExternalRequest);
|
|
|
|
_currentPhase = phase;
|
|
// 新阶段换招池,上一阶段的防重复记忆不应跨阶段影响选招
|
|
AttackSelector?.ResetRepeatMemory();
|
|
// 阶段 = 换招池:先换池,再广播阶段事件,保证订阅方看到的是新池
|
|
_phaseGate?.ApplyPhase(phase);
|
|
_onBossPhaseChanged?.Raise(new BossPhaseEvent
|
|
{
|
|
BossId = _bossId,
|
|
Phase = phase,
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动阶段过渡演出:无敌帧 + 可选定格时间,结束后自动调用 <see cref="EnterPhase"/>。
|
|
/// 决策层检查 <see cref="IsPhaseTransitioning"/> 来等待过渡完成。
|
|
/// </summary>
|
|
/// <param name="targetPhase">过渡目标阶段索引。</param>
|
|
/// <param name="invincibleDuration">无敌帧持续时间(秒)。</param>
|
|
public void BeginPhaseTransition(int targetPhase, float invincibleDuration)
|
|
{
|
|
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);
|
|
|
|
// 打断在跑的招 + 停止移动
|
|
Abilities.InterruptAll(InterruptReason.ExternalRequest);
|
|
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);
|
|
}
|
|
|
|
public override void OnSpawn()
|
|
{
|
|
base.OnSpawn();
|
|
_currentPhase = 0;
|
|
_phaseGate?.ApplyPhase(0); // 对象池复用:回到阶段 0 的启用集
|
|
}
|
|
}
|
|
}
|