using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Tests { /// /// MC-2 system tests for the SWARMER cluster spawn in the base-siege WaveSystem (fork 4a). A swarmer composition /// slot must instantiate a whole PACK in one tick (EnemyAIMath.ClusterOffset) while consuming exactly ONE wave /// SLOT, and MaxAlive must count ENTITIES — a pack that won't fit is DEFERRED (slot kept) rather than partially /// spawned (the review-flagged slot-vs-entity accounting). Plain-Entities world, server WaveSystem registered /// directly, faked NetworkTime; the 4-entry [Grunt,Charger,Spitter,Swarmer] roster is Prefab-tagged so the /// instances (and only the instances) count as live EnemyTag ghosts. /// public class SwarmerClusterSpawnTests { static void SetTick(World w, uint tick) { var em = w.EntityManager; using var q = em.CreateEntityQuery(typeof(NetworkTime)); Entity e = q.IsEmpty ? em.CreateEntity(typeof(NetworkTime)) : q.GetSingletonEntity(); em.SetComponentData(e, new NetworkTime { ServerTick = new NetworkTick(tick) }); } static (World, SimulationSystemGroup) WaveWorld(uint tick) { var w = new World("SwarmerCluster"); var g = w.GetOrCreateSystemManaged(); g.AddSystemToUpdateList(w.GetOrCreateSystem()); g.SortSystems(); w.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); SetTick(w, tick); return (w, g); } // Director with a swarmer-only band so slot 0 is unambiguously a swarmer pack. The 4-entry roster aliases // dummy Prefab-tagged EnemyTag prefabs; WaveSystem reads index [3] (KindSwarmer) and instantiates the pack. static Entity MakeDirector(EntityManager em, int swarmerSlotBase, int packSize, int maxAlive) { // Create the 4 prefab entities FIRST: every em.CreateEntity is a structural change, so a DynamicBuffer // handle grabbed before them would be invalidated (the bug this ordering avoids). The roster aliases // dummy Prefab-tagged EnemyTag prefabs; WaveSystem reads index [3] (KindSwarmer) and instantiates the pack. var prefabs = new Entity[4]; for (int i = 0; i < 4; i++) { var p = em.CreateEntity(); em.AddComponentData(p, LocalTransform.FromPosition(float3.zero)); em.AddComponent(p); em.AddComponent(p); prefabs[i] = p; } var dir = em.CreateEntity(); em.AddComponentData(dir, new WaveDirector { RingRadius = 10f, RingSlots = 8, BaseCount = 0, CountPerWave = 0, SpawnIntervalTicks = 1, LullTicks = 1, MaxAlive = maxAlive, ChargerBase = 0, SpitterBase = 0, SwarmerSlotBase = swarmerSlotBase, ChargerPerEpoch = 0, SpitterPerEpoch = 0, SwarmerSlotPerEpoch = 0, SwarmerPackSize = packSize, SwarmerPackPerEpoch = 0, ClusterTightRadius = 2.5f, }); em.AddComponentData(dir, new WaveState { WaveNumber = 0, Phase = WavePhase.Lull, NextActionTick = 0u, RemainingToSpawn = 0, SpawnCounter = 0 }); // AddBuffer LAST (after every structural change on dir), then populate with no further structural change. var buf = em.AddBuffer(dir); for (int i = 0; i < 4; i++) buf.Add(new WaveEnemyPrefab { Prefab = prefabs[i] }); return dir; } static int CountEnemies(EntityManager em) { using var q = em.CreateEntityQuery(ComponentType.ReadOnly()); return q.CalculateEntityCount(); } [Test] public void Swarmer_Slot_SpawnsWholePack_ConsumesOneSlot() { var (w, g) = WaveWorld(200); using (w) { var em = w.EntityManager; var dir = MakeDirector(em, swarmerSlotBase: 1, packSize: 4, maxAlive: 12); g.Update(); // Lull -> start wave: RemainingToSpawn = WaveSlots(1) = 1 swarmer slot Assert.AreEqual(1, em.GetComponentData(dir).RemainingToSpawn, "one swarmer SLOT this wave"); g.Update(); // Spawning -> the pack lands in one tick Assert.AreEqual(4, CountEnemies(em), "the whole pack spawns in a single tick"); var st = em.GetComponentData(dir); Assert.AreEqual(1, st.SpawnCounter, "exactly ONE slot consumed for the pack"); Assert.AreEqual(0, st.RemainingToSpawn, "the swarmer slot is done"); } } [Test] public void Swarmer_PackOverMaxAlive_Defers_KeepsSlot() { var (w, g) = WaveWorld(200); using (w) { var em = w.EntityManager; var dir = MakeDirector(em, swarmerSlotBase: 1, packSize: 4, maxAlive: 3); // pack(4) > cap(3) g.Update(); // start wave g.Update(); // try to spawn -> 0 + 4 > 3 -> defer (don't partially spawn, don't consume the slot) Assert.AreEqual(0, CountEnemies(em), "a pack that won't fit MaxAlive is NOT partially spawned"); var st = em.GetComponentData(dir); Assert.AreEqual(0, st.SpawnCounter, "the slot is NOT consumed when deferred"); Assert.AreEqual(1, st.RemainingToSpawn, "the swarmer slot remains pending"); } } } }