]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
DamageableSystem cleanup & performance improvements (#20820)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sun, 8 Oct 2023 16:27:41 +0000 (03:27 +1100)
committerGitHub <noreply@github.com>
Sun, 8 Oct 2023 16:27:41 +0000 (03:27 +1100)
12 files changed:
Content.Server/Bible/BibleSystem.cs
Content.Server/Electrocution/ElectrocutionSystem.cs
Content.Server/Projectiles/ProjectileSystem.cs
Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs
Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Content.Shared/Damage/DamageModifierSet.cs
Content.Shared/Damage/DamageSpecifier.cs
Content.Shared/Damage/Systems/DamageableSystem.cs
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Content.Tests/Shared/DamageTest.cs

index 18f34ba1cc16be9dca51f3bb77a83edfb814f85c..e2cdc8c7440a7acedf95e312e753117de96f725a 100644 (file)
@@ -133,7 +133,7 @@ namespace Content.Server.Bible
 
             var damage = _damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid);
 
-            if (damage == null || damage.Total == 0)
+            if (damage == null || damage.Empty)
             {
                 var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)),("target", Identity.Entity(args.Target.Value, EntityManager)),("bible", uid));
                 _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);
index 844d526c58e10526ae3af563d36ac10d370bb2ba..48415c3953318b2f3486551bd18baca1d9ce377a 100644 (file)
@@ -177,7 +177,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
         if (!electrified.OnAttacked)
             return;
 
-        if (_meleeWeapon.GetDamage(args.Used, args.User).Total == 0)
+        if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
             return;
 
         TryDoElectrifiedAct(uid, args.User, 1, electrified);
@@ -192,7 +192,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
     private void OnLightAttacked(EntityUid uid, PoweredLightComponent component, AttackedEvent args)
     {
 
-        if (_meleeWeapon.GetDamage(args.Used, args.User).Total == 0)
+        if (!_meleeWeapon.GetDamage(args.Used, args.User).Any())
             return;
 
         if (args.Used != args.User)
index 60fc0c3b5cda558253558ae45bbaba6cba0a5087..c52e712e741b74aaf69831a8a9998842a7a96c39 100644 (file)
@@ -53,7 +53,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem
 
         if (modifiedDamage is not null && EntityManager.EntityExists(component.Shooter))
         {
-            if (modifiedDamage.Total > FixedPoint2.Zero && !deleted)
+            if (modifiedDamage.Any() && !deleted)
             {
                 _color.RaiseEffect(Color.Red, new List<EntityUid> { target }, Filter.Pvs(target, entityManager: EntityManager));
             }
index a0a3f6d5d7b625fec0b1ccf538958c77af2f43f7..94ddc09e732665121f7ba29ca08c9e1313167b65 100644 (file)
@@ -62,7 +62,7 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
 
         var damageSpec = GetDamage(uid, args.User, component);
 
-        if (damageSpec.Total == FixedPoint2.Zero)
+        if (damageSpec.Empty)
             return;
 
         _damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));
