032. Stats - Part 5 - Adding our stats to the character class

Now that we have all the support classes in place, we can create our next class, CharacterStats. This will be one single place to go to get everything stats related for our character, MaxHP, Armor, Level, Exp. This would also be the class you use when you level up to store points such as Ragnarok's Stat Points that are used to increase the base stats. If you choose to have your stats "static" like WoW, you never give the user any points to level their stats up with. the current implementation does not have stat points added even though it does have a way to set the stat values. Also in this post, I will show you how to build a default calculator that will be used to calculate MaxHP.

First will start with the basic class and its properties:

CharacterStats.cs


namespace AegisBorn.Models.Base.Actor.Stats
{
    public class CharacterStats
    {
        private readonly AegisBornCharacter _character;
        private long _exp;
        private int _level;

        public Calculator[] Calculators { get; set; }

        public AegisBornCharacter Character { get { return _character; } }

        private BaseStats BaseStats { get; set; }

        public long Exp
        {
            get { return _exp; }
            set { _exp = value; }
        }

        public int Level
        {
            get { return _level; }
            set { _level = value; }
        }
    }
}

You will notice that this class has an AegisBornCharacter which is the character that has these stats. We have Exp and Level to help us with determining when we level up. Later we will introduce a couple of functions that will allow us to add exp and make the level up process automatic. You will see that this class also has its own BaseStats and an array of calculators. Next we will take a look at the constructor. It does the majority of the work:

CharacterStats.CharacterStats()

        public CharacterStats(AegisBornCharacter aegisBornCharacter)
        {
            _character = aegisBornCharacter;
            BaseStats = new BaseStats();

            if (aegisBornCharacter is AegisBornPlayer)
            {
                // Slick way of quickly creating an array of auto-initialized Calculators, one for each Stat.
                Calculators = new Calculator[Enum.GetNames(typeof(Stats)).Length];
                for (int i = 0; i < Calculators.Length; i++)
                {
                    Calculators[i] = new Calculator();
                }

                Formulas.AddCalculatorsToNewCharacter(this);
            }
        }

Because our _character class is marked read only, it can't change, so the only place we can set it is in the constructor. Next we create an initial set of empty stats. Lastly, if we are dealing with a player character, we are going to initialize our calculator list and make a call to a class called Formulas and have it populate all of our default calculators. Now, if we populate our character class from the database, we don't want to always set the Exp to 0 and the level to 1, so we build a function that sets all our default data when a new character is created. At the same time we also want to be able to get stats out of our character stats.

CharacterStats

        public void NewCharacter()
        {
            Level = 1;
            Exp = 0;
        }

        public int GetValue(Stats stat)
        {
            return GetValue(stat, null);
        }

        public int GetValue(Stats stat, AegisBornCharacter aegisBornCharacter)
        {
            return (int) CalcStat(stat, BaseStats.GetValue(stat), aegisBornCharacter);
        }

        public int GetBaseValue(Stats stat)
        {
            return BaseStats.GetValue(stat);
        }

So now we need this function called CalcStat. Its job is to run through all the calculators and return to us our finished value.

CharacterStats.CalcStat()

        public double CalcStat(Stats stat, double initialValue, AegisBornCharacter target)
        {
            if(_character == null)
            {
                return initialValue;
            }
            Calculator c = Calculators[(int)stat];

            if(c.Size == 0)
            {
                return initialValue;
            }

            var calculatorValue = new CalculatorValue {Player = _character, Target = target, Value = initialValue};

            c.Calc(calculatorValue);

            // Ensure certain stats do not drop below 1 no matter what debuffs are applied
            // Find a better way to do this than to switch on a list of Stats.);
            if(calculatorValue.Value <= 0 && StatsUtil.NonNegativeStatList.Contains(stat))
            {
                calculatorValue.Value = 1;
            }

            return calculatorValue.Value;
        }

It looks for a character and if it was null somehow, it returns the initial value. Next it finds the calculator and if it is empty it returns the default value. Lastly if there are calculators, it creates a calculator value and calls the calculator's calc function passing in the value. After all the processing is completed, it checks to see if the value that comes out is less than 1. If it is AND it is in the list we created earlier of all stats that must ALWAYS be greater than 0, such as base stats, max hp, and max mp, it sets the value to 1. Then at the very end it returns that last calculated value. This does the bulk of the work for us when it comes to determining our final stats.

