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