diff --git a/Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs b/Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs
index b1a7b572..ca004669 100644
--- a/Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs
+++ b/Assets/Tests/EditMode/AI/PerceptionStateMachineTests.cs
@@ -357,6 +357,33 @@ namespace BaseGames.Tests.EditMode.AI
Assert.AreEqual("Death", rt.CurrentStateName);
}
+ // 进 Attack 后玩家脱离全部感知区:攻击仍在跑 → 不回 Approach,改判 leftAllZones → Rest(Patrol)。
+ [Test]
+ public void Attack_ToRest_WhenLeftAllZones()
+ {
+ var ctx = new Ctx();
+ var rt = new AiRuntime(GraphApproach(), ctx);
+ ctx.S.Chase = true; rt.Tick(0.1f); // → Approach
+ ctx.C.Eligible = true; rt.Tick(0.1f); // → Attack, Running="best"
+ Assert.AreEqual("Attack", rt.CurrentStateName);
+ ctx.S.Chase = false; ctx.S.Vision = false; // 攻击仍 running,但脱离全部感知
+ rt.Tick(0.1f);
+ Assert.AreEqual("Patrol", rt.CurrentStateName);
+ }
+
+ // 死亡是全局事件,从 Attack 态也能达 Death。
+ [Test]
+ public void ApproachAttack_Death_FromAttack()
+ {
+ var ctx = new Ctx();
+ var rt = new AiRuntime(GraphApproach(), ctx);
+ ctx.S.Chase = true; rt.Tick(0.1f); // → Approach
+ ctx.C.Eligible = true; rt.Tick(0.1f); // → Attack
+ Assert.AreEqual("Attack", rt.CurrentStateName);
+ rt.Send(AiSignal.Died); rt.Tick(0.1f);
+ Assert.AreEqual("Death", rt.CurrentStateName);
+ }
+
// ── 死亡(全局事件,任意态可达)───────────────────────────────────
[Test]
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs
index 24a4e7f8..30dc8e08 100644
--- a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs
@@ -26,15 +26,14 @@ namespace BaseGames.Enemies
/// 每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时(供需要的敌人用)。
public void Refresh(float dt)
{
- if (InVisionZone() && _enemy.PlayerTransform != null)
- {
+ bool vision = InVisionZone();
+ // 追逐区(aggro)或视野内都刷新最后已知位置:Approach 逼近态仅由 aggro 门控进入,
+ // 若只在视野时刷新,aggro 先于视野命中时 Pursue 会拿到 zero/过期点而扑向错误位置。
+ if ((vision || InChaseZone()) && _enemy.PlayerTransform != null)
_lastKnown = _enemy.PlayerTransform.position;
- _lostTimer = 0f;
- }
- else
- {
- _lostTimer += dt;
- }
+ // 丢失计时仍以视野为准(LostFor 语义=脱离视野的时长,不受 aggro 影响)。
+ if (vision) _lostTimer = 0f;
+ else _lostTimer += dt;
}
/// 对象池复用时清空临时态。
diff --git a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAttackSelector.cs b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAttackSelector.cs
index 310bff99..d10ee61c 100644
--- a/Assets/_Game/Scripts/Enemies/Abilities/EnemyAttackSelector.cs
+++ b/Assets/_Game/Scripts/Enemies/Abilities/EnemyAttackSelector.cs
@@ -53,7 +53,7 @@ namespace BaseGames.Enemies.Abilities
var c = _candidates[i];
if (Eligible(c, hasLOS, grounded)) total += Mathf.Max(0f, c.Weight);
}
- if (total <= 0f) return SelectByPriority(hasLOS, grounded); // 权重全 0 → 退化为确定性取首个合格
+ if (total <= 0f) return SelectByPriority(hasLOS, grounded); // 权重全 0 → 退化为按 Priority 选(并列取首个)
float roll = Random.value * total;
for (int i = 0; i < _candidates.Count; i++)
{