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++) {