index ab2553e31bfe5ac97d756bffc557c166c902e743..f4deffd113371c4518794236a1fdc1795409d782 100644 (file)
@@ -85,7 +85,7 @@ public sealed partial class GunSystem
             {
                 var p = (ProjectileComponent) projectile.Component;
 
-                if (p.Damage.Total > FixedPoint2.Zero)
+                if (!p.Damage.Empty)
                 {
                     return p.Damage;
                 }
index 6a5dd2d02d86a7676f5e32db45d8c6e5da8339ab..e7bd3683d382f0917a743e13b164fe8b107761ed 100644 (file)
@@ -37,7 +37,7 @@ public sealed partial class GunSystem
         {
             var p = (ProjectileComponent) projectile.Component;
 
-            if (p.Damage.Total > FixedPoint2.Zero)
+            if (!p.Damage.Empty)
             {
                 return p.Damage;
             }
index 0859cb9442715e484d2ab4a18ca6ba74b1b18037..adda6a94e3c123c7fe5915c989f4d37331a56643 100644 (file)
@@ -239,7 +239,7 @@ public sealed partial class GunSystem : SharedGunSystem
                         {
                             if (!Deleted(hitEntity))
                             {
-                                if (dmg.Total > FixedPoint2.Zero)
+                                if (dmg.Any())
                                 {
                                     _color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
                                 }
index 2dfc38715e478db024146e753d3acbaed4fec2d7..bd074ec30f8a752527809728e6d601bdbdffbf0e 100644 (file)
@@ -5,11 +5,15 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy
 namespace Content.Shared.Damage
 {
     /// <summary>
-    ///     A set of coefficients or flat modifiers to damage types.. Can be applied to <see cref="DamageSpecifier"/> using <see
+    ///     A set of coefficients or flat modifiers to damage types. Can be applied to <see cref="DamageSpecifier"/> using <see
     ///     cref="DamageSpecifier.ApplyModifierSet(DamageSpecifier, DamageModifierSet)"/>. This can be done several times as the
     ///     <see cref="DamageSpecifier"/> is passed to it's final target. By default the receiving <see cref="DamageableComponent"/>, will
     ///     also apply it's own <see cref="DamageModifierSet"/>.
     /// </summary>
+    /// <remarks>
+    /// The modifier will only ever be applied to damage that is being dealt. Healing is unmodified.
+    /// The modifier can also never convert damage into healing.
+    /// </remarks>
     [DataDefinition]
     [Serializable, NetSerializable]
     [Virtual]
index 903ee605f1ad20885875004d0da455a42b9886be..b7181e297f6998ded9f565d244d86e7b8178795a 100644 (file)
@@ -37,16 +37,42 @@ namespace Content.Shared.Damage
         [IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)]
         public Dictionary<string, FixedPoint2> DamageDict { get; set; } = new();
 
+        [JsonIgnore]
+        [Obsolete("Use GetTotal()")]
+        public FixedPoint2 Total => GetTotal();
+
         /// <summary>
-        ///     Sum of the damage values.
+        ///     Returns a sum of the damage values.
         /// </summary>
         /// <remarks>
         ///     Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
-        ///     in another. For this purpose, you should instead use <see cref="TrimZeros()"/> and then check the <see
-        ///     cref="Empty"/> property.
+        ///     in another. Consider using <see cref="Any()"/> or <see cref="Empty"/> instead.
         /// </remarks>
-        [JsonIgnore]
-        public FixedPoint2 Total => DamageDict.Values.Sum();
+        public FixedPoint2 GetTotal()
+        {
+            var total = FixedPoint2.Zero;
+            foreach (var value in DamageDict.Values)
+            {
+                total += value;
+            }
+            return total;
+        }
+
+        /// <summary>
+        /// Returns true if the specifier contains any positive damage values.
+        /// Differs from <see cref="Empty"/> as a damage specifier might contain entries with zeroes.
+        /// This also returns false if the specifier only contains negative values.
+        /// </summary>
+        public bool Any()
+        {
+            foreach (var value in DamageDict.Values)
+            {
+                if (value > FixedPoint2.Zero)
+                    return true;
+            }
+
+            return false;
+        }
 
         /// <summary>
         ///     Whether this damage specifier has any entries.
@@ -100,41 +126,39 @@ namespace Content.Shared.Damage
         /// </summary>
         /// <remarks>
         ///     Only applies resistance to a damage type if it is dealing damage, not healing.
+        ///     This will never convert damage into healing.
         /// </remarks>
         public static DamageSpecifier ApplyModifierSet(DamageSpecifier damageSpec, DamageModifierSet modifierSet)
         {
             // Make a copy of the given data. Don't modify the one passed to this function. I did this before, and weapons became
             // duller as you hit walls. Neat, but not FixedPoint2ended. And confusing, when you realize your fists don't work no
             // more cause they're just bloody stumps.
-            DamageSpecifier newDamage = new(damageSpec);
+            DamageSpecifier newDamage = new();
+            newDamage.DamageDict.EnsureCapacity(damageSpec.DamageDict.Count);
 
-            foreach (var entry in newDamage.DamageDict)
+            foreach (var (key, value) in damageSpec.DamageDict)
             {
-                if (entry.Value <= 0) continue;
-
-                float newValue = entry.Value.Float();
+                if (value == 0)
+                    continue;
 
-                if (modifierSet.FlatReduction.TryGetValue(entry.Key, out var reduction))
+                if (value < 0)
                 {
-                    newValue -= reduction;
-                    if (newValue <= 0)
-                    {
-                        // flat reductions cannot heal you
-                        newDamage.DamageDict[entry.Key] = FixedPoint2.Zero;
-                        continue;
-                    }
+                    newDamage.DamageDict[key] = value;
+                    continue;
                 }
 
-                if (modifierSet.Coefficients.TryGetValue(entry.Key, out var coefficient))
-                {
-                    // negative coefficients **can** heal you.
-                    newValue = newValue * coefficient;
-                }
+                float newValue = value.Float();
+
+                if (modifierSet.FlatReduction.TryGetValue(key, out var reduction))
+                    newValue -= reduction;
+
+                if (modifierSet.Coefficients.TryGetValue(key, out var coefficient))
+                    newValue *= coefficient;
 
-                newDamage.DamageDict[entry.Key] = FixedPoint2.New(newValue);
+                if (newValue > 0)
+                    newDamage.DamageDict[key] = FixedPoint2.New(newValue);
             }
 
-            newDamage.TrimZeros();
             return newDamage;
         }
 
@@ -146,13 +170,19 @@ namespace Content.Shared.Damage
         /// <returns></returns>
         public static DamageSpecifier ApplyModifierSets(DamageSpecifier damageSpec, IEnumerable<DamageModifierSet> modifierSets)
         {
-            DamageSpecifier newDamage = new(damageSpec);
+            bool any = false;
+            DamageSpecifier newDamage = damageSpec;
             foreach (var set in modifierSets)
             {
-                // this is probably really inefficient. just don't call this in a hot path I guess.
+                // This creates a new damageSpec for each modifier when we really onlt need to create one.
+                // This is quite inefficient, but hopefully this shouldn't ever be called frequently.
                 newDamage = ApplyModifierSet(newDamage, set);
+                any = true;
             }
 
+            if (!any)
+                newDamage = new DamageSpecifier(damageSpec);
+
             return newDamage;
         }
 
@@ -224,9 +254,10 @@ namespace Content.Shared.Damage
         {
             foreach (var (type, value) in other.DamageDict)
             {
-                if (DamageDict.ContainsKey(type))
+                // CollectionsMarshal my beloved.
+                if (DamageDict.TryGetValue(type, out var existing))
                 {
-                    DamageDict[type] += value;
+                    DamageDict[type] = existing + value;
                 }
             }
         }
@@ -262,18 +293,22 @@ namespace Content.Shared.Damage
         ///     total of each group. If no members of a group are present in this <see cref="DamageSpecifier"/>, the
         ///     group is not included in the resulting dictionary.
         /// </remarks>
-        public Dictionary<string, FixedPoint2> GetDamagePerGroup(IPrototypeManager? protoManager = null)
+        public Dictionary<string, FixedPoint2> GetDamagePerGroup(IPrototypeManager protoManager)
+        {
+            var dict = new Dictionary<string, FixedPoint2>();
+            GetDamagePerGroup(protoManager, dict);
+            return dict;
+        }
+
+        /// <inheritdoc cref="GetDamagePerGroup(Robust.Shared.Prototypes.IPrototypeManager)"/>
+        public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary<string, FixedPoint2> dict)
         {
-            IoCManager.Resolve(ref protoManager);
-            var damageGroupDict = new Dictionary<string, FixedPoint2>();
+            dict.Clear();
             foreach (var group in protoManager.EnumeratePrototypes<DamageGroupPrototype>())
             {
                 if (TryGetDamageInGroup(group, out var value))
-                {
-                    damageGroupDict.Add(group.ID, value);
-                }
+                    dict.Add(group.ID, value);
             }
-            return damageGroupDict;
         }
 
         #region Operators
@@ -372,6 +407,8 @@ namespace Content.Shared.Damage
 
             return true;
         }
+
+        public FixedPoint2 this[string key] => DamageDict[key];
     }
     #endregion
 }
