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 { /// /// Plain-Entities EditMode tests for the server-only (hitscan defense turret). /// A bare world is seeded with a NetworkTime singleton, a turret (PlacedStructure + Turret + RegionTag /// + LocalTransform) and Husks (Health + EnemyTag + RegionTag + LocalTransform + a DamageEvent buffer). The /// system snapshots living Husks, fires at the nearest in-range one in its OWN region on the /// PlacedStructure.NextTick cooldown, and appends a DamageEvent{SourceNetworkId=-1}. These tests /// pin range filtering, region gating, and the wrap-safe cooldown. /// public class TurretFireSystemTests { 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)); 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 MakeTurret(EntityManager em, float3 pos, byte region, float range, int cooldown, float damage) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new RegionTag { Region = region }); em.AddComponentData(e, new PlacedStructure { Type = StructureType.Turret, NextTick = 0 }); em.AddComponentData(e, new Turret { Range = range, CooldownTicks = cooldown, Damage = damage }); return e; } static Entity MakeHusk(EntityManager em, float3 pos, byte region, float health) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new RegionTag { Region = region }); em.AddComponentData(e, new Health { Current = health, Max = health }); em.AddComponent(e); em.AddBuffer(e); return e; } [Test] public void Turret_Fires_At_InRange_Husk_In_Its_Region() { var (world, group) = MakeWorld("TurretFireWorld", serverTick: 200); using (world) { var em = world.EntityManager; var turret = MakeTurret(em, new float3(5, 1, 5), RegionId.Base, range: 20f, cooldown: 30, damage: 10f); var husk = MakeHusk(em, new float3(10, 1, 10), RegionId.Base, health: 50f); group.Update(); var dmg = em.GetBuffer(husk); Assert.AreEqual(1, dmg.Length, "An in-range, same-region Husk takes exactly one turret hit."); Assert.AreEqual(10f, dmg[0].Amount, 1e-4f, "DamageEvent carries the turret's damage."); Assert.AreEqual(-1, dmg[0].SourceNetworkId, "Turret damage uses the -1 (world) source id."); Assert.AreNotEqual(0u, em.GetComponentData(turret).NextTick, "The cooldown tick is stamped after firing."); } } [Test] public void Turret_Ignores_Husk_Out_Of_Range() { var (world, group) = MakeWorld("TurretRangeWorld", serverTick: 200); using (world) { var em = world.EntityManager; MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 5f, cooldown: 30, damage: 10f); var husk = MakeHusk(em, new float3(100, 1, 0), RegionId.Base, health: 50f); group.Update(); Assert.AreEqual(0, em.GetBuffer(husk).Length, "A Husk beyond Range takes no hit."); } } [Test] public void Turret_Ignores_Husk_In_A_Different_Region() { var (world, group) = MakeWorld("TurretRegionWorld", serverTick: 200); using (world) { var em = world.EntityManager; MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); var husk = MakeHusk(em, new float3(2, 1, 2), RegionId.Expedition, health: 50f); group.Update(); Assert.AreEqual(0, em.GetBuffer(husk).Length, "A Husk in a different region is never targeted (region gating)."); } } [Test] public void Turret_Respects_Cooldown_Then_Fires_Again() { var (world, group) = MakeWorld("TurretCooldownWorld", serverTick: 200); using (world) { var em = world.EntityManager; MakeTurret(em, new float3(0, 1, 0), RegionId.Base, range: 50f, cooldown: 30, damage: 10f); var husk = MakeHusk(em, new float3(3, 1, 0), RegionId.Base, health: 5000f); group.Update(); // fires at tick 200, NextTick -> 230 Assert.AreEqual(1, em.GetBuffer(husk).Length, "Fires on the first ready tick."); SetServerTick(world, 210); group.Update(); // 210 < 230 -> still cooling down Assert.AreEqual(1, em.GetBuffer(husk).Length, "No second shot while cooling down."); SetServerTick(world, 240); group.Update(); // 240 >= 230 -> ready again Assert.AreEqual(2, em.GetBuffer(husk).Length, "Fires again once the cooldown elapses."); } } } }