using NUnit.Framework; using ProjectM.Server; using ProjectM.Simulation; using Unity.Core; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Plain-Entities tests for (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 < 0) grants nothing but is still latched. /// public class KillRewardSystemTests { const uint T0 = 5000; static (World world, SimulationSystemGroup group, EntityManager em) MakeWorld() { var world = new World("KillRewardTest"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); 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(e); em.AddBuffer(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(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(killer).Current, 50f, "Siphon healed the killer"); Assert.AreEqual(1, em.GetComponentData(corpse).Rewarded, "corpse latched as rewarded"); float afterFirst = em.GetComponentData(killer).Current; group.Update(); // second tick: Rewarded==1 -> no double-heal Assert.AreEqual(afterFirst, em.GetComponentData(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(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(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(corpse).Rewarded, "still latched so it is not reprocessed"); } } } }