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 (CharacterStatsRef / socket loadout / 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). The legacy /// AbilityRef -> EffectiveAbilityStats fold is deleted (LANTERN purge); the socket fold is the ability path. /// public class StatRecomputeSystemTests { const byte AbilityPrimary = (byte)AbilityId.Primary; const byte CharDefault = (byte)FrameKind.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(CharacterStatsRef), typeof(StatModifier), typeof(EffectiveCharacterStats), typeof(Simulate)); 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 ec = world.EntityManager.GetComponentData(player); Assert.AreEqual(6f, ec.MoveSpeed, 1e-3f); Assert.AreEqual(12.5f, ec.TurnRateRadiansPerSec, 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.MoveSpeed, ModOp.PercentAdd, 0.5f); // 6*1.5 = 9 AddMod(world, player, StatTarget.MaxHealth, ModOp.Flat, 30f); // 100+30 = 130 world.GetExistingSystemManaged().Update(); var ec = world.EntityManager.GetComponentData(player); Assert.AreEqual(9f, ec.MoveSpeed, 1e-3f); Assert.AreEqual(130f, ec.MaxHealth, 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.MaxHealth, ModOp.Flat, 10f); // 100+10 = 110 var group = world.GetExistingSystemManaged(); group.Update(); var first = world.EntityManager.GetComponentData(player).MaxHealth; for (int i = 0; i < 5; i++) group.Update(); var last = world.EntityManager.GetComponentData(player).MaxHealth; Assert.AreEqual(110f, 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. var player = em.CreateEntity( typeof(CharacterStatsRef), typeof(StatModifier), typeof(EffectiveCharacterStats), typeof(AbilitySocket), typeof(EffectiveSocketStats), typeof(Simulate)); 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(); } } } }