2
\$\begingroup\$

For the game that I am creating I came across the need for an Elemental-based Resistance system. By that I mean, a system which would allow the player to mitigate damage or take more damage based upon their Resistance rating towards an element of a given type. With a good Fire Resistance they would take less fire damage, and with a bad Fire Resistance (an Aversion as I call it) they would take more damage.

For my system I decided to go with integers instead of floats, which would have been clamped to a 0-2.0f range. I decided against this simply because it is easier and faster to work with integers than with floats (both IO serialization and actual usage). This was strictly my opinion and overall design choice, so I am not looking for that sort of correction; i'm aware that road could have been taken, I just chose not to do it.

The system itself works in a kind of inverted fashion. As an example, a Fire Resistance of 150 is actually bad. That means you have an Aversion to fire, and will take 50% more damage if hit by it. On the other end of the spectrum, a Fire Resistance of 50 is really good; you would take 50% less Fire damage!

I guess what I am really wanting here is for someone to look over my implementation and see if they can make any positive improvements.

This code can be easily plugged into any project and tested:

using System.Collections.Generic;
using System.Linq;

namespace GrimoireEngine.Framework.Game.AttributeSheets
{

    public enum ElementType
    {
        /// <summary>
        /// Represents a Typeless-based Element.
        /// </summary>
        Normal = 0,
        /// <summary>
        /// Represents a Fire-based Element.
        /// </summary>
        Fire = 1,
        /// <summary>
        /// Represents a Frost-based Element.
        /// </summary>
        Frost = 2,
        /// <summary>
        /// Represents an Energy-based Element.
        /// </summary>
        Energy = 3,
        /// <summary>
        /// Represents a Sound-based Element.
        /// </summary>
        Sonic = 4,
        /// <summary>
        /// Represents a Nature-based Element.
        /// </summary>
        Nature = 5,
        /// <summary>
        /// Represents an Arcane/Magic-based Element.
        /// </summary>
        Arcane = 6,
        /// <summary>
        /// Represents a Holy-based Element.
        /// </summary>
        Holy = 7,
        /// <summary>
        /// Represents a Shadow-based Element.
        /// </summary>
        Shadow = 8

    }

    public enum ElementalDamageType
    {
        Default,
        Resistance,
        Aversion,
        Critical,
        Immune
    }

