using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Plain-Entities EditMode tests for the server-only — the macro-loop director /// (Expedition → Defend → Build → next cycle). A bare world is seeded with a NetworkTime singleton and a cycle /// entity carrying CycleState + CycleRuntime (and optionally WaveState / GoalProgress). All timing is wrap-safe /// NetworkTick math; these tests pin each phase transition and the per-cycle goal-charge increment. /// public class CyclePhaseSystemTests { static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick) { 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) }); return (world, group); } static Entity MakeCycle(EntityManager em, byte phase, uint phaseEndTick, int cycleNumber, int defendStartWave) { var e = em.CreateEntity(typeof(CycleState), typeof(CycleRuntime)); em.SetComponentData(e, new CycleState { Phase = phase, PhaseEndTick = phaseEndTick, CycleNumber = cycleNumber }); em.SetComponentData(e, new CycleRuntime { DefendStartWave = defendStartWave }); return e; } static void MakeWaveState(EntityManager em, int waveNumber, int remainingToSpawn) { var e = em.CreateEntity(typeof(WaveState)); em.SetComponentData(e, new WaveState { WaveNumber = waveNumber, RemainingToSpawn = remainingToSpawn }); } [Test] public void Expedition_Enters_Defend_When_Timer_Due_Capturing_StartWave() { var (world, group) = MakeWorld("CycleExpToDefend", serverTick: 200); using (world) { var em = world.EntityManager; var cycle = MakeCycle(em, CyclePhase.Expedition, phaseEndTick: 100, cycleNumber: 1, defendStartWave: 0); MakeWaveState(em, waveNumber: 5, remainingToSpawn: 0); group.Update(); var cs = em.GetComponentData(cycle); Assert.AreEqual(CyclePhase.Defend, cs.Phase, "An expired Expedition timer enters Defend."); Assert.AreEqual(0u, cs.PhaseEndTick, "Defend is wave-driven, so PhaseEndTick is cleared."); Assert.AreEqual(5, em.GetComponentData(cycle).DefendStartWave, "DefendStartWave captures the current WaveState.WaveNumber."); } } [Test] public void Expedition_Holds_While_Timer_Pending() { var (world, group) = MakeWorld("CycleExpHold", serverTick: 200); using (world) { var em = world.EntityManager; var cycle = MakeCycle(em, CyclePhase.Expedition, phaseEndTick: 5000, cycleNumber: 1, defendStartWave: 0); group.Update(); Assert.AreEqual(CyclePhase.Expedition, em.GetComponentData(cycle).Phase, "Expedition holds until its timer is due."); } } [Test] public void Defend_Enters_Build_When_Wave_Cleared() { var (world, group) = MakeWorld("CycleDefendToBuild", serverTick: 200); using (world) { var em = world.EntityManager; var cycle = MakeCycle(em, CyclePhase.Defend, phaseEndTick: 0, cycleNumber: 1, defendStartWave: 1); // Wave advanced past the captured start, fully spawned, and no Husks alive (none created). MakeWaveState(em, waveNumber: 2, remainingToSpawn: 0); group.Update(); var cs = em.GetComponentData(cycle); Assert.AreEqual(CyclePhase.Build, cs.Phase, "A cleared Defend wave enters Build."); Assert.AreNotEqual(0u, cs.PhaseEndTick, "Build is timed, so a PhaseEndTick is stamped."); } } [Test] public void Build_Enters_Expedition_Incrementing_Cycle_And_Goal() { var (world, group) = MakeWorld("CycleBuildToExp", serverTick: 200); using (world) { var em = world.EntityManager; var cycle = MakeCycle(em, CyclePhase.Build, phaseEndTick: 100, cycleNumber: 1, defendStartWave: 0); em.AddComponentData(cycle, new GoalProgress { Charge = 0, Target = 10 }); group.Update(); var cs = em.GetComponentData(cycle); Assert.AreEqual(CyclePhase.Expedition, cs.Phase, "An expired Build timer starts the next Expedition."); Assert.AreEqual(2, cs.CycleNumber, "CycleNumber increments on the new cycle."); Assert.AreEqual(1, em.GetComponentData(cycle).Charge, "One goal charge accrues per completed cycle (single writer)."); } } [Test] public void WaveNumber_Is_Synced_From_WaveState_For_The_Hud() { var (world, group) = MakeWorld("CycleWaveSync", serverTick: 200); using (world) { var em = world.EntityManager; var cycle = MakeCycle(em, CyclePhase.Defend, phaseEndTick: 0, cycleNumber: 1, defendStartWave: 1); MakeWaveState(em, waveNumber: 4, remainingToSpawn: 2); group.Update(); Assert.AreEqual(4, em.GetComponentData(cycle).WaveNumber, "CycleState.WaveNumber mirrors the server-only WaveState.WaveNumber so the replicated-state-only HUD can show it."); } } } }