diff --git a/Assets/Tests/EditMode/AI/PerceptionSkeletonTests.cs b/Assets/Tests/EditMode/AI/PerceptionSkeletonTests.cs index b3f49a15..882dd2ab 100644 --- a/Assets/Tests/EditMode/AI/PerceptionSkeletonTests.cs +++ b/Assets/Tests/EditMode/AI/PerceptionSkeletonTests.cs @@ -216,5 +216,71 @@ namespace BaseGames.Tests.EditMode.AI Assert.Throws(() => PerceptionSkeleton.Add( b, null, new RushEngagement("rush"))); } + + // ── 模块组合:抽象的存在理由,必须端到端验证 ───────────────────── + + [Test] + public void Skeleton_WithApproachAttack_EscalatesAndDisengages() + { + var ctx = new Ctx(); + var rt = Build(ctx, new SinglePost(LocomotionMode.Patrol), new ApproachAttackEngagement()); + Assert.AreEqual(SinglePost.Post, rt.CurrentStateName); + + ctx.S.Chase = true; rt.Tick(0.1f); + Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); + + ctx.C.Eligible = true; rt.Tick(0.1f); + Assert.AreEqual(ApproachAttackEngagement.Attack, rt.CurrentStateName); + + ctx.C.Running = null; rt.Tick(0.1f); // 招式打完 + Assert.AreEqual(ApproachAttackEngagement.Approach, rt.CurrentStateName); + + ctx.S.Chase = false; ctx.S.Vision = false; ctx.C.Eligible = false; + rt.Tick(0.1f); + Assert.AreEqual(SinglePost.Post, rt.CurrentStateName); // 脱战回 Rest + } + + [Test] + public void Skeleton_WithDisguiseThenPatrol_DisengagesToPatrol_NotDisguise() + { + // 唯一 Entry != Rest 的未发现层:出生伪装静止,脱战只回巡逻,伪装态从此不可达。 + var ctx = new Ctx(); + var rt = Build(ctx, new DisguiseThenPatrol()); + Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName); + + ctx.S.Chase = true; rt.Tick(0.1f); + Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); + + ctx.S.Chase = false; ctx.S.Vision = false; rt.Tick(0.1f); + Assert.AreEqual(DisguiseThenPatrol.Patrol, rt.CurrentStateName); // 不是 Disguise + + // 再交战一轮,仍然只回巡逻——伪装态不可逆 + ctx.S.Chase = true; rt.Tick(0.1f); + Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); + ctx.S.Chase = false; rt.Tick(0.1f); + Assert.AreEqual(DisguiseThenPatrol.Patrol, rt.CurrentStateName); + } + + [Test] + public void EntryCanBeOverridden_ForEnemiesWithPrefixStates() + { + // 骨架注释承诺的出路:掉落链 / 出场链这类前置态,调 Add 之后再覆盖 Entry。 + var ctx = new Ctx(); + var b = new BrainBuilder(); + PerceptionSkeleton.Add(b, new SinglePost(LocomotionMode.Patrol), + new RushEngagement("rush", RushExit.OnLostTarget)); + AiStateFragments.Locomotion(b, "Ceiling", LocomotionMode.Idle) + .To(SinglePost.Post).When(x => x.Sensor.SeesPlayer(), "landed"); + b.Entry("Ceiling"); // 覆盖骨架设的入口 + + var rt = new AiRuntime(b.Build(), ctx); + Assert.AreEqual("Ceiling", rt.CurrentStateName); + + ctx.S.Sees = true; rt.Tick(0.1f); + Assert.AreEqual(SinglePost.Post, rt.CurrentStateName); // 落地后并入感知层 + + ctx.S.Chase = true; rt.Tick(0.1f); + Assert.AreEqual(RushEngagement.Rush, rt.CurrentStateName); // 升级边照常生效 + } } }