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(); } } } }