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 (Geyser_Build_Spec; design-review /// wf_900e9965-8f0). Pins the review-hardened contract: an ELAPSED geyser radius-damages LIVING players AND /// enemies (friendly fire) with SourceNetworkId = -1, RESCHEDULES NextEruptTick = now + period (never +=, never /// the 0 sentinel), and is NEVER destroyed (opposite of the barrel). The load-bearing HIGH regression: a geyser /// that ships an UNSTAMPED NextEruptTick (0) must NEVER erupt — the inverted guard lazy-stamps it born-correct /// instead of the party-wiping per-tick barrage that copying the barrel's invalid-guard verbatim would cause. /// public class GeyserEruptSystemTests { const uint T0 = 1000; static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick = T0) { 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 nt = world.EntityManager.CreateEntity(typeof(NetworkTime)); world.EntityManager.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(serverTick) }); return (world, group); } static Entity MakeGeyser(EntityManager em, float3 pos, uint nextEruptTick) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new Geyser { NextEruptTick = nextEruptTick }); return e; } static Entity MakePlayer(EntityManager em, float3 pos, float health, byte region) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new PlayerTag()); em.AddComponentData(e, new Health { Current = health, Max = 130f }); em.AddComponentData(e, new RegionTag { Region = region }); em.AddBuffer(e); return e; } static Entity MakeEnemy(EntityManager em, float3 pos, float health) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new EnemyTag()); em.AddComponentData(e, new Health { Current = health, Max = 30f }); em.AddBuffer(e); return e; } [Test] public void Elapsed_Geyser_Damages_Living_Both_Sides_Reschedules_And_Survives() { var (world, group) = MakeWorld("EruptBoth"); using (world) { var em = world.EntityManager; var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0); // due exactly now var player = MakePlayer(em, new float3(11, 0, 10), health: 100f, region: RegionId.Expedition); var enemy = MakeEnemy(em, new float3(10, 0, 12), health: 30f); group.Update(); Assert.IsTrue(em.Exists(geyser), "A geyser is PERMANENT — it is never destroyed on erupt."); var pd = em.GetBuffer(player); Assert.AreEqual(1, pd.Length, "Friendly fire: the living expedition player in radius is hit."); Assert.AreEqual(Tuning.GeyserEruptDamage, pd[0].Amount); Assert.AreEqual(-1, pd[0].SourceNetworkId, "Environment convention: never a player id."); Assert.AreNotEqual(0u, pd[0].SourceTick, "SourceTick stamped at NOW (the authored-tick contract)."); Assert.AreEqual(1, em.GetBuffer(enemy).Length, "The living enemy in radius is hit too (bait mechanic)."); Assert.AreEqual(TickUtil.NonZero(T0 + Tuning.GeyserPeriodTicks), em.GetComponentData(geyser).NextEruptTick, "Reschedule = now + period (never += ; never the 0 sentinel)."); } } [Test] public void Unstamped_Geyser_Never_Erupts_And_Is_Lazy_Stamped() { // The HIGH regression: a baked-0 NextEruptTick at a high serverTick must NOT storm (the inverted guard). var (world, group) = MakeWorld("UnstampedNoStorm", serverTick: 5000); using (world) { var em = world.EntityManager; var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: 0u); // unstamped var player = MakePlayer(em, new float3(10, 0, 10), health: 130f, region: RegionId.Expedition); group.Update(); Assert.AreEqual(0, em.GetBuffer(player).Length, "An unstamped (0) geyser must NEVER erupt — no per-tick barrage."); Assert.AreEqual(TickUtil.NonZero(5000u + Tuning.GeyserPeriodTicks), em.GetComponentData(geyser).NextEruptTick, "0 is lazy-stamped born-correct (now + period) instead of erupting."); Assert.IsTrue(em.Exists(geyser)); } } [Test] public void Dead_Player_And_Out_Of_Radius_Get_Nothing() { var (world, group) = MakeWorld("EruptFilters"); using (world) { var em = world.EntityManager; MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0); var deadPlayer = MakePlayer(em, new float3(10.5f, 0, 10), health: 0f, region: RegionId.Expedition); var basePlayer = MakePlayer(em, new float3(10.5f, 0, 10), health: 100f, region: RegionId.Base); var farEnemy = MakeEnemy(em, new float3(10 + Tuning.GeyserEruptRadius + 1f, 0, 10), health: 30f); group.Update(); Assert.AreEqual(0, em.GetBuffer(deadPlayer).Length, "A dead player in radius gets NOTHING."); Assert.AreEqual(0, em.GetBuffer(basePlayer).Length, "A non-expedition player gets NOTHING."); Assert.AreEqual(0, em.GetBuffer(farEnemy).Length, "Out of radius gets nothing."); } } [Test] public void Unelapsed_Geyser_Does_Nothing_And_Keeps_Counting_Down() { var (world, group) = MakeWorld("EruptWait"); using (world) { var em = world.EntityManager; var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0 + 120); // still counting down var player = MakePlayer(em, new float3(10, 0, 10), health: 100f, region: RegionId.Expedition); group.Update(); Assert.AreEqual(0, em.GetBuffer(player).Length, "No damage before the eruption tick."); Assert.AreEqual(T0 + 120, em.GetComponentData(geyser).NextEruptTick, "An un-elapsed geyser is not rescheduled."); } } } }