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 (Exploding_Barrels_Build_Spec). /// Pins the review-hardened contract: an ELAPSED fuse radius-damages LIVING enemies AND players /// (friendly fire) with SourceNetworkId = -1 (the environment convention — a player id would score /// Charger whiff-punishes) and destroys the barrel exactly once; a dead player in radius gets NOTHING /// (the BossAISystem slam filter, copied verbatim); out-of-radius targets get nothing; an UN-ELAPSED /// fuse does nothing. Also pins the pop-site half via ResourceHarvestSystem: an explosive (Variant 3) /// clutter pop lights the fuse (Remaining -> 0, BarrelFuse added, entity ALIVE) instead of destroying, /// and a lit barrel is unhittable. /// public class HazardExplosionSystemTests { const uint T0 = 1000; static (World world, SimulationSystemGroup group) MakeWorld(string name) { 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(T0) }); return (world, group); } static Entity MakeBarrel(EntityManager em, float3 pos, uint explodeTick) { var e = em.CreateEntity(); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new BlightClutter { Remaining = 0, Variant = 3, ScrapResourceId = ResourceId.Biomass, ScrapPerHit = 2f }); em.AddComponentData(e, new BarrelFuse { ExplodeTick = explodeTick }); 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_Fuse_Damages_Living_Player_And_Enemy_And_Destroys_Barrel() { var (world, group) = MakeWorld("BoomBoth"); using (world) { var em = world.EntityManager; var barrel = MakeBarrel(em, new float3(10, 1, 10), explodeTick: T0); // due exactly now var player = MakePlayer(em, new float3(11, 1, 10), health: 100f, region: RegionId.Expedition); var enemy = MakeEnemy(em, new float3(10, 1, 12), health: 30f); group.Update(); Assert.IsFalse(em.Exists(barrel), "An elapsed fuse destroys the barrel."); var pd = em.GetBuffer(player); Assert.AreEqual(1, pd.Length, "Friendly fire: the living expedition player in radius is hit."); Assert.AreEqual(Tuning.BarrelExplodeDamage, pd[0].Amount); Assert.AreEqual(-1, pd[0].SourceNetworkId, "Environment convention: never a player id (Charger punish scoring)."); Assert.AreNotEqual(0u, pd[0].SourceTick, "SourceTick stamped at detonation (the authored-tick contract)."); Assert.AreEqual(1, em.GetBuffer(enemy).Length, "The living enemy in radius is hit too (the bait mechanic)."); } } [Test] public void Dead_Player_And_Out_Of_Radius_Targets_Get_Nothing() { var (world, group) = MakeWorld("BoomFilters"); using (world) { var em = world.EntityManager; MakeBarrel(em, new float3(10, 1, 10), explodeTick: T0); var deadPlayer = MakePlayer(em, new float3(10.5f, 1, 10), health: 0f, region: RegionId.Expedition); var farEnemy = MakeEnemy(em, new float3(10 + Tuning.BarrelExplodeRadius + 1f, 1, 10), health: 30f); group.Update(); Assert.AreEqual(0, em.GetBuffer(deadPlayer).Length, "A dead player in radius gets NOTHING (the boss-slam filter)."); Assert.AreEqual(0, em.GetBuffer(farEnemy).Length, "Out of radius gets nothing."); } } [Test] public void Unelapsed_Fuse_Does_Nothing() { var (world, group) = MakeWorld("BoomWait"); using (world) { var em = world.EntityManager; var barrel = MakeBarrel(em, new float3(10, 1, 10), explodeTick: T0 + 30); // still burning var player = MakePlayer(em, new float3(10.5f, 1, 10), health: 100f, region: RegionId.Expedition); group.Update(); Assert.IsTrue(em.Exists(barrel), "A burning fuse leaves the barrel alive."); Assert.AreEqual(0, em.GetBuffer(player).Length, "No damage before detonation."); } } // ---- pop-site half (ResourceHarvestSystem drives the fuse, never a direct destroy) ---- static (World world, SimulationSystemGroup group) MakeHarvestWorld(string name) { 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 ledger = em.CreateEntity(typeof(ResourceLedger)); em.AddBuffer(ledger); var nt = em.CreateEntity(typeof(NetworkTime)); em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(T0) }); return (world, group); } [Test] public void Explosive_Pop_Lights_The_Fuse_Instead_Of_Destroying_And_Is_Unhittable() { var (world, group) = MakeHarvestWorld("PopFuse"); using (world) { var em = world.EntityManager; var barrel = em.CreateEntity(); em.AddComponentData(barrel, LocalTransform.FromPosition(new float3(10, 1, 10))); em.AddComponentData(barrel, new HitRadius { Value = 1f }); em.AddComponentData(barrel, new BlightClutter { Remaining = 2, Variant = 3, ScrapResourceId = ResourceId.Biomass, ScrapPerHit = 2f }); var proj = em.CreateEntity(); em.AddComponentData(proj, LocalTransform.FromPosition(new float3(10, 1, 10))); em.AddComponentData(proj, new Projectile { Direction = new float2(1, 0), LastStep = 5f }); group.Update(); Assert.IsTrue(em.Exists(barrel), "An explosive pop LIGHTS THE FUSE — the barrel survives the hit."); Assert.AreEqual(0, em.GetComponentData(barrel).Remaining, "Remaining=0 is the replicated fuse cue."); Assert.IsTrue(em.HasComponent(barrel), "The fuse component schedules detonation."); Assert.AreEqual(TickUtil.NonZero(T0 + Tuning.BarrelFuseTicks), em.GetComponentData(barrel).ExplodeTick); Assert.IsFalse(em.Exists(proj), "The popping projectile is consumed."); // A lit barrel is unhittable: a second projectile flies through (no target on the segment). var proj2 = em.CreateEntity(); em.AddComponentData(proj2, LocalTransform.FromPosition(new float3(10, 1, 10))); em.AddComponentData(proj2, new Projectile { Direction = new float2(0, 1), LastStep = 5f }); group.Update(); Assert.IsTrue(em.Exists(proj2), "A lit-fuse barrel is not a harvest target — the shot passes through."); Assert.IsTrue(em.Exists(barrel), "Still alive until detonation."); } } } }