using NUnit.Framework; using BaseGames.AI; using BaseGames.Enemies; namespace BaseGames.Tests.EditMode.AI { public class ApproachAttackEngagementTests { const string Rest = "Rest"; static AiRuntime RunAlone(FakeAiContext ctx) { var m = new ApproachAttackEngagement(); 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 Approach_PursuesLastKnown() { var ctx = new FakeAiContext(); ctx.S.Chase = true; ctx.S.Last = new UnityEngine.Vector2(5f, 1f); RunAlone(ctx); Assert.AreEqual(new UnityEngine.Vector2(5f, 1f), ctx.L.PursuedTo); } [Test] public void Approach_ToAttack_WhenEligibleAttackExists() { var ctx = new FakeAiContext(); ctx.S.Chase = true; var rt = RunAlone(ctx); Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); ctx.C.Eligible = true; rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); CollectionAssert.Contains(ctx.C.Used, "best"); } [Test] public void Attack_BackToApproach_WhenAbilityDone() { var ctx = new FakeAiContext(); ctx.S.Chase = true; ctx.C.Eligible = true; var rt = RunAlone(ctx); rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); ctx.C.Running = null; // 招式打完 rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); } [Test] public void Approach_ExitsToRest_WhenAllZonesLost() { var ctx = new FakeAiContext(); ctx.S.Chase = true; var rt = RunAlone(ctx); ctx.S.Chase = false; ctx.S.Vision = false; rt.Tick(0.1f); Assert.AreEqual(Rest, rt.CurrentStateName); } // 有意的行为变更:攻击起手后打完,不中途收招 [Test] public void Attack_CompletesEvenWhenPlayerLeavesAllZones() { var ctx = new FakeAiContext(); ctx.S.Chase = true; ctx.C.Eligible = true; var rt = RunAlone(ctx); rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); ctx.S.Chase = false; ctx.S.Vision = false; // 前摇中玩家跑掉 // 真实系统里攻击射程远小于感知区,脱离全部感知区必然也脱离攻击射程, // HasEligibleAttack() 会随之变假。FakeCombat.Eligible 是裸字段不会联动,这里手工对齐。 ctx.C.Eligible = false; rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); // 不中途收招 ctx.C.Running = null; // 招式打完 rt.Tick(0.1f); Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); 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); // 下一帧自愈 } } }