diff --git a/Assets/_Project/Tests/EditMode/BossAISystemTests.cs b/Assets/_Project/Tests/EditMode/BossAISystemTests.cs
new file mode 100644
index 000000000..7677c1dfe
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/BossAISystemTests.cs
@@ -0,0 +1,159 @@
+using NUnit.Framework;
+using ProjectM.Server;
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Transforms;
+
+namespace ProjectM.Tests
+{
+ ///
+ /// Plain-Entities EditMode coverage for the server-only BossAISystem (the sole boss mover/attacker, previously
+ /// Play-only). Exercises phase gating, expedition-only targeting, the telegraphed radial-slam AoE + its radius
+ /// boundary, the B4 lunge-vs-slam windup disambiguation (a naive shared-windup reuse would slam on a lunge
+ /// elapse), and knockback immunity. Values are pinned to Tuning.Boss* so the tests track tuning, not literals.
+ /// No PhysicsWorldSingleton is created -> sweep=false -> the boss moves/lands unswept (headless-safe).
+ ///
+ public class BossAISystemTests
+ {
+ static Entity MakeBoss(EntityManager em, float3 pos, float cur, float max,
+ byte pending = 0, uint windup = 0, uint slamReady = 0, uint lungeReady = 0, uint knockUntil = 0)
+ {
+ var e = em.CreateEntity(typeof(EnemyTag), typeof(BossState), typeof(LocalTransform), typeof(EnemyStats),
+ typeof(Health), typeof(AttackWindup), typeof(KnockbackState), typeof(LungeState));
+ em.SetComponentData(e, LocalTransform.FromPosition(pos));
+ em.SetComponentData(e, new EnemyStats { MoveSpeed = 3f, AttackRange = 2f, AttackDamage = 10f, AttackCooldownTicks = 30 });
+ em.SetComponentData(e, new Health { Current = cur, Max = max });
+ em.SetComponentData(e, new BossState { Phase = 1, PendingAttack = pending, SlamReadyTick = slamReady, LungeReadyTick = lungeReady });
+ em.SetComponentData(e, new AttackWindup { WindUpUntilTick = windup });
+ em.SetComponentData(e, new KnockbackState { UntilTick = knockUntil });
+ return e;
+ }
+
+ static Entity MakePlayer(EntityManager em, float3 pos, byte region = RegionId.Expedition, float hp = 100f)
+ {
+ var e = em.CreateEntity(typeof(PlayerTag), typeof(RegionTag), typeof(LocalTransform), typeof(Health), typeof(DamageEvent));
+ em.SetComponentData(e, new RegionTag { Region = region });
+ em.SetComponentData(e, LocalTransform.FromPosition(pos));
+ em.SetComponentData(e, new Health { Current = hp, Max = hp });
+ return e;
+ }
+
+ [Test]
+ public void Phase_One_When_Above_Half_HP()
+ {
+ var (world, group) = TestWorld.Make("Boss_P1", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ var boss = MakeBoss(em, float3.zero, cur: 60f, max: 100f);
+ MakePlayer(em, new float3(3f, 0f, 0f));
+ group.Update();
+ Assert.AreEqual((byte)1, em.GetComponentData(boss).Phase, "Above the phase-2 fraction -> phase 1.");
+ }
+ }
+
+ [Test]
+ public void Phase_Two_At_Or_Below_Half_HP()
+ {
+ var (world, group) = TestWorld.Make("Boss_P2", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ float cur = 100f * Tuning.BossPhase2HealthFraction; // exactly the boundary
+ var boss = MakeBoss(em, float3.zero, cur: cur, max: 100f);
+ MakePlayer(em, new float3(3f, 0f, 0f));
+ group.Update();
+ Assert.AreEqual((byte)2, em.GetComponentData(boss).Phase, "At/below the fraction -> phase 2.");
+ }
+ }
+
+ [Test]
+ public void No_Living_Expedition_Target_Leaves_Boss_Idle()
+ {
+ var (world, group) = TestWorld.Make("Boss_NoTgt", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ var boss = MakeBoss(em, float3.zero, cur: 100f, max: 100f);
+ MakePlayer(em, new float3(3f, 0f, 0f), region: RegionId.Base); // wrong region
+ MakePlayer(em, new float3(4f, 0f, 0f), region: RegionId.Expedition, hp: 0f); // dead
+
+ group.Update();
+
+ Assert.AreEqual(float3.zero, em.GetComponentData(boss).Position, "No valid target -> the boss does not move.");
+ Assert.AreEqual(0u, em.GetComponentData(boss).WindUpUntilTick, "No target -> no telegraph.");
+ }
+ }
+
+ [Test]
+ public void Slam_Lands_Radial_AoE_On_Windup_Elapse()
+ {
+ var (world, group) = TestWorld.Make("Boss_Slam", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ var boss = MakeBoss(em, float3.zero, cur: 100f, max: 100f, pending: 0, windup: 100); // 100 <= 200 -> elapsed
+ var player = MakePlayer(em, new float3(1f, 0f, 0f)); // inside slam radius
+
+ group.Update();
+
+ var dmg = em.GetBuffer(player);
+ Assert.AreEqual(1, dmg.Length, "A player inside the slam radius takes one hit on landing.");
+ Assert.AreEqual(Tuning.BossSlamDamage, dmg[0].Amount, 1e-3f);
+ Assert.AreEqual(-1, dmg[0].SourceNetworkId, "Slam damage is sourced from the boss/environment (-1).");
+ Assert.AreEqual(0u, em.GetComponentData(boss).WindUpUntilTick, "Windup cleared after landing.");
+ }
+ }
+
+ [Test]
+ public void Slam_Misses_A_Player_Outside_The_Radius()
+ {
+ var (world, group) = TestWorld.Make("Boss_SlamMiss", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ MakeBoss(em, float3.zero, cur: 100f, max: 100f, pending: 0, windup: 100);
+ var player = MakePlayer(em, new float3(Tuning.BossSlamRadius + 1f, 0f, 0f)); // just outside
+
+ group.Update();
+
+ Assert.AreEqual(0, em.GetBuffer(player).Length, "Outside the slam radius takes no hit.");
+ }
+ }
+
+ [Test]
+ public void Lunge_Windup_Elapse_Commits_Travel_And_Does_Not_Slam()
+ {
+ var (world, group) = TestWorld.Make("Boss_Lunge", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ var boss = MakeBoss(em, float3.zero, cur: 100f, max: 100f, pending: 1, windup: 100); // PendingAttack=1 => lunge
+ var player = MakePlayer(em, new float3(1f, 0f, 0f)); // inside slam radius, but a LUNGE must not slam
+
+ group.Update();
+
+ Assert.AreEqual(0, em.GetBuffer(player).Length, "A lunge elapse must NOT deal slam damage (B4 disambiguation).");
+ Assert.AreEqual(Tuning.BossLungeSpeed, em.GetComponentData(boss).Speed, 1e-3f, "Lunge travel committed.");
+ Assert.AreEqual(0u, em.GetComponentData(boss).WindUpUntilTick, "Windup cleared on lunge commit.");
+ }
+ }
+
+ [Test]
+ public void Knockback_Residual_Is_Zeroed_Boss_Is_Immune()
+ {
+ var (world, group) = TestWorld.Make("Boss_Knock", tick: 200, server: true);
+ using (world)
+ {
+ var em = world.EntityManager;
+ var boss = MakeBoss(em, float3.zero, cur: 100f, max: 100f, knockUntil: 999u);
+ MakePlayer(em, new float3(3f, 0f, 0f));
+
+ group.Update();
+
+ Assert.AreEqual(0u, em.GetComponentData(boss).UntilTick, "The boss is knockback-immune: residual is zeroed.");
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Tests/EditMode/BossAISystemTests.cs.meta b/Assets/_Project/Tests/EditMode/BossAISystemTests.cs.meta
new file mode 100644
index 000000000..c84c70e56
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/BossAISystemTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 407138571a76c6f4f821fcbde92327a5
\ No newline at end of file