    public class ResistanceTable
    {
        private Dictionary<ElementType, int> _resistanceTable;
        public string Name { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public ElementType[] Elements
        {
            get
            {
                return this._resistanceTable.Keys.ToArray();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public KeyValuePair<ElementType, int>[] Resistances
        {
            get
            {
                return this._resistanceTable.ToArray();
            }
        }

        public int this[ElementType type]
        {
            get
            {
                return GetResistanceVersus(type);
            }
            set
            {
                SetResistanceVersus(type, value);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public int Normal
        {
            get
            {
                return GetResistanceVersus(ElementType.Normal);
            }
            set
            {
                SetResistanceVersus(ElementType.Normal, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Fire
        {
            get
            {
                return GetResistanceVersus(ElementType.Fire);
            }
            set
            {
                SetResistanceVersus(ElementType.Fire, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Frost
        {
            get
            {
                return GetResistanceVersus(ElementType.Frost);
            }
            set
            {
                SetResistanceVersus(ElementType.Frost, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Energy
        {
            get
            {
                return GetResistanceVersus(ElementType.Energy);
            }
            set
            {
                SetResistanceVersus(ElementType.Energy, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Sonic
        {
            get
            {
                return GetResistanceVersus(ElementType.Sonic);
            }
            set
            {
                SetResistanceVersus(ElementType.Sonic, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Nature
        {
            get
            {
                return GetResistanceVersus(ElementType.Nature);
            }
            set
            {
                SetResistanceVersus(ElementType.Nature, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Arcane
        {
            get
            {
                return GetResistanceVersus(ElementType.Arcane);
            }
            set
            {
                SetResistanceVersus(ElementType.Arcane, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Holy
        {
            get
            {
                return GetResistanceVersus(ElementType.Holy);
            }
            set
            {
                SetResistanceVersus(ElementType.Holy, value);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public int Shadow
        {
            get
            {
                return GetResistanceVersus(ElementType.Shadow);
            }
            set
            {
                SetResistanceVersus(ElementType.Shadow, value);
            }
        }

        public const int DefaultResistance = 100;
        public const int MinimumResistance = 0;
        public const int MaximumResistance = 200;

        public ResistanceTable(
            int normal = DefaultResistance,
            int fire = DefaultResistance,
            int frost = DefaultResistance,
            int energy = DefaultResistance,
            int sonic = DefaultResistance,
            int nature = DefaultResistance,
            int arcane = DefaultResistance,
            int holy = DefaultResistance,
            int shadow = DefaultResistance)
        {
            Initialize(normal,fire,frost,energy,sonic,nature,arcane,holy,shadow);
        }

        /// <summary>
        /// Sets all Resistances to the given value(s).
        /// if this is the first pass during initialization,
        /// the internal Dictionary will also be created by this
        /// method.
        /// </summary>
        /// <param name="normal"></param>
        /// <param name="fire"></param>
        /// <param name="frost"></param>
        /// <param name="energy"></param>
        /// <param name="sonic"></param>
        /// <param name="nature"></param>
        /// <param name="arcane"></param>
        /// <param name="holy"></param>
        /// <param name="shadow"></param>
        public void Initialize(
            int normal = DefaultResistance,
            int fire = DefaultResistance,
            int frost = DefaultResistance,
            int energy = DefaultResistance,
            int sonic = DefaultResistance,
            int nature = DefaultResistance,
            int arcane = DefaultResistance,
            int holy = DefaultResistance,
            int shadow = DefaultResistance)
        {
            if (this._resistanceTable == null)
            {
                /**
                 * This will serve as the primary entry point for the class
                 * initialization. All contructors lead to this branch. We take
                 * this time to not only create the dictionary, but to also fill
                 * it with the default data, or the data presented by the user.
                 */
                this._resistanceTable = new Dictionary<ElementType, int>
                {
                    {ElementType.Normal, normal},
                    {ElementType.Fire, fire},
                    {ElementType.Frost, frost},
                    {ElementType.Energy, energy},
                    {ElementType.Sonic, sonic},
                    {ElementType.Nature, nature},
                    {ElementType.Arcane, arcane},
                    {ElementType.Holy, holy},
                    {ElementType.Shadow, shadow}
                };
            }
            else
            {
                /**
                 * This instance has already been initialized previously;
                 * the user simply wants to reset the values. This means we
                 * can avoid having to recreate the internal dictionary and
                 * just set the values.
                 */
                Normal = normal;
                Fire = fire;
                Frost = frost;
                Energy = energy;
                Sonic = sonic;
                Nature = nature;
                Arcane = arcane;
                Holy = holy;
                Shadow = shadow;
            }
        }

        /// <summary>
        /// Gets the type of <see cref="ElementalDamageType"/> that will result from the given <see cref="ElementType"/>
        /// and this instances Resistance towards it.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public ElementalDamageType GetResistanceDamageTypeVersus(ElementType type)
        {
            int mastery = GetResistanceVersus(type);
            // We intentionally check for min and max first.
            if (mastery == MaximumResistance)
            {
                return ElementalDamageType.Critical;
            }
            // We intentionally check for min and max first.
            if (mastery == MinimumResistance)
            {
                return ElementalDamageType.Immune;
            }
            if (mastery > DefaultResistance)
            {
                return ElementalDamageType.Aversion;
            }
            if (mastery < DefaultResistance)
            {
                return ElementalDamageType.Resistance;
            }
            return ElementalDamageType.Default;
        }

        /// <summary>
        /// Determines if the desired <see cref="ElementType"/> will be
        /// inflicted with decreased damage.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public bool HasResistanceVersus(ElementType type)
        {
            ElementalDamageType damageType = GetResistanceDamageTypeVersus(type);
            return damageType == ElementalDamageType.Resistance || damageType == ElementalDamageType.Immune;
        }

        /// <summary>
        /// Determines if the desired <see cref="ElementType"/> will result in increased damage.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public bool HasAversionVersus(ElementType type)
        {
            ElementalDamageType damageType = GetResistanceDamageTypeVersus(type);
            return damageType == ElementalDamageType.Aversion || damageType == ElementalDamageType.Critical;
        }

        /// <summary>
        /// Calculates the resulting damage given the damage, the
        /// desired <see cref="ElementType"/>, and this instances Resistance
        /// versus that element.
        /// </summary>
        /// <param name="damage"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public int ComputeResistanceVersus(int damage, ElementType type)
        {
            return (int)(damage * ((float)GetResistanceVersus(type) / DefaultResistance));
        }

        /// <summary>
        /// Increases the Resistance against the desired <see cref="ElementType"/>. This
        /// method works in an inverted fashion. So increasing is actually
        /// decreasing the internal variable.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        public void IncreaseResistanceVersus(ElementType type, int value)
        {
            // This is inverted
            SetResistanceVersus(type, GetResistanceVersus(type) - value);
        }

        /// <summary>
        /// Decreases the Resistance against the desired <see cref="ElementType"/>. This
        /// method works in an inverted fashion. So decreasing is actually
        /// increasing the internal variable.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        public void DecreaseResistanceVersus(ElementType type, int value)
        {
            // This is inverted
            SetResistanceVersus(type, GetResistanceVersus(type) + value);
        }

        /// <summary>
        /// Sets the desired Resistance of the given <see cref="ElementType"/> to
        /// its default value.
        /// </summary>
        /// <param name="type"></param>
        public void SetResistanceToDefault(ElementType type)
        {
            SetResistanceVersus(type, DefaultResistance);
        }

        /// <summary>
        /// Sets the desired Resistance of the given <see cref="ElementType"/> to the
        /// given value.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="mastery"></param>
        public void SetResistanceVersus(ElementType type, int mastery)
        {
            this._resistanceTable[type] = Clamp(mastery);
        }

        /// <summary>
        /// used internally to clamp the resistances to a 0-200 range.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private static int Clamp(int value)
        {
            return (value < MinimumResistance) ? MinimumResistance : (value > MaximumResistance) ? MaximumResistance : value;
        }

        /// <summary>
        /// Retrieves a collection of <see cref="ElementType"/> that will result in
        /// decreased damage when attacked by them.
        /// </summary>
        /// <returns></returns>
        public KeyValuePair<ElementType, int>[] GetResistances()
        {
            List<KeyValuePair<ElementType, int>> resistances = new List<KeyValuePair<ElementType, int>>(9);
            foreach (KeyValuePair<ElementType, int> resistancePair in _resistanceTable)
            {
                if (HasResistanceVersus(resistancePair.Key))
                {
                    resistances.Add(resistancePair);
                }
            }
            return resistances.ToArray();
        }

        /// <summary>
        /// Retrieves a collection of <see cref="ElementType"/> that will result in
        /// increased damage when attacked by them.
        /// </summary>
        /// <returns></returns>
        public KeyValuePair<ElementType, int>[] GetAversions()
        {
            List<KeyValuePair<ElementType, int>> aversions = new List<KeyValuePair<ElementType, int>>(9);
            foreach (KeyValuePair<ElementType, int> aversionPair in _resistanceTable)
            {
                if (HasAversionVersus(aversionPair.Key))
                {
                    aversions.Add(aversionPair);
                }
            }
            return aversions.ToArray();
        }

        /// <summary>
        /// Retrieves the current Resistance value of the specified
        /// <see cref="ElementType"/>.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public int GetResistanceVersus(ElementType type)
        {
            return _resistanceTable[type];
        }

        public override string ToString()
        {
            return $"Normal={Normal}," +
                   $"Fire={Fire}," +
                   $"Frost={Frost}," +
                   $"Energy={Energy}," +
                   $"Sonic={Sonic}," +
                   $"Nature={Nature}," +
                   $"Arcane={Arcane}," +
                   $"Holy={Holy}," +
                   $"Shadow={Shadow}";
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ Your commenting style is a little odd. Can you tell us more about that? \$\endgroup\$ – Mast Dec 31 '16 at 23:45
  • \$\begingroup\$ @Mast Define "odd" If you're talking about the blank comments it's placeholders for actual comments. \$\endgroup\$ – Krythic Dec 31 '16 at 23:51
  • 2
    \$\begingroup\$ If Fire Resistance mean Fire Aversion then why not call it that. And don't tell us what input is not welcome. \$\endgroup\$ – paparazzo Jan 1 '17 at 1:14
  • 1
    \$\begingroup\$ Why do you have "placeholders"? More importantly, why did you submit code for review that contains placeholders? \$\endgroup\$ – Cody Gray Jan 1 '17 at 12:35
  • 1
    \$\begingroup\$ A "have you stopped beating your wife yet?" question. Very nice. The purpose of this site is to review code—all of the code. That includes comments. We can't review things that we cannot see, and empty comments are completely worthless. They just waste space and provide absolutely no information. I'm trying to figure out if you're legitimately confused, or if you are just trying to bait me into continuing an argument. There is nothing "unprofessional" about wondering why you have blank comments in your code. \$\endgroup\$ – Cody Gray Jan 2 '17 at 16:52
4
\$\begingroup\$

This

public ResistanceTable(
    int normal = DefaultResistance,
    int fire = DefaultResistance,
    int frost = DefaultResistance,
    int energy = DefaultResistance,
    int sonic = DefaultResistance,
    int nature = DefaultResistance,
    int arcane = DefaultResistance,
    int holy = DefaultResistance,
    int shadow = DefaultResistance)
{
    Initialize(normal,fire,frost,energy,sonic,nature,arcane,holy,shadow);
}

and this

public void Initialize(
    int normal = DefaultResistance,
    int fire = DefaultResistance,
    int frost = DefaultResistance,
    int energy = DefaultResistance,
    int sonic = DefaultResistance,
    int nature = DefaultResistance,
    int arcane = DefaultResistance,
    int holy = DefaultResistance,
    int shadow = DefaultResistance)
{

and this

this._resistanceTable = new Dictionary<ElementType, int>
{
    {ElementType.Normal, normal},
    {ElementType.Fire, fire},
    {ElementType.Frost, frost},
    {ElementType.Energy, energy},
    {ElementType.Sonic, sonic},
    {ElementType.Nature, nature},
    {ElementType.Arcane, arcane},
    {ElementType.Holy, holy},
    {ElementType.Shadow, shadow}
};

and this

Normal = normal;
Fire = fire;
Frost = frost;
Energy = energy;
Sonic = sonic;
Nature = nature;
Arcane = arcane;
Holy = holy;
Shadow = shadow;

and this

public override string ToString()
{
    return $"Normal={Normal}," +
            $"Fire={Fire}," +
            $"Frost={Frost}," +
            $"Energy={Energy}," +
            $"Sonic={Sonic}," +
            $"Nature={Nature}," +
            $"Arcane={Arcane}," +
            $"Holy={Holy}," +
            $"Shadow={Shadow}";
}

and a property for each value.

Hard words by this is insane. Why don't you use the dictionary throughout the application? The code would be a fraction of its current size.

Imagine adding the one item to the ElementType. You need to adjust virtually every part of your code.

Consider using a dictionary for the construcotor too and if you need different configurations with different default values then a ResitanceTableFactory should help you predefine them and maintain in only one place. Example:

class ResitanceTableFactory
{
    public const int EasyResistance = 50;
    public const int NormalResistance = 100;
    public const int HardResistance = 150;

    public Dictionary<ElementType, int> CreateDefaultResistance(Level level)
    {
        switch(level)
        {
            case: Level.Normal:
                return CreateNormalResistance();
            ..
        }
    }

    private Dictionary<ElementType, int> CreateEasyResistance()
    {
        // or set each resitance manually, it is ok to to this here but only here
        return
            Enum.GetValues(typeof(ElementType))
            .Cast<ElementType>()
            .ToDictionary(x => x, x => EasyResistance);
    }

    private Dictionary<ElementType, int> CreateNormalResistance()
    {
        return
            Enum.GetValues(typeof(ElementType))
            .Cast<ElementType>()
            .ToDictionary(x => x, x => NormalResistance);
    }
}

You could pass this factory to the main ResitanceTable with dependency injection so that you can use it anytime to get a fresh dictionary for each level (if available).


if (this._resistanceTable == null)

Just create the dictionary again. It has only 10 items. This will probably execute in less then 1ns.

Instead of setting each value manually consider creating the dictionary dynamically:

_resitanceValues =
    Enum.GetValues(typeof(ElementType))
    .Cast<ElementType>()
    .ToDictionary(x => x, x=> DefaultResistance);

Done. Execute it agian if you want to reset the values. Again, it doesn't cost a penny.


public KeyValuePair<ElementType, int>[] GetAversions()
{
    List<KeyValuePair<ElementType, int>> aversions = new List<KeyValuePair<ElementType, int>>(9);
    foreach (KeyValuePair<ElementType, int> aversionPair in _resistanceTable)
    {
        if (HasAversionVersus(aversionPair.Key))
        {
            aversions.Add(aversionPair);
        }
    }
    return aversions.ToArray();
}

Mixing dictionaries with lists and arrays makes it only unnecessarily complex.

As far as the hard-coded (9) is considered you should at least use the _resistanceTable.Count if you need this optimization which I doubt.

It could be so much simpler with LINQ

return
    _resistanceTable
   .Where(x => HasAversionVersus(x.Key))
   // Pick one.
   .ToDictionary(x => x.Key, x => x.Value);
   .ToArray();
   .ToList();

public int this[ElementType type]
{
    get
    {
        return GetResistanceVersus(type);
    }
    set
    {
        SetResistanceVersus(type, value);
    }
}

You have an indexer already. Is it really necessary to expose each value via its own property anyway?

\$\endgroup\$
  • \$\begingroup\$ I only have one problem with your suggestions. "Again, it doesn't cost a penny." That's false, actually. You're looking at at least 4 heap allocations, especially with LINQ, and thats with some generous rounding down. The goal is to avoid the garbage collector, especially in a game loop. \$\endgroup\$ – Krythic Jan 1 '17 at 8:08
  • 2
    \$\begingroup\$ @Krythic honestly, how often are you going to reset the dictionary? 1k or 1mln loops per minute or maybe once or twice per game? I don't think it's a critical part. Run a profiler and see what is really eating memory before optimizing every single line. \$\endgroup\$ – t3chb0t Jan 1 '17 at 8:12
  • \$\begingroup\$ Technically...(tries to count it in his head). List+2 internal arrays+ array copy+to array. I count five heap allocations with the my previous method. I guarantee Linq generates upwards of 8, but I'm not going to sit here try to figure that out, let alone argue about it. I agree with just keeping the indexer, and deleting the hard coded fields, then implementing a better load mechanism for the enum types; this would greatly increase maintainability down the road. \$\endgroup\$ – Krythic Jan 1 '17 at 8:22
  • \$\begingroup\$ @Krythic no, I don't want arguing either ;-) I'm not saying optimizing doesn't make sense, what I wanted to say is just that the effort is worth only for parts that are called frequently. I don't know how the application is build so I can give only as much advice as I see code but I'm on your side that it should run fast and use less memory in cases where it really matters. I'd optimize it myself but probably only if I discovered a bottle-neck or knew from the beginning that something will be executed quite often. If this is the case then do the right thing and make it fast ;-] \$\endgroup\$ – t3chb0t Jan 1 '17 at 8:34
  • \$\begingroup\$ I have a new implementation that achieves this, and so much more, in a generic class. Can you please take a look at it when you find some time? codereview.stackexchange.com/questions/151514/… \$\endgroup\$ – Krythic Jan 3 '17 at 3:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.