using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Tests { /// /// EditMode coverage for the MC-0/MC-1 DevTelemetry counter WIRING (the fun-gate is measured, not argued): /// DashIFrameNegatedHits + DashState.NegatedCount (HealthApplyDamageSystem), DashesWasted (DashSystem /// window-close edge, server-gated on the DevTelemetry singleton), and ChargerWhiffPunishesLanded /// (player-sourced hit inside a Charger's StaggerUntilTick window, scored ONCE per window). /// public class TelemetryCountersTests { 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 (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick, bool withTelemetry) 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); if (withTelemetry) world.EntityManager.CreateEntity(typeof(DevTelemetry)); return (world, group); } static DevTelemetry Telemetry(EntityManager em) { using var q = em.CreateEntityQuery(typeof(DevTelemetry)); return q.GetSingleton(); } static Entity MakeDashingPlayer(EntityManager em, DashState ds) { var e = em.CreateEntity(); em.AddComponentData(e, ds); em.AddComponentData(e, new DashCooldown { NextTick = 0 }); em.AddComponentData(e, new CharacterControl { MoveVelocity = float3.zero }); em.AddComponentData(e, CharacterComponent.GetDefault()); em.AddComponentData(e, new PlayerInput()); em.AddComponentData(e, new PlayerFacing { Direction = new float2(0, 1) }); em.AddComponent(e); em.AddComponent(e); em.SetComponentEnabled(e, false); return e; } static Entity MakeStaggeredCharger(EntityManager em, uint staggerUntil) { var e = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(LungeState)); em.SetComponentData(e, new Health { Current = 200f, Max = 200f }); em.SetComponentData(e, new LungeState { StaggerUntilTick = staggerUntil }); return e; } [Test] public void Negation_Increments_DevTelemetry_And_DashState_NegatedCount() { var (world, group) = MakeWorld("TelemNegate", 113, withTelemetry: true); using (world) { var em = world.EntityManager; var e = em.CreateEntity(typeof(Health), typeof(DamageEvent), typeof(DashState)); em.SetComponentData(e, new Health { Current = 100f, Max = 100f }); em.SetComponentData(e, new DashState { StartTick = 100, IFrameUntilTick = 112, RecoverUntilTick = 121 }); var dmg = em.GetBuffer(e); dmg.Add(new DamageEvent { Amount = 5f, SourceNetworkId = -1, SourceTick = 101 }); // in-window -> negated dmg.Add(new DamageEvent { Amount = 6f, SourceNetworkId = -1, SourceTick = 111 }); // in-window -> negated dmg.Add(new DamageEvent { Amount = 7f, SourceNetworkId = -1, SourceTick = 113 }); // outside -> applies group.Update(); Assert.AreEqual(2u, Telemetry(em).DashIFrameNegatedHits, "Both in-window hits counted."); Assert.AreEqual(2u, em.GetComponentData(e).NegatedCount, "NegatedCount written back onto DashState (the wasted-dash signal source)."); Assert.AreEqual(93f, em.GetComponentData(e).Current, 1e-4f, "Only the outside hit applied."); } } [Test] public void Wasted_Dash_Counts_Once_On_Window_Close_With_Zero_Negations() { // Window fully elapsed at tick 120 (recover ended at 109) and nothing was negated. var (world, group) = MakeWorld("TelemWasted", 120, withTelemetry: true); using (world) { var em = world.EntityManager; var e = MakeDashingPlayer(em, new DashState { Dir = new float2(0, 1), StartTick = 80, IFrameUntilTick = 100, RecoverUntilTick = 109, NegatedCount = 0 }); group.Update(); Assert.AreEqual(1u, Telemetry(em).DashesWasted, "A dash whose window negated nothing scores wasted."); Assert.AreEqual(0u, em.GetComponentData(e).RecoverUntilTick, "Close edge clears the window (one-shot)."); group.Update(); Assert.AreEqual(1u, Telemetry(em).DashesWasted, "The close edge fires exactly once."); } } [Test] public void Effective_Dash_Is_Not_Wasted() { var (world, group) = MakeWorld("TelemEffective", 120, withTelemetry: true); using (world) { var em = world.EntityManager; MakeDashingPlayer(em, new DashState { Dir = new float2(0, 1), StartTick = 80, IFrameUntilTick = 100, RecoverUntilTick = 109, NegatedCount = 2 }); group.Update(); Assert.AreEqual(0u, Telemetry(em).DashesWasted, "A dash that negated hits is not wasted."); } } [Test] public void Without_Telemetry_Singleton_The_Close_Edge_Leaves_DashState_Intact() { // Client-world behavior: no DevTelemetry singleton -> no zeroing, so rollback re-simulation of the // tail window stays correct on the client (the server is the only world that clears). var (world, group) = MakeWorld("TelemClientNoZero", 120, withTelemetry: false); using (world) { var em = world.EntityManager; var e = MakeDashingPlayer(em, new DashState { Dir = new float2(0, 1), StartTick = 80, IFrameUntilTick = 100, RecoverUntilTick = 109 }); group.Update(); Assert.AreEqual(109u, em.GetComponentData(e).RecoverUntilTick, "No telemetry singleton (client world): the window is never zeroed by the close edge."); } } [Test] public void Player_Hit_On_Staggered_Charger_Scores_Punish_Once() { var (world, group) = MakeWorld("TelemPunish", 120, withTelemetry: true); using (world) { var em = world.EntityManager; var e = MakeStaggeredCharger(em, staggerUntil: 150); // stagger window active at 120 var dmg = em.GetBuffer(e); dmg.Add(new DamageEvent { Amount = 10f, SourceNetworkId = 1, SourceTick = 119 }); // player hit dmg.Add(new DamageEvent { Amount = 10f, SourceNetworkId = 1, SourceTick = 119 }); // same drain, same window group.Update(); Assert.AreEqual(1u, Telemetry(em).ChargerWhiffPunishesLanded, "A stagger window counts at most ONE punish (ratio to windows-opened stays <= 1)."); Assert.AreEqual(0u, em.GetComponentData(e).StaggerUntilTick, "Scoring zeroes StaggerUntilTick (the one-shot)."); Assert.AreEqual(180f, em.GetComponentData(e).Current, 1e-4f, "Both hits still apply damage."); } } [Test] public void NonPlayer_Hit_On_Staggered_Charger_Does_Not_Score() { var (world, group) = MakeWorld("TelemPunishTurret", 120, withTelemetry: true); using (world) { var em = world.EntityManager; var e = MakeStaggeredCharger(em, staggerUntil: 150); em.GetBuffer(e).Add(new DamageEvent { Amount = 10f, SourceNetworkId = -1, SourceTick = 119 }); group.Update(); Assert.AreEqual(0u, Telemetry(em).ChargerWhiffPunishesLanded, "Environment/turret damage (SourceNetworkId=-1) never scores a punish."); Assert.AreEqual(150u, em.GetComponentData(e).StaggerUntilTick, "The window stays scoreable for a real player hit."); } } [Test] public void Expired_Stagger_Window_Does_Not_Score() { var (world, group) = MakeWorld("TelemPunishLate", 200, withTelemetry: true); using (world) { var em = world.EntityManager; var e = MakeStaggeredCharger(em, staggerUntil: 150); // already over at 200 em.GetBuffer(e).Add(new DamageEvent { Amount = 10f, SourceNetworkId = 1, SourceTick = 199 }); group.Update(); Assert.AreEqual(0u, Telemetry(em).ChargerWhiffPunishesLanded, "A hit after the stagger window elapses is not a punish."); } } } }