using NUnit.Framework; using ProjectM.Simulation; using Unity.Collections; using Unity.Entities; namespace ProjectM.Tests { /// /// Slice 2 — the pure class mapping: which Fire ability each class gets, and that the DRG-asymmetric trait /// seeds fold the right direction (Warrior = melee bruiser / tankier / slower; Ranger = ranged / faster / /// squishier + a wider auto-assist co-op hook), all on the reserved range. /// public class ClassTraitsTests { static DynamicBuffer SeededBuffer(World world, byte classId) { var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); var ecb = new EntityCommandBuffer(Allocator.Temp); ClassTraits.AppendSeeds(classId, e, ecb); ecb.Playback(em); ecb.Dispose(); return em.GetBuffer(e); } [Test] public void AbilityFor_And_Normalize_DefaultToWarrior() { Assert.AreEqual((byte)AbilityId.WarriorCone, ClassTraits.AbilityFor(ClassTraits.WarriorClass)); Assert.AreEqual((byte)AbilityId.Primary, ClassTraits.AbilityFor(ClassTraits.RangerClass)); Assert.AreEqual((byte)AbilityId.WarriorCone, ClassTraits.AbilityFor(0), "unknown class -> Warrior cone"); Assert.AreEqual(ClassTraits.WarriorClass, ClassTraits.Normalize(0)); Assert.AreEqual(ClassTraits.WarriorClass, ClassTraits.Normalize(99)); Assert.AreEqual(ClassTraits.RangerClass, ClassTraits.Normalize(ClassTraits.RangerClass)); } [Test] public void Warrior_Seeds_Buff_Melee_And_Tankiness_And_Slow() { using var world = new World("ClassTraitsWarrior"); var mods = SeededBuffer(world, ClassTraits.WarriorClass); Assert.AreEqual(4, mods.Length, "Warrior seeds 4 trait modifiers."); Assert.Greater(StatMath.Apply(10f, StatTarget.MeleeDamage, mods), 10f, "Warrior hits harder in melee."); Assert.Greater(StatMath.Apply(2.6f, StatTarget.MeleeRange, mods), 2.6f, "Warrior reaches further in melee."); Assert.Less(StatMath.Apply(5f, StatTarget.MoveSpeed, mods), 5f, "Warrior moves slower."); Assert.Greater(StatMath.Apply(100f, StatTarget.MaxHealth, mods), 100f, "Warrior is tankier."); for (int i = 0; i < mods.Length; i++) Assert.GreaterOrEqual(mods[i].SourceId, Tuning.ClassSourceId, "class seeds use the reserved SourceId range."); } [Test] public void Ranger_Seeds_Buff_Range_Speed_Assist_And_Leave_Melee_Weak() { using var world = new World("ClassTraitsRanger"); var mods = SeededBuffer(world, ClassTraits.RangerClass); Assert.AreEqual(4, mods.Length, "Ranger seeds 4 trait modifiers."); Assert.Greater(StatMath.Apply(5f, StatTarget.MoveSpeed, mods), 5f, "Ranger moves faster."); Assert.Less(StatMath.Apply(100f, StatTarget.MaxHealth, mods), 100f, "Ranger is squishier."); Assert.Greater(StatMath.Apply(20f, StatTarget.Range, mods), 20f, "Ranger has longer projectile range."); Assert.Greater(StatMath.Apply(0f, StatTarget.AutoTargetRange, mods), 0f, "Ranger has a wider auto-assist (the co-op hook)."); Assert.AreEqual(10f, StatMath.Apply(10f, StatTarget.MeleeDamage, mods), 0.001f, "Ranger melee is unbuffed (weaker than the Warrior)."); } static int CountClassSeeds(DynamicBuffer mods) { int n = 0; for (int i = 0; i < mods.Length; i++) if (ClassTraits.IsClassSeed(mods[i].SourceId)) n++; return n; } static bool ContainsSource(DynamicBuffer mods, uint sourceId) { for (int i = 0; i < mods.Length; i++) if (mods[i].SourceId == sourceId) return true; return false; } [Test] public void AppendSeeds_EmitsExactlyClassSeedCount_AllInClassRange() { foreach (var classId in new[] { ClassTraits.WarriorClass, ClassTraits.RangerClass }) { using var world = new World("ClassTraitsCount"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); ClassTraits.AppendSeeds(classId, em.GetBuffer(e)); var seeded = em.GetBuffer(e); Assert.AreEqual(ClassTraits.ClassSeedCount, seeded.Length, "AppendSeeds emits exactly ClassSeedCount seeds."); for (int i = 0; i < seeded.Length; i++) Assert.IsTrue(ClassTraits.IsClassSeed(seeded[i].SourceId), "every emitted seed SourceId is inside the class range."); } } [Test] public void Reapply_RoundTrip_StaysAtClassSeedCount_AndFoldsLikeFreshSpawn() { using var world = new World("ClassTraitsReapply"); var em = world.EntityManager; var e = em.CreateEntity(); em.AddBuffer(e); // Seed Warrior, swap to Ranger, then back to Warrior. ClassTraits.Reapply(ClassTraits.WarriorClass, em.GetBuffer(e)); Assert.AreEqual(ClassTraits.ClassSeedCount, CountClassSeeds(em.GetBuffer(e)), "Warrior seeds the class-seed count."); ClassTraits.Reapply(ClassTraits.RangerClass, em.GetBuffer(e)); Assert.AreEqual(ClassTraits.ClassSeedCount, CountClassSeeds(em.GetBuffer(e)), "Ranger leaves exactly the class-seed count (old seeds stripped)."); Assert.Greater(StatMath.Apply(5f, StatTarget.MoveSpeed, em.GetBuffer(e)), 5f, "Ranger moves faster after the swap."); ClassTraits.Reapply(ClassTraits.WarriorClass, em.GetBuffer(e)); var back = em.GetBuffer(e); Assert.AreEqual(ClassTraits.ClassSeedCount, CountClassSeeds(back), "Warrior again: no accumulation across swaps."); // Value-equality vs a fresh-spawned Warrior buffer (the ECB overload) — catches same-target PercentMult // doubling or a strip-order regression that a count-only assert would miss. using var refWorld = new World("ClassTraitsWarriorRef"); var fresh = SeededBuffer(refWorld, ClassTraits.WarriorClass); Assert.AreEqual(StatMath.Apply(100f, StatTarget.MaxHealth, fresh), StatMath.Apply(100f, StatTarget.MaxHealth, back), 1e-4f, "MaxHealth folds identically to a fresh Warrior."); Assert.AreEqual(StatMath.Apply(5f, StatTarget.MoveSpeed, fresh), StatMath.Apply(5f, StatTarget.MoveSpeed, back), 1e-4f, "MoveSpeed folds identically to a fresh Warrior."); } [Test] public void Reapply_StripsOnlyClassSeeds_PreservesForeignAndBoundaryMods() { using var world = new World("ClassTraitsForeign"); var em = world.EntityManager; var e = em.CreateEntity(); var seed = em.AddBuffer(e); // A debug-upgrade mod + a boundary mod at ClassSourceId + ClassSeedCount (first id OUTSIDE the class range). uint boundary = Tuning.ClassSourceId + (uint)ClassTraits.ClassSeedCount; Assert.IsFalse(ClassTraits.IsClassSeed(boundary), "ClassSourceId + ClassSeedCount is just outside the class-seed range."); seed.Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.PercentAdd, Value = 0.25f, SourceId = 0x00DEB061u }); seed.Add(new StatModifier { Target = (byte)StatTarget.MaxHealth, Op = (byte)ModOp.Flat, Value = 10f, SourceId = boundary }); ClassTraits.Reapply(ClassTraits.RangerClass, em.GetBuffer(e)); var after = em.GetBuffer(e); Assert.AreEqual(ClassTraits.ClassSeedCount, CountClassSeeds(after), "exactly the class-seed count of class seeds present."); Assert.AreEqual(ClassTraits.ClassSeedCount + 2, after.Length, "both foreign mods survive Reapply."); Assert.IsTrue(ContainsSource(after, 0x00DEB061u), "the debug damage upgrade survives."); Assert.IsTrue(ContainsSource(after, boundary), "the boundary mod at ClassSourceId+ClassSeedCount survives."); } } }