diff --git a/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs b/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs new file mode 100644 index 00000000..c474feb5 --- /dev/null +++ b/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs @@ -0,0 +1,58 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using BaseGames.Enemies; +using BaseGames.Enemies.Abilities; + +namespace BaseGames.Tests.EditMode.Enemies +{ + public class BossPhaseAbilityGateTests + { + /// 最小可实例化能力:不播动画、不碰 HitBox,只为验证启停与 CanUse。 + private sealed class StubAbility : EnemyAbilityBase + { + /// + /// 编辑模式下 Unity 不会在 AddComponent 时调用 Awake,_enemy 会留空导致 CanUse 恒假。 + /// 这里手动跑一次真实的 Awake(而非反射直写字段),让依赖解析走生产代码路径。 + /// + public void RunAwake() => Awake(); + + protected override IEnumerator ExecuteCoroutine() { yield break; } + } + + /// 测试期建出的宿主对象;在 TearDown 统一销毁,断言失败也不会残留到编辑器场景。 + private GameObject _host; + + // RunAwake 时 EnemyAbilityBase 找不到 AnimancerComponent 会告警,与本测试无关。 + [SetUp] + public void SetUp() => LogAssert.ignoreFailingMessages = true; + + [TearDown] + public void TearDown() + { + // 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句, + // 清理必须放在 TearDown,否则失败一次就往编辑器场景里漏一个对象。 + if (_host != null) Object.DestroyImmediate(_host); + _host = null; + LogAssert.ignoreFailingMessages = false; + } + + [Test] + public void DisabledAbility_IsNotUsable() + { + // 必须先挂 EnemyBase:否则 _enemy==null 会让 CanUse 恒假, + // 这条断言在修复前也会"通过",成为一个永远不会失败的测试。 + _host = new GameObject("ability-host"); + _host.AddComponent(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真 + var ab = _host.AddComponent(); + ab.RunAwake(); // 编辑模式不自动跑 Awake,手动解析 _enemy + + Assert.IsTrue(ab.CanUse, + "前提:启用且宿主存活时应可用——否则下一条断言分不清是 enabled 还是别的门在起作用"); + + ab.enabled = false; + Assert.IsFalse(ab.CanUse, "禁用的能力组件不可用——否则选招器会选中一个启不动招的招"); + } + } +} diff --git a/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs.meta b/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs.meta new file mode 100644 index 00000000..f3c91fef --- /dev/null +++ b/Assets/Tests/EditMode/Enemies/BossPhaseAbilityGateTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cc78469b00f228648844d2e34c13f80c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs index 42d1a474..9961fe2b 100644 --- a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs +++ b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs @@ -37,11 +37,16 @@ namespace BaseGames.Enemies.Abilities public float CooldownRemaining => Mathf.Max(0f, _cooldownEndTime - Time.time); public bool IsOnCooldown => CooldownRemaining > 0f; - /// 能力被外部中断时触发(BD Task / 状态机订阅用)。 + /// 能力被外部中断时触发(AI 决策层 / 状态机订阅用)。 public event System.Action Interrupted; - /// BD 任务统一查询入口:当前是否可用(冷却完毕且未执行中)。 - public virtual bool CanUse => !_isRunning && !IsOnCooldown && _enemy != null && _enemy.IsAlive; + /// + /// 统一可用性查询:组件启用 + 冷却完毕 + 未执行中 + 宿主存活。 + /// enabled 这一维供阶段门使用——被阶段禁用的能力不得进入选招候选, + /// 否则选招器会选中它然后 StartCoroutine 在禁用组件上必然失败。 + /// + public virtual bool CanUse => enabled && !_isRunning && !IsOnCooldown + && _enemy != null && _enemy.IsAlive; // ── IAttackCandidate(供 EnemyAttackSelector 选招)────────────────── public bool RequiresLineOfSight => _config != null && _config.requiresLineOfSight;