Files
Project-M/Assets/_Project/Tests/EditMode/PlayerRespawnSystemTests.cs
T
2026-06-04 11:35:57 -07:00

108 lines
5.2 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 server-only <see cref="PlayerRespawnSystem"/> — the authoritative
/// death→respawn timer. A bare world is seeded with NetworkTime + PlayerSpawner singletons and a player
/// (Health, RespawnState, RespawnInvuln, LocalTransform, GhostOwner, EffectiveCharacterStats, PlayerTag).
/// Pins: a newly-dead player schedules a respawn tick; a due tick refills health + repositions + grants
/// invuln + clears the schedule; an alive player clears any stale pending schedule.
/// </summary>
public class PlayerRespawnSystemTests
{
static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<PlayerRespawnSystem>());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var em = world.EntityManager;
var nt = em.CreateEntity(typeof(NetworkTime));
em.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(serverTick) });
var sp = em.CreateEntity(typeof(PlayerSpawner));
em.SetComponentData(sp, new PlayerSpawner { SpawnPoint = new float3(0, 1, 0), SpawnRingRadius = 10f, RingSlots = 4 });
return (world, group);
}
static Entity MakePlayer(EntityManager em, float health, float maxHealth, uint respawnTick,
int delayTicks, int invulnTicks, float3 pos, int networkId)
{
var e = em.CreateEntity();
em.AddComponentData(e, new Health { Current = health, Max = maxHealth });
em.AddComponentData(e, new RespawnState { RespawnTick = respawnTick, DelayTicks = delayTicks, InvulnTicks = invulnTicks });
em.AddComponentData(e, new RespawnInvuln { UntilTick = 0 });
em.AddComponentData(e, LocalTransform.FromPosition(pos));
em.AddComponentData(e, new GhostOwner { NetworkId = networkId });
em.AddComponentData(e, new EffectiveCharacterStats { MaxHealth = maxHealth });
em.AddComponent<PlayerTag>(e);
return e;
}
[Test]
public void Newly_Dead_Player_Schedules_Respawn_Tick()
{
var (world, group) = MakeWorld("RespawnSchedule", serverTick: 200);
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, health: 0f, maxHealth: 100f, respawnTick: 0,
delayTicks: 60, invulnTicks: 120, pos: new float3(5, 1, 5), networkId: 1);
group.Update();
Assert.AreEqual(RespawnMath.RespawnTick(200, 60), em.GetComponentData<RespawnState>(player).RespawnTick,
"A newly-dead player schedules its respawn tick (now + delay).");
Assert.AreEqual(0f, em.GetComponentData<Health>(player).Current, 1e-4f, "Still down until the tick is due.");
}
}
[Test]
public void Due_Respawn_Restores_Health_Repositions_And_Grants_Invuln()
{
var (world, group) = MakeWorld("RespawnRecover", serverTick: 200);
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, health: 0f, maxHealth: 100f, respawnTick: 160,
delayTicks: 60, invulnTicks: 120, pos: new float3(999, 1, 999), networkId: 1);
group.Update();
Assert.AreEqual(100f, em.GetComponentData<Health>(player).Current, 1e-4f, "Health refills to the effective max.");
Assert.AreEqual(320u, em.GetComponentData<RespawnInvuln>(player).UntilTick,
"Post-respawn invulnerability is granted until now + InvulnTicks (200 + 120).");
Assert.AreEqual(0u, em.GetComponentData<RespawnState>(player).RespawnTick, "The respawn schedule is cleared on recovery.");
Assert.Less(em.GetComponentData<LocalTransform>(player).Position.x, 100f,
"The player is teleported from its death spot back to the base spawn ring.");
}
}
[Test]
public void Alive_Player_Clears_Stale_Pending_Respawn()
{
var (world, group) = MakeWorld("RespawnAliveClear", serverTick: 200);
using (world)
{
var em = world.EntityManager;
var player = MakePlayer(em, health: 50f, maxHealth: 100f, respawnTick: 999,
delayTicks: 60, invulnTicks: 120, pos: new float3(5, 1, 5), networkId: 1);
group.Update();
Assert.AreEqual(0u, em.GetComponentData<RespawnState>(player).RespawnTick,
"An alive player clears any stale pending respawn schedule.");
Assert.AreEqual(50f, em.GetComponentData<Health>(player).Current, 1e-4f, "Alive health is untouched.");
}
}
}
}