index a3cdd14ef7b8c0dae5d090da27c27800d058c650..8f6ccc20e6247ea46b963007662f053c7e344119 100644 (file)
@@ -20,6 +20,9 @@ namespace Content.Shared.Damage
         [Dependency] private readonly INetManager _netMan = default!;
         [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
 
+        private EntityQuery<AppearanceComponent> _appearanceQuery;
+        private EntityQuery<DamageableComponent> _damageableQuery;
+
         public override void Initialize()
         {
             SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
@@ -27,6 +30,9 @@ namespace Content.Shared.Damage
             SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
             SubscribeLocalEvent<DamageableComponent, OnIrradiatedEvent>(OnIrradiated);
             SubscribeLocalEvent<DamageableComponent, RejuvenateEvent>(OnRejuvenate);
+
+            _appearanceQuery = GetEntityQuery<AppearanceComponent>();
+            _damageableQuery = GetEntityQuery<DamageableComponent>();
         }
 
         /// <summary>
@@ -45,9 +51,9 @@ namespace Content.Shared.Damage
                     component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
                 }
 
-                foreach (var groupID in damageContainerPrototype.SupportedGroups)
+                foreach (var groupId in damageContainerPrototype.SupportedGroups)
                 {
-                    var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
+                    var group = _prototypeManager.Index<DamageGroupPrototype>(groupId);
                     foreach (var type in group.DamageTypes)
                     {
                         component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
@@ -63,8 +69,8 @@ namespace Content.Shared.Damage
                 }
             }
 
