using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using BaseGames.Enemies; using BaseGames.Enemies.Abilities; namespace BaseGames.Tests.EditMode.Enemies { /// /// 池化敌人复活后不得带着上一条命的能力冷却。 /// /// EnemyBase.OnSpawn 里 InterruptAll 那一行的注释写着「重置能力冷却」,实际三处都不成立: /// EnemyAbilityRegistry.InterruptAll 与 EnemyAbilityBase.Interrupt 各有一道 IsRunning 门, /// 出生时无一在跑,循环体一次都不执行;即便执行到,末行也是**设置**半冷却而非清零。 /// 冷却基于绝对 Time.time,于是上一条命的剩余冷却原样活到下一条命。 /// public class EnemyAbilityCooldownResetTests { /// 最小可实例化能力:不播动画、不碰 HitBox,只为验证冷却的跨池化生命周期。 private sealed class StubAbility : EnemyAbilityBase { /// 编辑模式下 AddComponent 不触发 Awake,手动跑一次真实的依赖解析路径。 public void RunAwake() => Awake(); /// 等价于 Inspector 对 [SerializeField] _config 的赋值(子类可见,非反射伪造)。 public void SetConfig(EnemyAbilitySO cfg) => _config = cfg; protected override IEnumerator ExecuteCoroutine() { yield break; } } private GameObject _host; private EnemyAbilitySO _config; // RunAwake 时找不到 AnimancerComponent 会告警,与本测试无关。 [SetUp] public void SetUp() => LogAssert.ignoreFailingMessages = true; [TearDown] public void TearDown() { // 项目关闭了 Domain/Scene Reload:断言失败会跳过用例后续语句, // 清理必须放在 TearDown,否则失败一次就往编辑器场景里漏一个对象。 if (_host != null) Object.DestroyImmediate(_host); if (_config != null) Object.DestroyImmediate(_config); _host = null; _config = null; LogAssert.ignoreFailingMessages = false; } /// /// 建一个能力已进入冷却的敌人。冷却全程由生产代码写入: /// Execute() 启真实协程(编辑模式下跑到首个 yield 即挂起,_isRunning 留 true), /// 再由 Interrupt() 经真实路径写 _cooldownEndTime。 /// 不反射直写冷却字段——那样测到的是伪造状态,而非真实时序产出的状态。 /// private (EnemyBase enemy, StubAbility ability) MakeEnemyWithAbilityOnCooldown() { _host = new GameObject("pooled-enemy"); var enemy = _host.AddComponent(); // 裸 EnemyBase 的 _currentState=Controlled → IsAlive 为真 var ab = _host.AddComponent(); _config = ScriptableObject.CreateInstance(); _config.abilityId = "stub_ability"; _config.cooldown = 10f; // 远长于单次用例耗时,排除"跑着跑着自然冷却完"的假绿 ab.SetConfig(_config); ab.RunAwake(); // EnemyBase.Awake 里的生产写法,编辑模式下手动跑一次 enemy.Abilities.CollectFrom(_host); Assert.IsTrue(ab.Execute(), "前提:能力应能启动,否则下面写不进冷却"); ab.Interrupt(InterruptReason.ExternalRequest); Assert.IsTrue(ab.IsOnCooldown, "前提:本用例覆盖的是带冷却复活,此刻必须真的在冷却中"); return (enemy, ab); } [Test] public void OnSpawn_ClearsAbilityCooldown() { var (enemy, ability) = MakeEnemyWithAbilityOnCooldown(); enemy.OnSpawn(); Assert.AreEqual(0f, ability.CooldownRemaining, "池化复活的敌人不得带着上一条命的剩余冷却——否则新生的敌人有数秒出不了招"); Assert.IsFalse(ability.IsOnCooldown); } } }