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 tests for (Phase 1.7 Blade Dash). A dashing player with the /// boon damages a nearby enemy ONCE per dash (StartTick-keyed dedup survives a re-tick); a fresh dash hits again; /// no boon → no damage. /// public class DashTrailDamageSystemTests { static (World world, SimulationSystemGroup group, EntityManager em) MakeWorld(uint tick) { var world = new World("DashTrailTest"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f)); var em = world.EntityManager; em.SetComponentData(em.CreateEntity(typeof(NetworkTime)), new NetworkTime { ServerTick = new NetworkTick(tick) }); return (world, group, em); } static void SetTick(EntityManager em, uint tick) { using var q = em.CreateEntityQuery(typeof(NetworkTime)); em.SetComponentData(q.GetSingletonEntity(), new NetworkTime { ServerTick = new NetworkTick(tick) }); } static Entity MakeDasher(EntityManager em, byte flags, uint startTick, uint iframeUntil) { var e = em.CreateEntity(typeof(PlayerTag), typeof(GhostOwner), typeof(BoonEffects), typeof(DashTrailState)); em.AddComponentData(e, LocalTransform.FromPosition(new float3(0f, 0f, 0f))); em.AddComponentData(e, new DashState { Dir = new float2(1f, 0f), StartTick = startTick, IFrameUntilTick = iframeUntil, RecoverUntilTick = iframeUntil + 9 }); em.AddComponent(e); // enabled by default em.SetComponentData(e, new GhostOwner { NetworkId = 1 }); em.SetComponentData(e, new BoonEffects { Flags = flags }); return e; } static Entity MakeEnemy(EntityManager em, float3 pos) { var e = em.CreateEntity(typeof(EnemyTag)); em.AddComponentData(e, LocalTransform.FromPosition(pos)); em.AddComponentData(e, new HitRadius { Value = 0.5f }); em.AddComponentData(e, new Health { Current = 60f, Max = 60f }); em.AddBuffer(e); return e; } [Test] public void BladeDash_DamagesNearbyEnemy_OncePerDash_ReHitsOnNextDash() { var (world, group, em) = MakeWorld(100); using (world) { var player = MakeDasher(em, BoonFlag.DashTrail, startTick: 100, iframeUntil: 112); var enemy = MakeEnemy(em, new float3(1f, 0f, 0f)); // within 1.6 + 0.5 group.Update(); // tick 100, dashing Assert.AreEqual(1, em.GetBuffer(enemy).Length, "enemy in the dash path takes one hit"); group.Update(); // same tick + same StartTick -> dedup, no second hit Assert.AreEqual(1, em.GetBuffer(enemy).Length, "no re-hit within the same dash"); // A fresh dash (new StartTick) resets the dedup set -> the enemy can be hit again. SetTick(em, 130); em.SetComponentData(player, new DashState { Dir = new float2(1f, 0f), StartTick = 130, IFrameUntilTick = 142, RecoverUntilTick = 151 }); group.Update(); Assert.AreEqual(2, em.GetBuffer(enemy).Length, "a fresh dash hits the enemy again"); } } [Test] public void NoBoon_NoDamage() { var (world, group, em) = MakeWorld(100); using (world) { MakeDasher(em, flags: 0, startTick: 100, iframeUntil: 112); // no DashTrail flag var enemy = MakeEnemy(em, new float3(1f, 0f, 0f)); group.Update(); Assert.AreEqual(0, em.GetBuffer(enemy).Length, "no Blade Dash boon -> no trail damage"); } } } }