diff --git a/Assets/Tests/EditMode/AI/ApproachAttackEngagementTests.cs b/Assets/Tests/EditMode/AI/ApproachAttackEngagementTests.cs index 724e5edc..f084d8ea 100644 --- a/Assets/Tests/EditMode/AI/ApproachAttackEngagementTests.cs +++ b/Assets/Tests/EditMode/AI/ApproachAttackEngagementTests.cs @@ -88,5 +88,23 @@ namespace BaseGames.Tests.EditMode.AI rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); // 再经 Approach 自然脱战 } + + [Test] + public void Attack_SelfHealsToApproach_WhenNothingActuallyTriggered() + { + // Attack 只有 attackDone 一条出边,安全性依赖:"UseBestAttack 失败时不会留下运行中的能力"。 + // 若该不变量被破坏,敌人会永久卡在 Attack 里出不来。 + var ctx = new FakeAiContext(); + ctx.S.Chase = true; + ctx.C.Eligible = true; + ctx.C.BestAttackFails = true; // 够得着,但触发失败 + var rt = RunAlone(ctx); + rt.Tick(0.1f); + Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); + Assert.IsNull(ctx.C.Running); // 什么都没启动 + + rt.Tick(0.1f); + Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); // 下一帧自愈 + } } } diff --git a/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs b/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs index a0d037bf..34b6b28e 100644 --- a/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs +++ b/Assets/Tests/EditMode/AI/Fakes/FakeAiContext.cs @@ -26,10 +26,11 @@ namespace BaseGames.Tests.EditMode.AI public bool CanUseAbility(string id) => CanUse; public void InterruptAbilities() => Running = null; public bool Eligible; // 测试控制:是否有合格攻击 + public bool BestAttackFails; // 模拟:选招器认为够得着,但实际没触发成功 public bool HasEligibleAttack() => Eligible; public bool UseBestAttack() { - if (!Eligible) return false; + if (!Eligible || BestAttackFails) return false; Used.Add("best"); Running = "best"; return true; } } diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs index e473322f..96cf612a 100644 --- a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Engagement/ApproachAttackEngagement.cs @@ -25,6 +25,10 @@ namespace BaseGames.Enemies .OnEnter(x => x.Locomotion.Pursue(x.Sensor.LastKnown)) .Tick (x => x.Locomotion.Pursue(x.Sensor.LastKnown)) .OnExit(x => x.Locomotion.Stop()) + // 边序:attackInRange 先于 leftAllZones。前提是攻击射程恒小于感知区 + // (EnemyAttackSelector.Eligible 的 InAttackRange 门),故"够得着"不会在 + // "已脱离全部感知"之后仍成立,两条件不会相争。若将来出现射程大于追击区的招式, + // 那属于射程/感知区配置错误,由 AI 配方校验器报出,不要靠调整此处边序绕开。 .To(Attack).When(x => x.Combat.HasEligibleAttack(), "attackInRange") .To(rest) .When(AiStateFragments.LostAllZones, "leftAllZones");