-            component.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager);
-            component.TotalDamage = component.Damage.Total;
+            component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
+            component.TotalDamage = component.Damage.GetTotal();
         }
 
         /// <summary>
@@ -90,11 +96,11 @@ namespace Content.Shared.Damage
         public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null,
             bool interruptsDoAfters = true, EntityUid? origin = null)
         {
-            component.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager);
-            component.TotalDamage = component.Damage.Total;
-            Dirty(component);
+            component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
+            component.TotalDamage = component.Damage.GetTotal();
+            Dirty(uid, component);
 
-            if (EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance) && damageDelta != null)
+            if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null)
             {
                 var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList());
                 _appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance);
@@ -117,7 +123,7 @@ namespace Content.Shared.Damage
         public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
             bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null)
         {
-            if (!uid.HasValue || !Resolve(uid.Value, ref damageable, false))
+            if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false))
             {
                 // TODO BODY SYSTEM pass damage onto body system
                 return null;
@@ -140,6 +146,8 @@ namespace Content.Shared.Damage
                 if (damageable.DamageModifierSetId != null &&
                     _prototypeManager.TryIndex<DamageModifierSetPrototype>(damageable.DamageModifierSetId, out var modifierSet))
                 {
+                    // TODO DAMAGE PERFORMANCE
+                    // use a local private field instead of creating a new dictionary here..
                     damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
                 }
 
@@ -153,20 +161,30 @@ namespace Content.Shared.Damage
                 }
             }
 
-            // Copy the current damage, for calculating the difference
-            DamageSpecifier oldDamage = new(damageable.Damage);
+            // TODO DAMAGE PERFORMANCE
+            // Consider using a local private field instead of creating a new dictionary here.
+            // Would need to check that nothing ever tries to cache the delta.
+            var delta = new DamageSpecifier();
+            delta.DamageDict.EnsureCapacity(damage.DamageDict.Count);
 
-            damageable.Damage.ExclusiveAdd(damage);
-            damageable.Damage.ClampMin(FixedPoint2.Zero);
+            var dict = damageable.Damage.DamageDict;
+            foreach (var (type, value) in damage.DamageDict)
+            {
+                // CollectionsMarshal my beloved.
+                if (!dict.TryGetValue(type, out var oldValue))
+                    continue;
 
-            var delta = damageable.Damage - oldDamage;
-            delta.TrimZeros();
+                var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
+                if (newValue == oldValue)
+                    continue;
 
-            if (!delta.Empty)
-            {
-                DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin);
+                dict[type] = newValue;
+                delta.DamageDict[type] = newValue - oldValue;
             }
 
+            if (delta.DamageDict.Count > 0)
+                DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin);
+
             return delta;
         }
 
@@ -196,12 +214,11 @@ namespace Content.Shared.Damage
 
         public void SetDamageModifierSetId(EntityUid uid, string damageModifierSetId, DamageableComponent? comp = null)
         {
-            if (!Resolve(uid, ref comp))
+            if (!_damageableQuery.Resolve(uid, ref comp))
                 return;
 
             comp.DamageModifierSetId = damageModifierSetId;
-
-            Dirty(comp);
+            Dirty(uid, comp);
         }
 
         private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
