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 { /// /// END-1 — plain-Entities EditMode tests for the Engine Core server systems. : /// a Husk that reaches the base drains integrity (the live /// default with no singleton) and is consumed; a distant Husk is untouched; at 0 /// the system idles (the lose-edge owns resolution). : the Core regenerates /// exactly +1 across one regen interval ONLY in Calm, never mid-Siege, and never past Max. The lose-edge /// itself is covered in CyclePhaseSystemTests. BaseAnchor is configured so PlotCenter == origin. /// public class CoreSystemsTests { static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick) where T : unmanaged, ISystem { var world = new World(name); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); SetServerTick(world, serverTick); return (world, group); } static void SetServerTick(World world, uint tick) { var em = world.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 Entity MakeCore(EntityManager em, int current, int max) { var e = em.CreateEntity(typeof(CoreIntegrity)); em.SetComponentData(e, new CoreIntegrity { Current = current, Max = max }); return e; } // PlotCenter = GridOrigin.xz + GridDims*CellSize*0.5; origin + zero dims => (0,0,0). static void MakeBaseAnchor(EntityManager em) { var e = em.CreateEntity(typeof(BaseAnchor)); em.SetComponentData(e, new BaseAnchor { AnchorPos = float3.zero, GridOrigin = float3.zero, CellSize = 1f, GridDims = int2.zero, }); } static void MakeHusk(EntityManager em, float3 pos) { var e = em.CreateEntity(typeof(EnemyTag), typeof(LocalTransform)); em.SetComponentData(e, LocalTransform.FromPosition(pos)); } static Entity MakeCycle(EntityManager em, byte phase) { var e = em.CreateEntity(typeof(CycleState)); em.SetComponentData(e, new CycleState { Phase = phase }); return e; } [Test] public void CoreDamage_Breaching_Husk_Drains_And_Is_Consumed() { var (world, group) = MakeWorld("CoreDamage", serverTick: 100); using (world) { var em = world.EntityManager; var core = MakeCore(em, current: 100, max: 100); MakeBaseAnchor(em); MakeHusk(em, new float3(0, 0, 0)); // at the Core -> breaches MakeHusk(em, new float3(20, 0, 20)); // far -> safe group.Update(); Assert.AreEqual(90, em.GetComponentData(core).Current, "one breaching Husk drains the default 10 integrity."); using var hq = em.CreateEntityQuery(typeof(EnemyTag)); Assert.AreEqual(1, hq.CalculateEntityCount(), "the breaching Husk is consumed; the distant one survives."); } } [Test] public void CoreDamage_Idles_When_Already_Breached() { var (world, group) = MakeWorld("CoreBreached", serverTick: 100); using (world) { var em = world.EntityManager; MakeCore(em, current: 0, max: 100); MakeBaseAnchor(em); MakeHusk(em, float3.zero); group.Update(); using var hq = em.CreateEntityQuery(typeof(EnemyTag)); Assert.AreEqual(1, hq.CalculateEntityCount(), "at 0 integrity CoreDamageSystem idles (the lose-edge owns resolution)."); } } [Test] public void CoreRestore_Regens_Exactly_Once_Per_Interval_In_Calm() { var (world, group) = MakeWorld("CoreRegenCalm", serverTick: 100); using (world) { var em = world.EntityManager; var core = MakeCore(em, current: 50, max: 100); MakeCycle(em, CyclePhase.Calm); // Across one full default interval (18) of consecutive ticks, exactly ONE is on the regen boundary. const uint interval = 18; for (uint t = 100; t < 100 + interval; t++) { SetServerTick(world, t); group.Update(); } Assert.AreEqual(51, em.GetComponentData(core).Current, "Calm regenerates exactly +1 across one regen interval."); } } [Test] public void CoreRestore_Does_Not_Regen_During_Siege() { var (world, group) = MakeWorld("CoreNoRegenSiege", serverTick: 100); using (world) { var em = world.EntityManager; var core = MakeCore(em, current: 50, max: 100); MakeCycle(em, CyclePhase.Siege); for (uint t = 100; t < 100 + 18; t++) { SetServerTick(world, t); group.Update(); } Assert.AreEqual(50, em.GetComponentData(core).Current, "no regen mid-Siege (a chipped Core heals only between sieges)."); } } [Test] public void CoreRestore_Never_Exceeds_Max() { var (world, group) = MakeWorld("CoreCap", serverTick: 100); using (world) { var em = world.EntityManager; var core = MakeCore(em, current: 100, max: 100); MakeCycle(em, CyclePhase.Calm); for (uint t = 100; t < 100 + 18; t++) { SetServerTick(world, t); group.Update(); } Assert.AreEqual(100, em.GetComponentData(core).Current, "regen clamps at Max."); } } } }