From 3d00c666773aff168bf9b26b6dacb8742fd94a85 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 13:08:46 +0800 Subject: [PATCH] =?UTF-8?q?test(enemy):=20=E8=A1=A5=E6=B5=8B=20Attack=20?= =?UTF-8?q?=E5=8D=95=E5=87=BA=E8=BE=B9=E7=9A=84=E8=87=AA=E6=84=88=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=8C=E5=86=99=E6=98=8E=E8=BE=B9=E5=BA=8F=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=9A=84=E5=B0=84=E7=A8=8B=E5=89=8D=E6=8F=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Approach 边序注释:attackInRange 先于 leftAllZones 的前提是攻击射程恒小于 感知区(EnemyAttackSelector.Eligible 的 InAttackRange 门),故两条件不会相争; 若未来出现射程更大的招式,应由 AI 配方校验器报错,而非靠调边序绕开。 FakeCombat 新增 BestAttackFails 开关(与 Eligible 解耦),补测 Attack 唯一出边 attackDone 的自愈不变量:UseBestAttack 触发失败时不留下运行中能力,下一帧即可 自愈回 Approach,不会永久卡死。 --- .../AI/ApproachAttackEngagementTests.cs | 18 ++++++++++++++++++ .../Tests/EditMode/AI/Fakes/FakeAiContext.cs | 3 ++- .../Engagement/ApproachAttackEngagement.cs | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) 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");