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");
}
}
}
}