@@ -265,7 +282,7 @@ namespace Content.Shared.Damage
     ///     Raised before damage is done, so stuff can cancel it if necessary.
     /// </summary>
     [ByRefEvent]
-    public record struct BeforeDamageChangedEvent(DamageSpecifier Delta, EntityUid? Origin = null, bool Cancelled = false);
+    public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false);
 
     /// <summary>
     ///     Raised on an entity when damage is about to be dealt,
@@ -312,14 +329,14 @@ namespace Content.Shared.Damage
         /// <summary>
         ///     Was any of the damage change dealing damage, or was it all healing?
         /// </summary>
-        public readonly bool DamageIncreased = false;
+        public readonly bool DamageIncreased;
 
         /// <summary>
         ///     Does this event interrupt DoAfters?
         ///     Note: As provided in the constructor, this *does not* account for DamageIncreased.
         ///     As written into the event, this *does* account for DamageIncreased.
         /// </summary>
-        public readonly bool InterruptsDoAfters = false;
+        public readonly bool InterruptsDoAfters;
 
         /// <summary>
         ///     Contains the entity which caused the change in damage, if any was responsible.
index 9b61fd03b7d5eec44bab79e3c39c211a19fe5d84..ebe1a21e679ca3fe09ed82b1dc58c19d585a1226 100644 (file)
@@ -495,7 +495,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
         var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user);
 
-        if (damageResult != null && damageResult.Total > FixedPoint2.Zero)
+        if (damageResult != null && damageResult.Any())
         {
             // If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
             if (damageResult.DamageDict.TryGetValue("Blunt", out var bluntDamage))
@@ -522,7 +522,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
             {
                 Audio.PlayPredicted(hitEvent.HitSoundOverride, meleeUid, user);
             }
-            else if (GetDamage(meleeUid, user, component).Total.Equals(FixedPoint2.Zero) && component.HitSound != null)
+            else if (!GetDamage(meleeUid, user, component).Any() && component.HitSound != null)
             {
                 Audio.PlayPredicted(component.HitSound, meleeUid, user);
             }
index 4f8647b3e40bdaf3f78c116d573679b800329c46..4221ff25a25d879db4965f3431a1aeda37ad081a 100644 (file)
@@ -17,15 +17,15 @@ namespace Content.Tests.Shared
     public sealed class DamageTest : ContentUnitTest
     {
 
-        static private Dictionary<string, float> _resistanceCoefficientDict = new()
+        private static Dictionary<string, float> _resistanceCoefficientDict = new()
         {
             // "missing" blunt entry
-            { "Piercing", -2 },// Turn Piercing into Healing
+            { "Piercing", -2 }, // negative multipliers just cause the damage to be ignored.
             { "Slash", 3 },
             { "Radiation", 1.5f },
         };
 
-        static private Dictionary<string, float> _resistanceReductionDict = new()
+        private static Dictionary<string, float> _resistanceReductionDict = new()
         {
             { "Blunt", - 5 },
             // "missing" piercing entry
@@ -152,14 +152,14 @@ namespace Content.Tests.Shared
             // Apply once
             damageSpec = DamageSpecifier.ApplyModifierSet(damageSpec, modifierSet);
             Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(FixedPoint2.New(25)));
-            Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(FixedPoint2.New(-40))); // became healing
+            Assert.That(!damageSpec.DamageDict.ContainsKey("Piercing"));  // Cannot convert damage into healing.
             Assert.That(damageSpec.DamageDict["Slash"], Is.EqualTo(FixedPoint2.New(6)));
             Assert.That(damageSpec.DamageDict["Radiation"], Is.EqualTo(FixedPoint2.New(44.25)));
 
             // And again, checking for some other behavior
             damageSpec = DamageSpecifier.ApplyModifierSet(damageSpec, modifierSet);
             Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(FixedPoint2.New(30)));
-            Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(FixedPoint2.New(-40))); // resistances don't apply to healing
+            Assert.That(!damageSpec.DamageDict.ContainsKey("Piercing"));
             Assert.That(!damageSpec.DamageDict.ContainsKey("Slash"));  // Reduction reduced to 0, and removed from specifier
             Assert.That(damageSpec.DamageDict["Radiation"], Is.EqualTo(FixedPoint2.New(65.63)));
         }