using NUnit.Framework; using ProjectM.Simulation; using Unity.Collections; using Unity.Entities; namespace ProjectM.Tests { /// /// Plain-Entities test for : builds an AbilityDatabase blob singleton /// + a player-like entity (refs / modifier buffer / effective components), ticks the /// SimulationSystemGroup, and asserts the effective stats equal the folded (base + modifiers) values /// and stay stable across repeated ticks (the every-tick recompute is idempotent). Version-independent. /// public class StatRecomputeSystemTests { const byte AbilityPrimary = (byte)AbilityId.Primary; const byte CharDefault = (byte)CharacterId.Default; static BlobAssetReference BuildDb() { using var b = new BlobBuilder(Allocator.Temp); ref var root = ref b.ConstructRoot(); var a = b.Allocate(ref root.Abilities, 1); a[0] = new AbilityDefBlob { Id = AbilityPrimary, Damage = 20f, ProjectileSpeed = 25f, Range = 20f, AutoTargetRange = 12f, AutoTargetConeRadians = 0.6f, CooldownTicks = 12, Name = "Primary" }; var c = b.Allocate(ref root.Characters, 1); c[0] = new CharacterStatsBlob { Id = CharDefault, MoveSpeed = 6f, TurnRateRadiansPerSec = 12.5f, MaxHealth = 100f, Name = "Default" }; return b.CreateBlobAssetReference(Allocator.Persistent); } static (World world, Entity player) MakeWorld(out BlobAssetReference blob) { var world = new World("StatRecomputeTestWorld"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); var em = world.EntityManager; blob = BuildDb(); var dbEntity = em.CreateEntity(typeof(AbilityDatabase)); em.SetComponentData(dbEntity, new AbilityDatabase { Value = blob }); var player = em.CreateEntity( typeof(AbilityRef), typeof(CharacterStatsRef), typeof(StatModifier), typeof(EffectiveAbilityStats), typeof(EffectiveCharacterStats), typeof(Simulate)); em.SetComponentData(player, new AbilityRef { Id = AbilityPrimary }); em.SetComponentData(player, new CharacterStatsRef { Id = CharDefault }); return (world, player); } static void AddMod(World world, Entity player, StatTarget target, ModOp op, float value) { var buf = world.EntityManager.GetBuffer(player); buf.Add(new StatModifier { Target = (byte)target, Op = (byte)op, Value = value }); } [Test] public void NoModifiers_Effective_Equals_Base() { var (world, player) = MakeWorld(out var blob); try { world.GetExistingSystemManaged().Update(); var ea = world.EntityManager.GetComponentData(player); var ec = world.EntityManager.GetComponentData(player); Assert.AreEqual(20f, ea.Damage, 1e-3f); Assert.AreEqual(25f, ea.ProjectileSpeed, 1e-3f); Assert.AreEqual(12, ea.CooldownTicks); Assert.AreEqual(6f, ec.MoveSpeed, 1e-3f); Assert.AreEqual(100f, ec.MaxHealth, 1e-3f); } finally { world.Dispose(); blob.Dispose(); } } [Test] public void Modifiers_Fold_Into_Effective() { var (world, player) = MakeWorld(out var blob); try { AddMod(world, player, StatTarget.Damage, ModOp.Flat, 5f); AddMod(world, player, StatTarget.Damage, ModOp.PercentAdd, 0.5f); // (20+5)*1.5 = 37.5 AddMod(world, player, StatTarget.MoveSpeed, ModOp.PercentAdd, 0.5f); // 6*1.5 = 9 world.GetExistingSystemManaged().Update(); var ea = world.EntityManager.GetComponentData(player); var ec = world.EntityManager.GetComponentData(player); Assert.AreEqual(37.5f, ea.Damage, 1e-3f); Assert.AreEqual(9f, ec.MoveSpeed, 1e-3f); } finally { world.Dispose(); blob.Dispose(); } } [Test] public void Recompute_Is_Idempotent_Across_Ticks() { var (world, player) = MakeWorld(out var blob); try { AddMod(world, player, StatTarget.Damage, ModOp.Flat, 10f); // 20+10 = 30 var group = world.GetExistingSystemManaged(); group.Update(); var first = world.EntityManager.GetComponentData(player).Damage; for (int i = 0; i < 5; i++) group.Update(); var last = world.EntityManager.GetComponentData(player).Damage; Assert.AreEqual(30f, first, 1e-3f); Assert.AreEqual(first, last, 1e-4f); } finally { world.Dispose(); blob.Dispose(); } } [Test] public void SocketFold_Folds_Per_Socket_With_Uniform_Band() { var world = new World("SocketFoldTestWorld"); var group = world.GetOrCreateSystemManaged(); group.AddSystemToUpdateList(world.GetOrCreateSystem()); group.SortSystems(); var em = world.EntityManager; var blob = BuildDb(); try { var dbEntity = em.CreateEntity(typeof(AbilityDatabase)); em.SetComponentData(dbEntity, new AbilityDatabase { Value = blob }); // player carrying the socket loadout + per-socket effective buffer (plus the legacy singles the // first fold loop still needs during the step-1b transition). var player = em.CreateEntity( typeof(AbilityRef), typeof(CharacterStatsRef), typeof(StatModifier), typeof(EffectiveAbilityStats), typeof(EffectiveCharacterStats), typeof(AbilitySocket), typeof(EffectiveSocketStats), typeof(Simulate)); em.SetComponentData(player, new AbilityRef { Id = AbilityPrimary }); em.SetComponentData(player, new CharacterStatsRef { Id = CharDefault }); var socketBuf = em.GetBuffer(player); var effBuf = em.GetBuffer(player); for (int i = 0; i < SocketId.Count; i++) { socketBuf.Add(new AbilitySocket { SparkId = 0 }); effBuf.Add(new EffectiveSocketStats()); } socketBuf[1] = new AbilitySocket { SparkId = AbilityPrimary }; // socket 1 holds the Spark; 0/2/3 empty // uniform band: +5 flat Damage applies to every socket's fold (Phase-1 band semantics). em.GetBuffer(player).Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.Flat, Value = 5f }); group.Update(); var result = em.GetBuffer(player); Assert.AreEqual(SocketId.Count, result.Length); Assert.AreEqual(25f, result[1].Damage, 1e-3f, "socket 1: Primary base 20 + band 5"); Assert.AreEqual(25f, result[1].ProjectileSpeed, 1e-3f); Assert.AreEqual(12, result[1].CooldownTicks); Assert.AreEqual(0f, result[0].Damage, 1e-3f, "empty socket 0 -> zeroed default"); Assert.AreEqual(0f, result[2].Damage, 1e-3f, "empty socket 2 -> zeroed default"); } finally { world.Dispose(); blob.Dispose(); } } } }