Next up we have a bit of trickery. In order to keep stats generic, i needed a generic way to store them in the database so that you wouldn't have to modify the DB every time you added a new base stat. We accomplish this through the use of XML Serialization. I created a small function to read in and spit out this xml so that we could quickly put it into the db as a string. I also created a function that the formula class will use to add functions to our calculators called AddStatFunction.

BaseStatsXML

        public String BaseStatsXML
        {
            get
            {
                XmlSerializer mySerializer = new XmlSerializer(typeof(BaseStats));
                StringWriter outStream = new StringWriter();
                mySerializer.Serialize(outStream, BaseStats);
                return outStream.ToString();
            }
            set
            {
                String tempStr = value;
                XmlSerializer mySerializer = new XmlSerializer(typeof(BaseStats));
                StringReader inStream = new StringReader(tempStr);
                BaseStats = (BaseStats)mySerializer.Deserialize(inStream);
            }
        }

        public void AddStatFunction(StatFunction statFunction)
        {
            if (statFunction == null)
                return;

            int stat = (int) statFunction.Stat;

            Log.Debug("Adding calculator to stat - " + statFunction.Stat);
            if(Calculators[stat] == null)
            {
                Calculators[stat] = new Calculator();
            }

            Calculators[stat].Add(statFunction);
        }

Now I wanted to point out here that if you want to use a different set of stats than the ones I have used, you will want to open up the StatsUtil class and the Stats enum. By making the Stats enum your list of stats starting with the base stats, you can then create the list in the StatsUtil which will mark them as base stats for the BaseStats class. This means that at any time, you can open up that set of classes and add or remove stats as you see fit. I wanted to make it quick and easy for anyone to make this game unique to them rather than force them into a set list of stats that may not suit the genre or the game design itself.

Lastly I wanted to leave you with the Formulas class. It shows how to build a unique list of stats that won't change in the major part of the game. these are usually the stat formulas that get modified on the base character itself. I have one example coded which is the HPMax. I chose to make it 40 + (5 * level) + Math.Floor(vitality / 10).

Formulas.cs

namespace AegisBorn.Models.Base.Actor.Stats.Calculators
{
    public class Formulas
    {

        public static void AddCalculatorsToNewCharacter(CharacterStats stats)
        {
            stats.AddStatFunction(MaxHPFunction.Instance);
        }

        public class MaxHPFunction : StatFunction
        {

            private static MaxHPFunction _instance;

            public static StatFunction Instance
            {
                get { return _instance ?? (_instance = new MaxHPFunction()); }
            }

            // Slot 0x10 is slot 16, this leaves 15 slots for functions that can happen before it.
            private MaxHPFunction()
                : base(Stats.Max_HP, 0x10, null)
            {
            }

            #region Overrides of StatFunction

            public override void Calc(CalculatorValue calculatorValue)
            {
                double levelMult = 5 * (calculatorValue.Player == null ? 1 : calculatorValue.Player.Stats.Level);
                double vitDiv = (calculatorValue.Player == null ? 1 : calculatorValue.Player.Stats.GetValue(Stats.VIT)) / 10.0;
                calculatorValue.Value += 40 + levelMult + vitDiv;
            }

            #endregion
        }
    }
}

Now inside this class i created another class called MaxHPFunction. This function does just what I said, when Calc is called, it returns a number 40 plus 5 for every level we are, plus one for every 10 points of vitality. This is where I will keep a running list of all stat calculators for the base stats such as starting armor, starting MP, etc.

Pretty simple this time around. Next time we will create a DTO (DataType Object) which we will use to store our character in the database. So there we have the Stats class. Along with the DTO we will detail how I set up the inheritance from AegisBornObject to AegisBornPlayer.

Comments

lazalong's picture

Impressive work

Just one question. Sorry if I missed it.
How do you plan to modify the base stats values?
For example adding +1 to VIT as I see that CharacterStats.BaseStats is private?

The only way would be to get the BaseStatsXML (from DB), modify it, reload it in CharacterStats and -probably- put it back in DB to save the modified derived stats. Seems a bit complicated, no? Modify the value in CharacterStats and save the xml in DB seems easier.

Actually, you do just like you would any other stat. You would add a statFunctionAdd(Stats.Vit, 0x11, null, StatementConstant(1));

If you want to permanently increase it, I haven't written in that portion yet. I will be adding a function to character stats that will call BaseStat.IncreaseBaseStat(Stats.Vit, 5) for example. Then the next time the character gets saved (log off, timer) it'll be written into the DB in the XML format. You don't need to read anything out of the database except when the character gets loaded when they first log in.