using NUnit.Framework; using BaseGames.AI; using BaseGames.Enemies; namespace BaseGames.Tests.EditMode.AI { public class RushEngagementTests { const string Rest = "Rest"; // 单独跑交战模块:手搓一个最小 rest 态,直接以交战入口为图入口。 static AiRuntime RunAlone(RushEngagement m, FakeAiContext ctx) { var b = new BrainBuilder(); AiStateFragments.Locomotion(b, Rest, LocomotionMode.Patrol); m.Build(b, Rest); b.Entry(m.EntryState); return new AiRuntime(b.Build(), ctx); } [Test] public void OnLostTarget_CanEngage_IsChaseZoneOnly() { var m = new RushEngagement("rush", RushExit.OnLostTarget); var ctx = new FakeAiContext(); ctx.C.CanUse = false; // 冷却中 ctx.S.Chase = true; Assert.IsTrue(m.CanEngage(ctx)); // 不看冷却 Assert.AreEqual("InChaseZone", m.EngageLabel); } [Test] public void Committed_CanEngage_AlsoRequiresOffCooldown() { var m = new RushEngagement("rush", RushExit.Committed); var ctx = new FakeAiContext(); ctx.S.Chase = true; ctx.C.CanUse = false; Assert.IsFalse(m.CanEngage(ctx)); // CD 中不进 ctx.C.CanUse = true; Assert.IsTrue(m.CanEngage(ctx)); Assert.AreEqual("InChaseZone+offCD", m.EngageLabel); } [Test] public void OnLostTarget_ExitsWhenAllZonesLost() { var ctx = new FakeAiContext { }; ctx.S.Chase = true; var rt = RunAlone(new RushEngagement("rush", RushExit.OnLostTarget), ctx); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); rt.Tick(0.1f); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); // 还在追逐区 ctx.S.Chase = false; ctx.S.Vision = false; rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); } [Test] public void Committed_NotInterruptedByLosingTarget() { var ctx = new FakeAiContext(); ctx.S.Chase = true; var rt = RunAlone(new RushEngagement("rush", RushExit.Committed), ctx); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); ctx.S.Chase = false; ctx.S.Vision = false; // 中途丢失感知 rt.Tick(0.1f); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); // 冲锋不被打断 ctx.C.Running = null; // 冲锋跑完 rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); } [Test] public void Build_Throws_WhenAbilityMissing() { var b = new BrainBuilder(); AiStateFragments.Locomotion(b, Rest, LocomotionMode.Patrol); var m = new RushEngagement(); // 未配能力 Assert.Throws(() => m.Build(b, Rest)); } [Test] public void Committed_CooldownGate_BreaksRushRestThrashLoop() { // Committed 存在的全部理由:冲锋打完回未发现态后,若进入条件不带冷却门, // 下一帧仍在追逐区就会立刻再冲,形成 Rush↔Rest 每帧抖动。 // 本用例补上骨架会挂的那条进入边,跑完整循环。 var m = new RushEngagement("rush", RushExit.Committed); var ctx = new FakeAiContext(); var b = new BrainBuilder(); AiStateFragments.Locomotion(b, Rest, LocomotionMode.Patrol) .To(m.EntryState).When(m.CanEngage, m.EngageLabel); // 骨架的升级边 m.Build(b, Rest); b.Entry(Rest); var rt = new AiRuntime(b.Build(), ctx); ctx.S.Chase = true; // 进追逐区且冷却就绪 rt.Tick(0.1f); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); ctx.C.Running = null; // 冲锋跑完 ctx.C.CanUse = false; // 能力进入冷却 rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); // 关键断言:仍在追逐区、但冷却未好 → 不得再冲(否则就是每帧抖动) rt.Tick(0.1f); rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); ctx.C.CanUse = true; // 冷却结束,仍在追逐区 rt.Tick(0.1f); Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); } } }