using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Tests { /// /// Plain-Entities EditMode tests for the server-only (Husk wave/threat director). /// A bare world is seeded with NetworkTime + CycleState singletons and a director entity carrying /// WaveDirector + WaveState + a WaveEnemyPrefab buffer (whose prefab is a real Prefab-tagged entity so /// it is excluded from the alive-Husk query and Instantiate yields plain Husk instances). Pins: a due Lull /// starts the next (escalating) wave; Spawning emits one Husk per interval; the director is gated off outside /// Defend; a fully-spawned, cleared wave returns to Lull. /// public class WaveSystemTests { static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick, byte cyclePhase) { var world = new World(name); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); var em = world.EntityManager; var nt = em.CreateEntity(typeof(NetworkTime)); em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(serverTick) }); var cyc = em.CreateEntity(typeof(CycleState)); em.SetComponentData(cyc, new CycleState { Phase = cyclePhase }); return (world, group); } static Entity MakeHuskPrefab(EntityManager em) { var e = em.CreateEntity(typeof(LocalTransform), typeof(EnemyTag)); em.AddComponent(e); // real prefab: excluded from EnemyTag queries; Instantiate strips the tag return e; } static Entity MakeDirector(EntityManager em, Entity huskPrefab, byte phase, int waveNumber, uint nextActionTick, int remainingToSpawn, int spawnCounter) { var e = em.CreateEntity(typeof(WaveDirector), typeof(WaveState)); em.SetComponentData(e, new WaveDirector { RingRadius = 10f, RingSlots = 12, BaseCount = 3, CountPerWave = 2, SpawnIntervalTicks = 10, LullTicks = 5, }); em.SetComponentData(e, new WaveState { Phase = phase, WaveNumber = waveNumber, NextActionTick = nextActionTick, RemainingToSpawn = remainingToSpawn, SpawnCounter = spawnCounter, }); var buf = em.AddBuffer(e); buf.Add(new WaveEnemyPrefab { Prefab = huskPrefab }); return e; } static int HuskCount(EntityManager em) { using var q = em.CreateEntityQuery(typeof(EnemyTag)); return q.CalculateEntityCount(); } [Test] public void Due_Lull_Starts_Wave_With_Escalating_Count() { var (world, group) = MakeWorld("WaveLullStart", serverTick: 100, cyclePhase: CyclePhase.Defend); using (world) { var em = world.EntityManager; var prefab = MakeHuskPrefab(em); var dir = MakeDirector(em, prefab, WavePhase.Lull, waveNumber: 0, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 0); group.Update(); var w = em.GetComponentData(dir); Assert.AreEqual(WavePhase.Spawning, w.Phase, "A due lull starts spawning."); Assert.AreEqual(1, w.WaveNumber, "Wave number advances."); Assert.AreEqual(3, w.RemainingToSpawn, "Wave 1 count = BaseCount + (1-1)*CountPerWave = 3."); Assert.AreEqual(0, HuskCount(em), "No Husk is spawned on the lull->spawning transition tick itself."); } } [Test] public void Spawning_Emits_One_Husk_And_Decrements_Remaining() { var (world, group) = MakeWorld("WaveSpawnOne", serverTick: 100, cyclePhase: CyclePhase.Defend); using (world) { var em = world.EntityManager; var prefab = MakeHuskPrefab(em); var dir = MakeDirector(em, prefab, WavePhase.Spawning, waveNumber: 1, nextActionTick: 100, remainingToSpawn: 3, spawnCounter: 0); group.Update(); Assert.AreEqual(1, HuskCount(em), "One Husk spawns this interval."); var w = em.GetComponentData(dir); Assert.AreEqual(2, w.RemainingToSpawn, "RemainingToSpawn decrements."); Assert.AreEqual(1, w.SpawnCounter, "SpawnCounter advances (drives ring slot + round-robin)."); } } [Test] public void Director_Is_Gated_Off_Outside_Defend() { var (world, group) = MakeWorld("WaveGated", serverTick: 100, cyclePhase: CyclePhase.Expedition); using (world) { var em = world.EntityManager; var prefab = MakeHuskPrefab(em); var dir = MakeDirector(em, prefab, WavePhase.Lull, waveNumber: 0, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 0); group.Update(); var w = em.GetComponentData(dir); Assert.AreEqual(WavePhase.Lull, w.Phase, "Outside Defend the director does not run."); Assert.AreEqual(0, w.WaveNumber, "Wave number stays put outside Defend."); } } [Test] public void Fully_Spawned_Cleared_Wave_Returns_To_Lull() { var (world, group) = MakeWorld("WaveCleared", serverTick: 100, cyclePhase: CyclePhase.Defend); using (world) { var em = world.EntityManager; var prefab = MakeHuskPrefab(em); var dir = MakeDirector(em, prefab, WavePhase.Spawning, waveNumber: 1, nextActionTick: 100, remainingToSpawn: 0, spawnCounter: 3); group.Update(); Assert.AreEqual(WavePhase.Lull, em.GetComponentData(dir).Phase, "A fully-spawned wave with no live Husks returns to Lull."); } } } }