Further Tests & Progress
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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 enemy KNOCKBACK (server-only, no re-bake). Two halves:
|
||||
/// ProjectileDamageSystem STAMPS a <see cref="KnockbackState"/> on a hit Husk (Dir = the projectile heading,
|
||||
/// UntilTick = now + Tuning.KnockbackDurationTicks); EnemyAISystem APPLIES it — moving the Husk along the
|
||||
/// knockback heading (overriding seek) and suppressing its strike for the window, then resuming seek. Both
|
||||
/// systems are server-only Burst ISystems; a NetworkTime singleton is seeded (TurretFireSystems pattern).
|
||||
/// Knockback is gated by Tuning.KnockbackSpeed (0 disables) — so ProjectileDamageSystem only stamps when > 0.
|
||||
/// </summary>
|
||||
public class KnockbackTests
|
||||
{
|
||||
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) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ProjectileDamage_Stamps_Knockback_On_Hit_Husk()
|
||||
{
|
||||
var world = new World("KnockbackStampWorld");
|
||||
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
||||
group.AddSystemToUpdateList(world.GetOrCreateSystem<ProjectileDamageSystem>());
|
||||
group.SortSystems();
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
SetServerTick(world, 200);
|
||||
|
||||
var husk = em.CreateEntity();
|
||||
em.AddComponentData(husk, LocalTransform.FromPosition(new float3(0, 0, 5)));
|
||||
em.AddComponentData(husk, new HitRadius { Value = 0.8f });
|
||||
em.AddComponentData(husk, new Health { Current = 50f, Max = 50f });
|
||||
em.AddBuffer<DamageEvent>(husk);
|
||||
em.AddComponentData(husk, new KnockbackState()); // baked on real Husks
|
||||
|
||||
var proj = em.CreateEntity();
|
||||
em.AddComponentData(proj, LocalTransform.FromPosition(new float3(0, 0, 5)));
|
||||
em.AddComponentData(proj, new Projectile { Direction = new float2(0, 1), Speed = 10f, Damage = 20f, Range = 20f, DistanceTravelled = 5f });
|
||||
em.AddComponentData(proj, new GhostOwner { NetworkId = 1 });
|
||||
|
||||
world.SetTime(new TimeData(elapsedTime: 0.1f, deltaTime: 0.1f));
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(1, em.GetBuffer<DamageEvent>(husk).Length, "The hit still deals damage.");
|
||||
Assert.IsFalse(em.Exists(proj), "The projectile is consumed on hit.");
|
||||
var kb = em.GetComponentData<KnockbackState>(husk);
|
||||
Assert.AreEqual(TickUtil.NonZero(200 + (uint)Tuning.KnockbackDurationTicks), kb.UntilTick,
|
||||
"Knockback is scheduled until now + KnockbackDurationTicks.");
|
||||
Assert.AreEqual(0f, kb.Dir.x, 1e-4f);
|
||||
Assert.AreEqual(1f, kb.Dir.y, 1e-4f, "Knockback heading matches the projectile direction.");
|
||||
Assert.AreEqual(Tuning.KnockbackSpeed, kb.Speed, 1e-4f);
|
||||
}
|
||||
}
|
||||
|
||||
static (World world, SimulationSystemGroup group) MakeAiWorld(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 MakeHusk(EntityManager em, float3 pos, KnockbackState kb)
|
||||
{
|
||||
var e = em.CreateEntity();
|
||||
em.AddComponentData(e, LocalTransform.FromPosition(pos));
|
||||
em.AddComponentData(e, new EnemyStats { MoveSpeed = 3f, AttackRange = 1.6f, AttackDamage = 8f, AttackCooldownTicks = 36 });
|
||||
em.AddComponentData(e, new EnemyAttackCooldown { NextAttackTick = 0 });
|
||||
em.AddComponentData(e, kb);
|
||||
em.AddComponentData(e, new AttackWindup());
|
||||
em.AddComponent<EnemyTag>(e);
|
||||
return e;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Knockback_Overrides_Seek_Then_Resumes()
|
||||
{
|
||||
var (world, group) = MakeAiWorld("KnockbackAiWorld", 200);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
MakePlayer(em, new float3(10, 1, 0)); // player at +X
|
||||
var husk = MakeHusk(em, new float3(5, 1, 0),
|
||||
new KnockbackState { Dir = new float2(-1, 0), Speed = Tuning.KnockbackSpeed, UntilTick = TickUtil.NonZero(208) });
|
||||
|
||||
group.Update(); // tick 200: knocked -> moves -X (against the seek toward +X)
|
||||
float xKnocked = em.GetComponentData<LocalTransform>(husk).Position.x;
|
||||
Assert.Less(xKnocked, 5f, "While knocked the Husk moves along the knockback heading (-X), not toward the player (+X).");
|
||||
|
||||
SetServerTick(world, 208);
|
||||
group.Update(); // window elapsed -> seek resumes toward the player at +X
|
||||
float xResumed = em.GetComponentData<LocalTransform>(husk).Position.x;
|
||||
Assert.Greater(xResumed, xKnocked, "Once the knockback window elapses the Husk seeks back toward the player.");
|
||||
Assert.AreEqual(0u, em.GetComponentData<KnockbackState>(husk).UntilTick, "Knockback state is cleared after the window.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Knocked_Husk_Does_Not_Strike()
|
||||
{
|
||||
var (world, group) = MakeAiWorld("KnockbackNoStrikeWorld", 200);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
var player = MakePlayer(em, new float3(10, 1, 0));
|
||||
// Husk inside AttackRange of the player, but knocked.
|
||||
MakeHusk(em, new float3(9, 1, 0),
|
||||
new KnockbackState { Dir = new float2(-1, 0), Speed = Tuning.KnockbackSpeed, UntilTick = TickUtil.NonZero(208) });
|
||||
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(0, em.GetBuffer<DamageEvent>(player).Length,
|
||||
"A recoiling Husk does not strike even when inside AttackRange.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user