2aebc37115
Cone/SpecialSlam damage lands at its visual contact via ConeContactPending (knob 32, 0=legacy; early-flush + resolve re-validate; death + BOTH class-swap paths drop armed pendings — fixes the shipped melee death-strand in the same stroke). Zones: [GhostField] Caster/Radius/NextTick + ZoneTelegraphSystem (Geyser latch contract on InterpolationTick; rim = true radius; arm grows, persistent phase drains). Cone cues latch to contact (FireStartRaw, C14); TuningConfig.Defaults() fallback at client cue sites (release-build timing fix). G4: SaturationMath ally-FX degrade (living-enemy census, solo-exempt; enemy telegraphs structurally exempt) + CombatStressDebug + overlay saturation rows. Reviews wf_98bf1268 (13 confirmed folded) / wf_9757d214 (5 confirmed fixed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
178 lines
8.2 KiB
C#
178 lines
8.2 KiB
C#
using NUnit.Framework;
|
|
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Tests
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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<AbilitySocket>(e);
|
|
em.AddBuffer<EffectiveSocketStats>(e);
|
|
var sockets = em.GetBuffer<AbilitySocket>(e);
|
|
sockets.Add(new AbilitySocket { SparkId = 2 });
|
|
for (int i = 1; i < SocketId.Count; i++) sockets.Add(default);
|
|
var effs = em.GetBuffer<EffectiveSocketStats>(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<AbilitySocket>(e), em.GetBuffer<EffectiveSocketStats>(e),
|
|
default, new NetworkTick(105u), N, out bool firing, out bool cone);
|
|
Assert.IsTrue(firing);
|
|
Assert.IsFalse(cone);
|
|
}
|
|
|
|
|
|
// ---- SocketFireAndCone (Movement skip) ----
|
|
|
|
static BlobAssetReference<AbilityDatabaseBlob> BuildDb()
|
|
{
|
|
using var builder = new BlobBuilder(Allocator.Temp);
|
|
ref var root = ref builder.ConstructRoot<AbilityDatabaseBlob>();
|
|
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<AbilityDatabaseBlob>(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<AbilitySocket>(e);
|
|
em.AddBuffer<EffectiveSocketStats>(e);
|
|
var sockets = em.GetBuffer<AbilitySocket>(e);
|
|
sockets.Add(new AbilitySocket { SparkId = sparkId });
|
|
for (int i = 1; i < SocketId.Count; i++) sockets.Add(default);
|
|
var effs = em.GetBuffer<EffectiveSocketStats>(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<AbilitySocket>(e), em.GetBuffer<EffectiveSocketStats>(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");
|
|
}
|
|
}
|
|
} |