using NUnit.Framework; using ProjectM.Simulation; using Unity.Collections; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Coverage for the shared tick-window predicates (extracted 07-15 from PlayerAnimationDriveSystem so the /// predicted facing path and the anim path share one implementation): swing/fire window boundaries /// (wrap-safe NetworkTick math, 0-sentinel guards) and the Movement-archetype skip (a blink is a dodge, /// never a cast — and never a phantom IsFiring at its cooldown tail). /// public class TickWindowMathTests { const uint N = 13; // window ticks (the anim pulse length; callers pass their own) // ---- SwingActive ---- [Test] public void SwingActive_Window_Is_HalfOpen_From_Start() { var mc = new MeleeCombo { SwingStartTick = 100u }; Assert.IsFalse(TickWindowMath.SwingActive(mc, new NetworkTick(99u), N), "before start"); Assert.IsTrue(TickWindowMath.SwingActive(mc, new NetworkTick(100u), N), "start tick included"); Assert.IsTrue(TickWindowMath.SwingActive(mc, new NetworkTick(112u), N), "last tick included"); Assert.IsFalse(TickWindowMath.SwingActive(mc, new NetworkTick(113u), N), "end excluded"); } [Test] public void SwingActive_Zero_Stamp_Or_Invalid_Tick_Is_False() { Assert.IsFalse(TickWindowMath.SwingActive(default, new NetworkTick(100u), N)); Assert.IsFalse(TickWindowMath.SwingActive(new MeleeCombo { SwingStartTick = 100u }, default, N)); } // ---- FireActive ---- [Test] public void FireActive_Reconstructs_The_Window_From_The_Cooldown_Stamp() { // NextFireTick 120, CooldownTicks 20 -> fired at 100 -> window [100, 113). Assert.IsFalse(TickWindowMath.FireActive(120u, 20, new NetworkTick(99u), N)); Assert.IsTrue(TickWindowMath.FireActive(120u, 20, new NetworkTick(100u), N)); Assert.IsTrue(TickWindowMath.FireActive(120u, 20, new NetworkTick(112u), N)); Assert.IsFalse(TickWindowMath.FireActive(120u, 20, new NetworkTick(113u), N)); } [Test] public void FireActive_Sentinels_Are_False() { Assert.IsFalse(TickWindowMath.FireActive(0u, 20, new NetworkTick(100u), N), "no stamp"); Assert.IsFalse(TickWindowMath.FireActive(120u, 0, new NetworkTick(100u), N), "no cooldown"); Assert.IsFalse(TickWindowMath.FireActive(120u, 20, default, N), "invalid now"); } [Test] public void FireActive_Window_Start_Wrapping_To_Zero_Coerces_Through_NonZero() { // The project's canonical tick-wrap hazard: a computed start of exactly 0 (the "invalid" sentinel) // must coerce through TickUtil.NonZero to 1, not read as no-window. NextFireTick 20, CooldownTicks 20 // -> raw start 0 -> window [1, 14). Assert.IsTrue(TickWindowMath.FireActive(20u, 20, new NetworkTick(5u), N)); Assert.IsTrue(TickWindowMath.FireActive(20u, 20, new NetworkTick(13u), N)); Assert.IsFalse(TickWindowMath.FireActive(20u, 20, new NetworkTick(14u), N), "end excluded"); } [Test] public void Missing_AbilityDb_Falls_Back_To_Include_All() { // Without a blob the archetype is unknown: the socket is INCLUDED (matches the old include-all // behavior) — even a Movement spark counts, because it cannot be identified as one. using var world = new World("TickWindowTestNoDb"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); em.AddBuffer(e); var sockets = em.GetBuffer(e); sockets.Add(new AbilitySocket { SparkId = 2 }); for (int i = 1; i < SocketId.Count; i++) sockets.Add(default); var effs = em.GetBuffer(e); effs.Add(new EffectiveSocketStats { CooldownTicks = 20 }); for (int i = 1; i < SocketId.Count; i++) effs.Add(default); var cd = default(SocketCooldown); cd.Set(0, 120u); TickWindowMath.SocketFireAndCone(cd, em.GetBuffer(e), em.GetBuffer(e), default, new NetworkTick(105u), N, out bool firing, out bool cone); Assert.IsTrue(firing); Assert.IsFalse(cone); } // ---- SocketFireAndCone (Movement skip) ---- static BlobAssetReference BuildDb() { using var builder = new BlobBuilder(Allocator.Temp); ref var root = ref builder.ConstructRoot(); var abilities = builder.Allocate(ref root.Abilities, 3); abilities[0] = new AbilityDefBlob { Id = 1, Archetype = (byte)AbilityArchetype.Projectile, CooldownTicks = 20, Name = "Proj" }; abilities[1] = new AbilityDefBlob { Id = 2, Archetype = (byte)AbilityArchetype.Movement, CooldownTicks = 20, Name = "Blink" }; abilities[2] = new AbilityDefBlob { Id = 3, Archetype = (byte)AbilityArchetype.Cone, CooldownTicks = 20, Name = "Cone" }; builder.Allocate(ref root.Characters, 0); return builder.CreateBlobAssetReference(Allocator.Persistent); } static void RunSocketCase(byte sparkId, out bool firing, out bool cone) { using var world = new World("TickWindowTest"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); em.AddBuffer(e); var sockets = em.GetBuffer(e); sockets.Add(new AbilitySocket { SparkId = sparkId }); for (int i = 1; i < SocketId.Count; i++) sockets.Add(default); var effs = em.GetBuffer(e); effs.Add(new EffectiveSocketStats { CooldownTicks = 20 }); for (int i = 1; i < SocketId.Count; i++) effs.Add(default); var db = BuildDb(); try { var cd = default(SocketCooldown); cd.Set(0, 120u); // fired at 100 (CooldownTicks 20) -> window [100, 113) TickWindowMath.SocketFireAndCone(cd, em.GetBuffer(e), em.GetBuffer(e), db, new NetworkTick(105u), N, out firing, out cone); } finally { db.Dispose(); } } [Test] public void Movement_Socket_Never_Counts_As_Firing() { RunSocketCase(2, out bool firing, out bool cone); Assert.IsFalse(firing, "a blink (Movement archetype) is a dodge, never a cast/IsFiring pulse"); Assert.IsFalse(cone); } [Test] public void Projectile_Socket_Mid_Window_Fires_Without_Cone() { RunSocketCase(1, out bool firing, out bool cone); Assert.IsTrue(firing); Assert.IsFalse(cone); } [Test] public void Cone_Socket_Mid_Window_Fires_With_Cone() { RunSocketCase(3, out bool firing, out bool cone); Assert.IsTrue(firing); Assert.IsTrue(cone); } // ---- 07-21 G6 (review wf_98bf1268): FireStartRaw — the ONE home of the window-start reconstruction ---- [Test] public void FireStartRaw_Reconstructs_The_Fire_Tick() { Assert.AreEqual(978u, TickWindowMath.FireStartRaw(1000u, 22)); } [Test] public void FireStartRaw_Degenerate_Inputs_Return_Zero() { Assert.AreEqual(0u, TickWindowMath.FireStartRaw(0u, 22), "unstamped cooldown row"); Assert.AreEqual(0u, TickWindowMath.FireStartRaw(1000u, 0), "no cooldown length"); Assert.AreEqual(0u, TickWindowMath.FireStartRaw(1000u, -5), "negative cooldown length"); } [Test] public void FireStartRaw_Wrap_To_Zero_Coerces_Through_NonZero() { Assert.AreEqual(TickUtil.NonZero(0u), TickWindowMath.FireStartRaw(10u, 10), "the 0-sentinel is never returned for a REAL window start"); } } }