telegraphVfxKey / telegraphDuration / TelegraphRoutine 删除。 预警的正确载体是 clip 本身——姿态在动画里,音效与特效在动画事件上 (PlaySFX / TriggerFeedback 已于阶段一补接)。旧路径会在动画之前 插一段无动画的静止等待,是同一件事的第二条平行路。 AbilityRunState.Telegraph 枚举值保留:BlinkStrikeAbility 现身闪光段 仍在自己协程里显式置位(计划原文假设它零消费者,实测不成立), 只删了基类的默认预警协程。顺带清掉该文件里遗留的行为树插件措辞。 18 个 ABL_ 资产经 AssetDatabase.ForceReserializeAssets 重写, 去掉已删字段的死键;顺带补齐了这些旧资产缺失的当前字段默认值 (designNote / category / weight / rangeRadius / rangeOffset / maxDashDuration), 无任何已有取值被改动。SOValidationRunner:0 错误 0 警告。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
9.8 KiB
C#
215 lines
9.8 KiB
C#
using System.Collections;
|
||
using UnityEngine;
|
||
using Animancer;
|
||
|
||
namespace BaseGames.Enemies.Abilities
|
||
{
|
||
/// <summary>
|
||
/// 敌人能力抽象基类(架构 07_EnemyModule §8.4)。
|
||
/// 责任:生命周期管理、冷却计时、中断分发。子类只实现 <see cref="ExecuteCoroutine"/>。
|
||
///
|
||
/// 设计要点:
|
||
/// - 单一执行实例:同一能力同时只有一个协程在跑。
|
||
/// - 协程内 yield WaitForSeconds 复用 <see cref="EnemyAbilityWaits"/>(无 GC)。
|
||
/// - 受击/死亡时由 <see cref="EnemyBase"/> 调用 <see cref="Interrupt"/>。
|
||
/// </summary>
|
||
[DisallowMultipleComponent]
|
||
public abstract class EnemyAbilityBase : MonoBehaviour, IAttackCandidate
|
||
{
|
||
[Header("配置 SO")]
|
||
[SerializeField] protected EnemyAbilitySO _config;
|
||
|
||
// 缓存依赖(Awake 填入,热路径无 GetComponent)
|
||
protected EnemyBase _enemy;
|
||
protected AnimancerComponent _animancer;
|
||
protected Transform _transform;
|
||
|
||
private Coroutine _runner;
|
||
private float _cooldownEndTime = -1f;
|
||
private bool _isRunning;
|
||
|
||
// ── 公共状态 ─────────────────────────────────────────────────────
|
||
public EnemyAbilitySO Config => _config;
|
||
public bool IsRunning => _isRunning;
|
||
public AbilityRunState Phase { get; protected set; } = AbilityRunState.Idle;
|
||
public float CooldownRemaining => Mathf.Max(0f, _cooldownEndTime - Time.time);
|
||
public bool IsOnCooldown => CooldownRemaining > 0f;
|
||
|
||
/// <summary>能力被外部中断时触发(AI 决策层 / 状态机订阅用)。</summary>
|
||
public event System.Action<InterruptReason> Interrupted;
|
||
|
||
/// <summary>
|
||
/// 统一可用性查询:组件启用 + 冷却完毕 + 未执行中 + 宿主存活。
|
||
/// enabled 这一维供阶段门使用——被阶段禁用的能力不得进入选招候选,
|
||
/// 否则选招器会选中它然后 StartCoroutine 在禁用组件上必然失败。
|
||
/// </summary>
|
||
public virtual bool CanUse => enabled && !_isRunning && !IsOnCooldown
|
||
&& _enemy != null && _enemy.IsAlive;
|
||
|
||
// ── IAttackCandidate(供 EnemyAttackSelector 选招)──────────────────
|
||
public bool RequiresLineOfSight => _config != null && _config.requiresLineOfSight;
|
||
public bool RequiresGrounded => _config != null && _config.requiresGrounded;
|
||
public float Weight => _config != null ? _config.weight : 0f;
|
||
public int Priority => _config != null ? _config.priority : 0;
|
||
|
||
/// <summary>本招圆形攻击范围内是否有玩家(招式自管其射程;LOS 复用敌人级 IsPlayerVisible)。</summary>
|
||
public virtual bool InAttackRange()
|
||
{
|
||
if (_config == null || _config.rangeRadius <= 0f
|
||
|| _enemy == null || _enemy.PlayerTransform == null) return false;
|
||
float sign = _transform.localScale.x < 0f ? -1f : 1f;
|
||
Vector2 origin = (Vector2)_transform.position
|
||
+ new Vector2(_config.rangeOffset.x * sign, _config.rangeOffset.y);
|
||
float r = _config.rangeRadius;
|
||
return ((Vector2)_enemy.PlayerTransform.position - origin).sqrMagnitude <= r * r;
|
||
}
|
||
|
||
protected virtual void Awake()
|
||
{
|
||
_enemy = GetComponentInParent<EnemyBase>();
|
||
_animancer = _enemy != null ? _enemy.Animancer : GetComponentInParent<AnimancerComponent>();
|
||
_transform = transform;
|
||
if (_enemy == null)
|
||
Debug.LogError($"[EnemyAbilityBase] {GetType().Name} 找不到 EnemyBase。", this);
|
||
if (_animancer == null)
|
||
Debug.LogWarning($"[EnemyAbilityBase] {GetType().Name} 找不到 AnimancerComponent,动画能力将无法播放动画。", this);
|
||
}
|
||
|
||
protected virtual void OnDisable()
|
||
{
|
||
if (_isRunning) Interrupt(InterruptReason.ExternalRequest);
|
||
}
|
||
|
||
// ── 执行 ─────────────────────────────────────────────────────────
|
||
/// <summary>
|
||
/// 启动能力。重复调用、冷却中或已运行将返回 false。
|
||
/// 若 <see cref="EnemyAbilitySO.exclusionGroup"/> 非空,会先中断同组其他能力(互斥)。
|
||
/// </summary>
|
||
public bool Execute()
|
||
{
|
||
if (!CanUse) return false;
|
||
|
||
// 互斥组:启动前中断同组正在运行的其他能力
|
||
if (_config != null && !string.IsNullOrEmpty(_config.exclusionGroup))
|
||
_enemy?.Abilities.InterruptGroup(_config.exclusionGroup, InterruptReason.ExternalRequest);
|
||
|
||
_runner = StartCoroutine(RunInternal());
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 强制启动能力,忽略冷却检查(连段语义:外部组合技调用子能力时使用)。
|
||
/// 若能力正在运行则先中断再重启。
|
||
/// </summary>
|
||
public bool ForceExecute()
|
||
{
|
||
if (_enemy == null || !_enemy.IsAlive) return false;
|
||
if (_isRunning) Interrupt(InterruptReason.ExternalRequest);
|
||
_runner = StartCoroutine(RunInternal());
|
||
return true;
|
||
}
|
||
|
||
private IEnumerator RunInternal()
|
||
{
|
||
_isRunning = true;
|
||
Phase = AbilityRunState.Windup;
|
||
try
|
||
{
|
||
yield return ExecuteCoroutine();
|
||
Phase = AbilityRunState.Recovery;
|
||
}
|
||
finally
|
||
{
|
||
_isRunning = false;
|
||
_runner = null;
|
||
_cooldownEndTime = Time.time + (_config != null ? _config.cooldown : 0f);
|
||
if (Phase != AbilityRunState.Interrupted) Phase = AbilityRunState.Idle;
|
||
OnAbilityEnded();
|
||
}
|
||
}
|
||
|
||
/// <summary>子类实现:能力主体。可分多段、含 HitBox 激活/弹幕生成/物理推进等。</summary>
|
||
protected abstract IEnumerator ExecuteCoroutine();
|
||
|
||
/// <summary>能力结束钩子(被中断或正常结束都会调用)。</summary>
|
||
protected virtual void OnAbilityEnded() { }
|
||
|
||
/// <summary>中断当前执行。冷却仍会按配置计入。</summary>
|
||
public void Interrupt(InterruptReason reason)
|
||
{
|
||
if (!_isRunning) return;
|
||
if (_config != null)
|
||
{
|
||
if (reason == InterruptReason.Hurt && !_config.interruptOnHurt) return;
|
||
if (reason == InterruptReason.Stagger && !_config.interruptOnStagger) return;
|
||
}
|
||
if (_runner != null) StopCoroutine(_runner);
|
||
_runner = null;
|
||
_isRunning = false;
|
||
Phase = AbilityRunState.Interrupted;
|
||
OnInterrupted(reason);
|
||
Interrupted?.Invoke(reason);
|
||
OnAbilityEnded();
|
||
_cooldownEndTime = Time.time + (_config != null ? _config.cooldown * 0.5f : 0f);
|
||
}
|
||
|
||
protected virtual void OnInterrupted(InterruptReason reason) { }
|
||
|
||
/// <summary>子类辅助:朝向目标(写入输入信号,下一 FixedUpdate 由 EnemyMovement 消费)。</summary>
|
||
protected void FaceTarget(Transform target)
|
||
{
|
||
if (target == null || _enemy == null) return;
|
||
_enemy.FaceTarget(target.position);
|
||
}
|
||
|
||
/// <summary>把 _config 解析为期望的子类型;类型不符/为空时显式报错(不回退)。</summary>
|
||
protected T ResolveConfig<T>() where T : EnemyAbilitySO
|
||
{
|
||
if (_config is T typed) return typed;
|
||
Debug.LogError($"[{GetType().Name}] _config 需为 {typeof(T).Name}," +
|
||
$"实际 = {(_config != null ? _config.GetType().Name : "null")}。请重建为正确的子类 SO。", this);
|
||
return null;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>选中时绘制攻击射程范围(编辑器可视化辅助,不影响运行时)。</summary>
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (_config == null || _config.category != AbilityCategory.Attack || _config.rangeRadius <= 0f) return;
|
||
float sign = transform.localScale.x < 0f ? -1f : 1f;
|
||
Vector3 origin = transform.position
|
||
+ new Vector3(_config.rangeOffset.x * sign, _config.rangeOffset.y, 0f);
|
||
UnityEditor.Handles.color = new Color(1f, 0.4f, 0.2f, 0.9f);
|
||
UnityEditor.Handles.DrawWireDisc(origin, Vector3.forward, _config.rangeRadius);
|
||
UnityEditor.Handles.Label(origin, $"{_config.abilityId} range");
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/// <summary>WaitForSeconds 池(架构 §10 GC 优化)。能力协程统一通过此获取等待指令。</summary>
|
||
internal static class EnemyAbilityWaits
|
||
{
|
||
private const int MaxCacheSize = 64;
|
||
|
||
private static readonly System.Collections.Generic.Dictionary<float, WaitForSeconds> _cache
|
||
= new System.Collections.Generic.Dictionary<float, WaitForSeconds>(32);
|
||
public static WaitForSeconds Get(float seconds)
|
||
{
|
||
if (seconds <= 0f) return null;
|
||
if (!_cache.TryGetValue(seconds, out var w))
|
||
{
|
||
if (_cache.Count < MaxCacheSize)
|
||
{
|
||
w = new WaitForSeconds(seconds);
|
||
_cache[seconds] = w;
|
||
}
|
||
else
|
||
{
|
||
return new WaitForSeconds(seconds);
|
||
}
|
||
}
|
||
return w;
|
||
}
|
||
}
|
||
}
|