diff --git a/Assets/_Project/Abilities/Ability_WarriorCone.asset b/Assets/_Project/Abilities/Ability_WarriorCone.asset
new file mode 100644
index 000000000..1b4283772
--- /dev/null
+++ b/Assets/_Project/Abilities/Ability_WarriorCone.asset
@@ -0,0 +1,24 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84b20ca889a744e888c8c3b3b723ec69, type: 3}
+ m_Name: Ability_WarriorCone
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.AbilityDefinition
+ Id: 4
+ Archetype: 2
+ DisplayName: Warrior Cone
+ Damage: 22
+ ProjectileSpeed: 0
+ Range: 2.2
+ AutoTargetRange: 0
+ AutoTargetConeDegrees: 65
+ CooldownTicks: 22
+ ProjectilePrefab: {fileID: 0}
diff --git a/Assets/_Project/Abilities/Ability_WarriorCone.asset.meta b/Assets/_Project/Abilities/Ability_WarriorCone.asset.meta
new file mode 100644
index 000000000..028f85a0c
--- /dev/null
+++ b/Assets/_Project/Abilities/Ability_WarriorCone.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4cf5f3ead961b1242b0e3c79de96fa44
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity
index fa5e8d698..b474b9d11 100644
--- a/Assets/_Project/Subscenes/Gameplay.unity
+++ b/Assets/_Project/Subscenes/Gameplay.unity
@@ -1006,6 +1006,7 @@ MonoBehaviour:
- {fileID: 11400000, guid: 013954d16c1ff4c1dad82495e38b4657, type: 2}
- {fileID: 11400000, guid: b594c2953f8304189b91ca8c96a9d0c4, type: 2}
- {fileID: 11400000, guid: 601a090742e5341cc9ee1f25d215136b, type: 2}
+ - {fileID: 11400000, guid: 4cf5f3ead961b1242b0e3c79de96fa44, type: 2}
Characters:
- {fileID: 11400000, guid: e675b529048144a41a2054c729180bce, type: 2}
--- !u!4 &409538539
diff --git a/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs
new file mode 100644
index 000000000..f18471bec
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs
@@ -0,0 +1,65 @@
+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).");
+ }
+ }
+}
diff --git a/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs.meta b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs.meta
new file mode 100644
index 000000000..c6d8041f9
--- /dev/null
+++ b/Assets/_Project/Tests/EditMode/ClassTraitsTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c97bc5f5f2142144f85272e159e3c7b1
\ No newline at end of file