129 lines
6.4 KiB
C#
129 lines
6.4 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Plain-Entities EditMode tests for the MC-1 Charger branch in EnemyAISystem. A Husk variant baked with
|
|
/// LungeState commits to a fixed-direction lunge on wind-up elapse (UNLIKE the Grunt, it does NOT cancel when
|
|
/// the target leaves range — the commit is the punishable tell), deals contact damage if it connects, and
|
|
/// staggers (extends EnemyAttackCooldown + clears the lunge + opens a telemetry whiff window) if it overshoots
|
|
/// or wall-stops. Knockback cancels an in-flight lunge so EnemyAISystem stays the SOLE Position writer.
|
|
/// </summary>
|
|
public class ChargerTests
|
|
{
|
|
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)
|
|
{
|
|
var world = new World(name);
|
|
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
group.AddSystemToUpdateList(world.GetOrCreateSystem<EnemyAISystem>());
|
|
group.SortSystems();
|
|
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
|
SetServerTick(world, serverTick);
|
|
return (world, group);
|
|
}
|
|
|
|
static Entity MakePlayer(EntityManager em, float3 pos)
|
|
{
|
|
var e = em.CreateEntity();
|
|
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
|
em.AddComponentData(e, new Health { Current = 100f, Max = 100f });
|
|
em.AddComponent<PlayerTag>(e);
|
|
em.AddBuffer<DamageEvent>(e);
|
|
return e;
|
|
}
|
|
|
|
static Entity MakeCharger(EntityManager em, float3 pos)
|
|
{
|
|
var e = em.CreateEntity();
|
|
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
|
em.AddComponentData(e, new EnemyStats { MoveSpeed = 3f, AttackRange = 1.6f, AttackDamage = 12f, AttackCooldownTicks = 36 });
|
|
em.AddComponentData(e, new EnemyAttackCooldown { NextAttackTick = 0 });
|
|
em.AddComponentData(e, new KnockbackState());
|
|
em.AddComponentData(e, new AttackWindup());
|
|
em.AddComponentData(e, new LungeState());
|
|
em.AddComponent<EnemyTag>(e);
|
|
return e;
|
|
}
|
|
|
|
[Test]
|
|
public void Commit_Fires_Even_When_Target_Left_Range()
|
|
{
|
|
var (world, group) = MakeWorld("ChargerCommit", 200);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, new float3(10, 1, 0)); // far out of AttackRange (1.6)
|
|
var charger = MakeCharger(em, new float3(0, 1, 0));
|
|
em.SetComponentData(charger, new AttackWindup { WindUpUntilTick = 200 }); // elapses this tick
|
|
|
|
group.Update(); // tick 200
|
|
|
|
var lunge = em.GetComponentData<LungeState>(charger);
|
|
Assert.AreNotEqual(0u, lunge.UntilTick, "Charger commits the lunge even with the target out of range (no cancel-on-leave-range).");
|
|
Assert.Greater(lunge.Dir.x, 0.5f, "Lunge direction is locked toward the target at commit (+X).");
|
|
Assert.AreEqual(0u, em.GetComponentData<AttackWindup>(charger).WindUpUntilTick, "The wind-up clears on commit.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Overshoot_Whiff_Staggers_And_Opens_A_Punish_Window()
|
|
{
|
|
var (world, group) = MakeWorld("ChargerWhiff", 206);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, new float3(-10, 1, 0)); // player is behind; the lunge goes +X, never connects
|
|
var charger = MakeCharger(em, new float3(0, 1, 0));
|
|
em.SetComponentData(charger, new LungeState { Dir = new float2(1, 0), Speed = 16f, UntilTick = 205 }); // expiring lunge
|
|
em.CreateEntity(typeof(DevTelemetry)); // so the whiff telemetry increment is observable
|
|
|
|
group.Update(); // tick 206 > 205 -> lunge timer elapsed without landing -> overshoot whiff
|
|
|
|
Assert.AreEqual(0u, em.GetComponentData<LungeState>(charger).UntilTick, "A whiffed lunge is cleared.");
|
|
Assert.AreEqual(TickUtil.NonZero(206 + 36), em.GetComponentData<EnemyAttackCooldown>(charger).NextAttackTick,
|
|
"An overshoot whiff extends the attack cooldown by the stagger window (the punish window).");
|
|
using var tq = em.CreateEntityQuery(typeof(DevTelemetry));
|
|
Assert.AreEqual(1u, tq.GetSingleton<DevTelemetry>().ChargerWhiffWindowsOpened, "A whiff opens one telemetry punish window.");
|
|
Assert.AreEqual(TickUtil.NonZero(206 + 36), em.GetComponentData<LungeState>(charger).StaggerUntilTick,
|
|
"The whiff stamps the scoreable StaggerUntilTick window (ChargerWhiffPunishesLanded source).");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Knockback_Cancels_An_InFlight_Lunge()
|
|
{
|
|
var (world, group) = MakeWorld("ChargerKnockback", 305);
|
|
using (world)
|
|
{
|
|
var em = world.EntityManager;
|
|
MakePlayer(em, new float3(10, 1, 0));
|
|
var charger = MakeCharger(em, new float3(0, 1, 0));
|
|
em.SetComponentData(charger, new LungeState { Dir = new float2(1, 0), Speed = 16f, UntilTick = 320 }); // mid-lunge +X
|
|
em.SetComponentData(charger, new KnockbackState { Dir = new float2(-1, 0), Speed = 10f, UntilTick = 315 }); // recoil -X
|
|
|
|
group.Update(); // tick 305: knockback (until 315) wins
|
|
|
|
Assert.AreEqual(0u, em.GetComponentData<LungeState>(charger).UntilTick,
|
|
"Knockback cancels the in-flight lunge (no two-writer contention on Position).");
|
|
Assert.Less(em.GetComponentData<LocalTransform>(charger).Position.x, 0f,
|
|
"The recoiling Charger moved along its knockback direction (-X), not its lunge direction.");
|
|
}
|
|
}
|
|
}
|
|
}
|