diff --git a/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs b/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs
new file mode 100644
index 00000000..33e32b7d
--- /dev/null
+++ b/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs
@@ -0,0 +1,91 @@
+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);
+ }
+ }
+}
diff --git a/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs.meta b/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs.meta
new file mode 100644
index 00000000..8005e64f
--- /dev/null
+++ b/Assets/Tests/EditMode/Enemies/EnemyAbilityCooldownResetTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b8c4bfbd21220fd429f7f845fee03cff
+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 814cb5be..bf823aa6 100644
--- a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs
+++ b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityBase.cs
@@ -155,6 +155,15 @@ namespace BaseGames.Enemies.Abilities
protected virtual void OnInterrupted(InterruptReason reason) { }
+ ///
+ /// 把冷却清回出生态,使能力立刻可用。
+ /// 供对象池复活()调用:冷却以绝对 Time.time 记时,
+ /// 不显式清零就会原样活过 despawn/spawn,让新生的敌人带着上一条命的剩余冷却。
+ /// 注意不能靠 代劳——它有 _isRunning 门(出生时无一在跑),
+ /// 且其语义是"中断后计半程冷却",是写入冷却而非清除。
+ ///
+ public void ResetCooldown() => _cooldownEndTime = -1f;
+
/// 子类辅助:朝向目标(写入输入信号,下一 FixedUpdate 由 EnemyMovement 消费)。
protected void FaceTarget(Transform target)
{
diff --git a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityRegistry.cs b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityRegistry.cs
index 15e9a290..2ea275f6 100644
--- a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityRegistry.cs
+++ b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAbilityRegistry.cs
@@ -61,6 +61,17 @@ namespace BaseGames.Enemies.Abilities
}
}
+ ///
+ /// 清空所有能力的冷却。对象池复活时调用。
+ /// 与 的区别是**不看 IsRunning**——出生时没有能力在跑,
+ /// 那道门会让中断路径一次都不执行,正是冷却跨命残留的成因。
+ ///
+ public void ResetAllCooldowns()
+ {
+ for (int i = 0; i < _all.Count; i++)
+ if (_all[i] != null) _all[i].ResetCooldown();
+ }
+
///
/// 中断指定互斥组内所有正在执行的能力。
/// 由 在 Execute 开始时调用,确保同组互斥。
diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs
index 868d5602..aa6a7f44 100644
--- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs
+++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs
@@ -763,8 +763,11 @@ namespace BaseGames.Enemies
if (_stats != null && _statsSO != null)
_stats.Initialize(_statsSO);
- // 重置能力冷却
+ // 上一条命若在出招途中被回收,OnDespawn 与组件 OnDisable 已经中断过;此处为兜底中断。
+ // 它**不**负责重置冷却:两侧的 IsRunning 门让它在出生时恒为空转(曾被误注释为"重置能力冷却")。
_abilities.InterruptAll(InterruptReason.Dead);
+ // 冷却以绝对时间记,必须显式清零,否则新生的敌人带着上一条命的剩余冷却出不了招
+ _abilities.ResetAllCooldowns();
// 对象池复用不得继承上一条命的防重复记忆(否则新生即带着上次的招被折扣)
_attackSelector?.ResetRepeatMemory();