Files
Project-M/Assets/_Project/Tests/EditMode/KillRewardSystemTests.cs
T
2026-07-13 18:30:41 -07:00

129 lines
5.4 KiB
C#

using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Tests
{
/// <summary>
/// Plain-Entities tests for <see cref="KillRewardSystem"/> (Phase 1.7 on-kill boons). Siphon heals the credited
/// killer (clamped to their effective max, once per corpse via the Dying.Rewarded latch); Frenzy upserts a single
/// cooldown-reduction row; an unresolved killer (KillerNetId &lt; 0) grants nothing but is still latched.
/// </summary>
public class KillRewardSystemTests
{
const uint T0 = 5000;
static (World world, SimulationSystemGroup group, EntityManager em) MakeWorld()
{
var world = new World("KillRewardTest");
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
group.AddSystemToUpdateList(world.GetOrCreateSystem<KillRewardSystem>());
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(T0) });
return (world, group, em);
}
static Entity MakeKiller(EntityManager em, int netId, byte flags, float hp, float maxHp)
{
var e = em.CreateEntity(typeof(PlayerTag), typeof(GhostOwner), typeof(BoonEffects),
typeof(Health), typeof(EffectiveCharacterStats));
em.AddBuffer<StatModifier>(e);
em.AddBuffer<TimedModifier>(e);
em.SetComponentData(e, new GhostOwner { NetworkId = netId });
em.SetComponentData(e, new BoonEffects { Flags = flags });
em.SetComponentData(e, new Health { Current = hp, Max = maxHp });
em.SetComponentData(e, new EffectiveCharacterStats { MaxHealth = maxHp });
return e;
}
static Entity MakeCorpse(EntityManager em, int killerNetId)
{
var e = em.CreateEntity(typeof(EnemyTag), typeof(Dying));
em.SetComponentData(e, new Dying { UntilTick = T0 + 50, KillerNetId = killerNetId, Rewarded = 0 });
return e;
}
static int FrenzyRows(EntityManager em, Entity player)
{
var mods = em.GetBuffer<StatModifier>(player);
int n = 0;
for (int i = 0; i < mods.Length; i++) if (mods[i].SourceId == Tuning.FrenzySourceId) n++;
return n;
}
[Test]
public void Siphon_HealsKiller_ClampedToMax_OncePerCorpse()
{
var (world, group, em) = MakeWorld();
using (world)
{
var killer = MakeKiller(em, 1, BoonFlag.Siphon, hp: 50f, maxHp: 130f);
var corpse = MakeCorpse(em, killerNetId: 1);
group.Update();
Assert.Greater(em.GetComponentData<Health>(killer).Current, 50f, "Siphon healed the killer");
Assert.AreEqual(1, em.GetComponentData<Dying>(corpse).Rewarded, "corpse latched as rewarded");
float afterFirst = em.GetComponentData<Health>(killer).Current;
group.Update(); // second tick: Rewarded==1 -> no double-heal
Assert.AreEqual(afterFirst, em.GetComponentData<Health>(killer).Current, 1e-4f, "no double-heal on a re-tick");
}
}
[Test]
public void Siphon_DoesNotOverheal_AboveEffectiveMax()
{
var (world, group, em) = MakeWorld();
using (world)
{
var killer = MakeKiller(em, 1, BoonFlag.Siphon, hp: 128f, maxHp: 130f);
MakeCorpse(em, killerNetId: 1);
group.Update();
Assert.AreEqual(130f, em.GetComponentData<Health>(killer).Current, 1e-4f, "heal clamps to the effective max");
}
}
[Test]
public void Frenzy_UpsertsSingleCooldownRow()
{
var (world, group, em) = MakeWorld();
using (world)
{
var killer = MakeKiller(em, 1, BoonFlag.Frenzy, hp: 100f, maxHp: 130f);
MakeCorpse(em, killerNetId: 1);
group.Update();
Assert.AreEqual(1, FrenzyRows(em, killer), "one Frenzy StatModifier row");
// A second corpse (new kill) re-stamps rather than stacking.
var c2 = em.CreateEntity(typeof(EnemyTag), typeof(Dying));
em.SetComponentData(c2, new Dying { UntilTick = T0 + 60, KillerNetId = 1, Rewarded = 0 });
group.Update();
Assert.AreEqual(1, FrenzyRows(em, killer), "Frenzy refreshes, never stacks");
}
}
[Test]
public void UnresolvedKiller_GrantsNothing_ButLatches()
{
var (world, group, em) = MakeWorld();
using (world)
{
var killer = MakeKiller(em, 1, BoonFlag.Siphon | BoonFlag.Frenzy, hp: 50f, maxHp: 130f);
var corpse = MakeCorpse(em, killerNetId: -1); // environment/AoE kill — no credit
group.Update();
Assert.AreEqual(50f, em.GetComponentData<Health>(killer).Current, 1e-4f, "no heal for an uncredited kill");
Assert.AreEqual(0, FrenzyRows(em, killer), "no Frenzy for an uncredited kill");
Assert.AreEqual(1, em.GetComponentData<Dying>(corpse).Rewarded, "still latched so it is not reprocessed");
}
}
}
}