Design Docs - Thinking about Characters - Part 6

A word of caution! This chapter was extremely difficult for me to write. The main reason had been as a result of myself having to refactor a large portion of code mid-way during the implementation of the character job class system.

Unfortunately, it was a lot more work than I expected, and ultimately I had refactored in way that wouldn't allow me to get away with not explaining what I had done. If I had chose to skip the explanation of all the changes, I believe that I would have written this chapter with a lot of context missing in trying to understand how jrpg-system does character job classes.

Yikes

Therefore, in order to be effective in communicating how jrpg-system does character job classes, I'll have to rewind pretty far, and explain some of the changes in parts of the code that ultimately lead to implementing a semi-clean character job class system.

It's going to be a big chapter, so grab a cup of coffee, or 10 cups ☕️ , and let's tackle this journey together!

Introduction

Most of us have jobs to pay the bills. Sometimes we're good at them, sometimes we're not. For myself, it's important that I am good at my job, because I need to pay for rent, groceries, and maybe a vacation once in a while. Let's not also forget saving up enough for retirement, and all that other stuff that responsible adults are supposed to do (supposedly?).

We're now closing into the last element of what makes a JRPG character. This last element is the profession. Character job classes are the example of profession, and this will be the final subsystem that will be discussed with respect to the Character Management System we have created together so far.

Character Job Classes

In JRPGs, characters can take on jobs that bring them a variety of elements to their growth:

  • Different jobs cause certain statistics to grow (and scale) at different rates. For example, a Warrior will have their attack, and defense stat grow much faster than a Mage. Conversely, the Mage will have their magic and intelligence grow much faster than the Warrior. The different growth rates of these statistics provide different quality of the character -- adding a different dimension to their personality aside from the story.
  • Character job classes also restrict certain types of equipment to be worn on the character. Not all classes can wield a sword and shielf. You'll have some characters, depending on their class, prefer to carry a staff, or maybe a two claws for fist-fights.
  • Characters can have different affinities towards the type of techniques they are able to learn and perform. For sure, a characer who is a Thief will likely posses certain skills which make them adept at evading in battles, or perhaps stealing from the enemy.

Those are just some of the different realizations we obtain from having the character job class concept. It really does make a JRPG game much more interesting in that our characters can be better and worse at different areas than others.

The Great Refactor

My initial goal, and still, the goal of jrpg-system was to create a framework for creating JRPGs. Over time, after introducting things such as statistics, status effects, and techniques, the codebase has grown larger, but has been sloppily implement for the sake of productivity.

Taking an elevated look at jrpg-system, I have come to admit that I've been adding code in places where they shouldn't have been added. For example, looking at concrete status effects such as Poison, Regen and techniques such as Fire, Fira, etc actually belong outside of the jrpg-system project. Ideally, these types of objects are more appropriate for being implemented by the game using jrpg-system.

Is it too late to actually write code to shoulder that responsibility to the game developer? Certainly not. That's why it makes sense to actually do our best now to perform a slight refactoring of the framework. 😄

Okay, now here's a disclaimer... this refactor won't make the codebase perfect, but it will set us up to be at a cleaner state. We can then start making it a habit to clean everything up over time. Heck, maybe add more tests while we are at it!

The main idea I am driving here is that we should not expect to solve all our problems at once, and only solve enough to unblock the road. There is a sense of balance I will need to maintain if I ever want to get enough of this project done to make a game! 🎮

Roadmap

  • I will be discussing a basic design of character job classes, and the requirements which we would find useful. It will also segue into what changes are needed to be done in the current system to accomodate these basic requirements.

  • Here is what I think might be appropriate to discuss in order, the compnents I had to rewrite and move around:

    • Statistics
    • Status Effects
    • Techniques
  • Finally, we'll implement the the design, and write the tests needed to demonstrate the character job class subsystem.

Getting the Code

As usual, grab the code from the GitHub repository, navigate to here for the specific commit/tag of most of the code we will be discussing in this post.

https://github.com/urbanspr1nter/jrpg-system/commit/0989cc386752ed2bc6c722e6c0bb5c11c390b74d

Character Job Classes

I like to keep things simple in the beginning. The scope of character job classes in jrpg-system is to create basic job class system in the context of a JRPG. Therefore, for now, all our character job class system needs to be able to fulfill are these three requirements:

  • Allow the character gain different growth rates against different statistics when gaining experience and leveling up.
  • Allow the character to posses a variety of techniques only found within the character job class they are currently trying to master.
  • Allow the character to "hot-swap" character job classes, but nothing more.

My plan is to build out a system which can perform the functions above and gradually improve it with more features that make sense for JRPG character job classes.

With the above, we'll probably end up with character job classes functioning closer to the original Final Fantasy for the NES, but where we want to get to is closer to Final Fantasy V for the SNES.

Final Fantasy Job Class

From here... to...

Final Fantasy V Job class system

There.

Setting Up

In an effort for me to make things less confusing, I'll be noting some facts up-front.

  • Most of the additions, and changes will still happen in the Jrpg.CharacterSystem namespace. We'll be building out the Jrpg.CharacterSystem.Classes subsystem.

Statistics

The most basic thing about character job classes is that they change the character's basic statistics. Since this quality about character job classes is most easily understood, I think I'll start with the discussion in shaping our current character statistics implementation to fit with the new character job class components to be developed!

There were a few qualities about how I managed statistics I did not like.

  1. Building a Dictionary<StatisticType, Statistic> and then loading it the resources which needed a set of statistics was cumbersome, and also inflexible in that we would have to hard-code, or define a set of values ahead of time.
  2. The Character and set of Dictionary<StatisticType, Statistic> were coupled too closely with each other. This woul cause a problem in the future when character classes need to alter the statistics when being switched back and forth.

The most inflexible part about the current statistics system is that the Dictionary<StatisticType, Statistic> is currently loaded with default values which are static within code. Currently the usage is somewhere along the lines of this:

var startingStatistics = new Dictionary<StatisticType, Statistic>();

var strengthStat = new Statistic(..);
strengthStat.currentvalue = 12;

startingStatistics.Add(StatisticType.Strength, strengthStat);

var magicStat = new Statistic(...);
magicStat.currentValue = 3;

startingStatistics.Add(StatisticType.Magic, magicStat);

....

Whew! That is actually a lot of work to just create a set of statistics to be loaded into the Character! What would be more ideal is that the game provides some hints through JSON definition files so that we can dynamically load these files and intelligently instantiate the Statistic to be added into the set of statistics.

A simple item can have this type of schema:

[
	{
		"Name": "Strength",
		"Value": 12
	},
	...,
	{
		"Name": "Magic",
		"Value": 3
	}
]

We can also introduce a new object Jrpg.CharacterSystem.Classes.Definitions.ClassStatistic which can represent an item within the data set:

using System;
namespace Jrpg.CharacterSystem.Classes.Definitions
{
    public class ClassStatistic
    {
        public string Name { get; set; }
        public int Value { get; set; }
    }
}

Potentially, we can load the JSON as a string, and then use a deserializer to load the objects into memory:

var statisticDefRaw = File.ReadAllText("Statistics.json");

var statisticDefinitions 
	= Newtonsoft.Json.JsonConvert.DeserializeObject<List<ClassStatistic>>(statisticDefRaw);

var startingStatistics = BuildFromDefinitions(statisticDefinitions);

Alright, the above abstracts a lot of the manual building out but how do we actually provide the means to translate the definition files into the ClassStatistic object? Well, there's where handy-dandy "util" methods come in handy! Jrpg.CharacterSystem.CommUtils is a new class that will for now, serve as a dumping ground fo all relevant utility methods for the character management system.

CommonUtils.cs
sing System;
using Jrpg.CharacterSystem.Classes.Definitions;
namespace Jrpg.CharacterSystem
{
    public class CommonUtils
    {
        public static StatisticType ToStatisticType(string label)
        {
            switch(label)
            {
                case "HP Current":
                    return StatisticType.HpCurrent;
                case "HP Max":
                    return StatisticType.HpMax;
                case "MP Current":
                    return StatisticType.MpCurrent;
                case "MP Max":
                    return StatisticType.MpMax;
                case "Experience":
                    return StatisticType.Experience;
                case "Strength":
                    return StatisticType.Strength;
                case "Speed":
                    return StatisticType.Speed;
                case "Stamina":
                    return StatisticType.Stamina;
                case "Magic":
                    return StatisticType.Magic;
                case "Attack":
                    return StatisticType.Attack;
                case "Defense":
                    return StatisticType.Defense;
                case "Evasion":
                    return StatisticType.Evasion;
                case "Magic Defense":
                    return StatisticType.MagicDefense;
                case "Magic Evasion":
                    return StatisticType.MagicEvasion;
                default:
                    return StatisticType.Level;
            }
        }

        public static Statistic ToStatistic(ClassStatistic stat)
        {
            var statisticType = ToStatisticType(stat.Name);
            var statistic = new Statistic(statisticType, StatisticTypeCollection.MaxValues[statisticType])
            {
                CurrentValue = stat.Value
            };

            return statistic;
        }
    }
}
  • CommonUtils::ToStatisticType will translate the label found in ClassStatistic.Name to a valid StatisticType. This is used indirectly by CommonUtils::ToStatistic
  • CommonUtils.ToStatistic will receive a deserialized ClassStatistic and transform it into a Statistic type.

Well, cool! We've solved how to actually make building out groups of statistics less static-y for characters. Now, what about the situation relating to the set of statistics being coupled too tightly with Character? Let's solve that now!

As I was building out the character job class system, I had come to realization that statistics at the character-level made sense if-and-only-if the character need not take on any job classes. With the fact that taking on different character job classes meant different statistic values and growth rates, It no longer made any sense for it to be tied to the same level as Character.

The more appropriate solution I had found was to tie the statistics to the job class. Here is where we actually introduce a new class called BaseCharacterClass which implements ICharacterClass that will not only hold the Statistics, but also the TechniqueDefinitions necessary to define the character job class.

 

ICharacterClass.cs
using System;
namespace Jrpg.CharacterSystem.Classes
{
    public interface ICharacterClass
    {
        Statistic NextLevel();
        Statistic NextHpMax();
        Statistic NextMpMax();
        Statistic NextStrength();
        Statistic NextSpeed();
        Statistic NextStamina();
        Statistic NextMagic();
        Statistic NextAttack();
        Statistic NextDefense();
        Statistic NextEvasion();
        Statistic NextMagicDefense();
        Statistic NextMagicEvasion();
        string ClassName();
        void LevelUp();
    }
}

 

BaseCharacterClass.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.Techniques;

namespace Jrpg.CharacterSystem.Classes
{
    public abstract class BaseCharacterClass : ICharacterClass
    {
        public Dictionary<StatisticType, Statistic> Statistics { get; set; }
        public List<TechniqueDefinition> TechniqueDefinitions { get; private set; }

        public BaseCharacterClass(Dictionary<StatisticType, Statistic> statistics)
        {
            Statistics = statistics;
            TechniqueDefinitions = new List<TechniqueDefinition>();
        }

        public virtual void LevelUp()
        {
            Statistics[StatisticType.Level] = NextLevel();
            Statistics[StatisticType.HpMax] = NextHpMax();
            Statistics[StatisticType.MpMax] = NextMpMax();
            Statistics[StatisticType.Strength] = NextStrength();
            Statistics[StatisticType.Speed] = NextSpeed();
            Statistics[StatisticType.Stamina] = NextStamina();
            Statistics[StatisticType.Magic] = NextMagic();
            Statistics[StatisticType.Attack] = NextAttack();
            Statistics[StatisticType.Defense] = NextDefense();
            Statistics[StatisticType.Evasion] = NextEvasion();
            Statistics[StatisticType.MagicDefense] = NextMagicDefense();
            Statistics[StatisticType.MagicEvasion] = NextMagicEvasion();
        }

        public virtual void AddTechnique(TechniqueDefinition techniqueDefinition)
        {
            TechniqueDefinitions.Add(techniqueDefinition);
        }

        public virtual Statistic NextLevel()
        {
            var statistic = new Statistic(StatisticType.Level, StatisticTypeCollection.MaxValues[StatisticType.Level]);

            statistic.CurrentValue = Statistics[StatisticType.Level].CurrentValue + 1;

            return statistic;
        }

        public virtual Statistic NextHpMax()
        {
            var statistic = new Statistic(StatisticType.HpMax, StatisticTypeCollection.MaxValues[StatisticType.HpMax]);

            statistic.CurrentValue = Statistics[StatisticType.HpMax].CurrentValue;

            return statistic;
        }

      	// ...
				/** Abridged! */
    		// ...
      
        public abstract string ClassName();
    }
}

Now, internally, the Character class can now hold an instance of BaseCharacterClass as the currentClass property. The main goal here is that we will have classes such as Warrior, Mage, or Blackblet which inherit the BaseCharacterClass and extend it with different types of attributes.

To keep things backwards compatible, the Character::Statistics getters and setters will now reference to the statistics found in the current instance of BaseCharacterClass. I'll only briefly touch upon the changes here:

Character.cs
...
        public Dictionary<StatisticType, Statistic> Statistics {
            get
            {
                return currentClass.Statistics;
            }
            set
            {
                currentClass.Statistics = value;
            }
        }
        
        ...
        
        private BaseCharacterClass currentClass;
        
        ...
        
        public Character(string name)
        {
            Body = new CharacterBody();
            Name = name;
            var startingStats = new Dictionary<StatisticType, Statistic>();

            var StatMaxes = StatisticTypeCollection.MaxValues;
            var DefaultValues = StatisticTypeCollection.DefaultValues;
            foreach(var statType in StatisticTypeCollection.All)
            {
                startingStats.Add(statType, new Statistic(statType, StatMaxes[statType]));
                startingStats[statType].CurrentValue = DefaultValues[statType];
            }

            currentClass = new Freelancer(startingStats);
        }

        public Character(string name, BaseCharacterClass defaultJobClass)
        {
            Body = new CharacterBody();
            Name = name;
            currentClass = defaultJobClass;
        }
        
        ....
        
        public void ChangeClass(BaseCharacterClass jobClass)
        {
            currentClass = jobClass;
        }
        
 ...

I've cut out a lot of stuff here, but it makes it easier to highlight 2 parts:

  • There are now 2 different constructors for Character. We can start out with

    1. a default character with a name and default statistics, or
    2. a character with a name and an initial job class
  • The character's job class can also be changed dynamically through ChangeClass. Right now, we just care about changing the job class. Handling how the stats and techniques get restored is a discussion for later. 😄

We'll still want to provide our default character job class -- Freelancer as the option that the Character takes on should the default constructor with only the name provided be used. Let's make changes to that class to make sure it now inherits BaseCharacterClass.

Freelancer.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.Scalers;
using Jrpg.CharacterSystem.Scalers.Freelancer;

namespace Jrpg.CharacterSystem.Classes
{
    class Freelancer : BaseCharacterClass
    {
        private IStatisticScaler hpScaler;
        private IStatisticScaler mpScaler;
        private IStatisticScaler strengthScaler;
        private IStatisticScaler speedScaler;
        private IStatisticScaler staminaScaler;
        private IStatisticScaler magicScaler;
        private IStatisticScaler attackScaler;
        private IStatisticScaler defenseScaler;
        private IStatisticScaler evasionScaler;
        private IStatisticScaler magicDefenseScaler;
        private IStatisticScaler magicEvasionScaler;

        public Freelancer(Dictionary<StatisticType, Statistic> statistics) : base(statistics)
        {
            hpScaler = new HpScaler();
            mpScaler = new MpScaler();
            strengthScaler = new StrengthScaler();
            speedScaler = new SpeedScaler();
            staminaScaler = new StaminaScaler();
            magicScaler = new MagicScaler();
            attackScaler = new AttackScaler();
            defenseScaler = new DefenseScaler();
            evasionScaler = new EvasionScaler();
            magicDefenseScaler = new MagicDefenseScaler();
            magicEvasionScaler = new MagicEvasionScaler();
        }

        public override Statistic NextHpMax()
        {
            return hpScaler.NewStatistic(Statistics);
        }

        public override Statistic NextMpMax()
        {
            return mpScaler.NewStatistic(Statistics);
        }

				// ...
      	// Abridged!
      	// ...

        public override Statistic NextMagicEvasion()
        {
            return magicEvasionScaler.NewStatistic(Statistics);
        }

        public override string ClassName()
        {
            return "Freelancer";
        }
    }
}

Now, suppose we are now the game developer using jrpg-system to develop some JRPG game. We can quickly spin up a very basic Black Mage by just extending BaseCharacterClass and loading it up with a default set of statistics.

using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes;
namespace Jrpg.SampleGame.Characters.JobClasses
{
    public class BlackMage : BaseCharacterClass
    {
        public BlackMage(Dictionary<StatisticType, Statistic> statistics) : base(statistics)
        {
        }

        public override string ClassName()
        {
            return "Black Mage";
        }
    }
}

Then we can just create the instance and manually load up the techniques, for now:

var BlackMage = new BlackMage(GetDefaultBlackMageStats());

BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Fire"]);
BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Fira"]);
BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Firaga"]);

Black Mage

Phew, getting tired yet? Time for a coffee break? Oh, no? Still want to go?! Okay, onwards to status effects...

Status Effects

I don't really have a true reason, or explanation on how this ties into the character job class system, but one thing that really bothered me was how static status effects were in the game. It didn't make sense to have concrete implementations of status effects such as Poison laying around the codebase.

I originally implemented them for the sake of discussion, but now, it is time to clean it up and actually put the power of building out status effects to the developer!

Similar to how we build out a definition for statistics with ClassStatistic, we want to also do something similar for status effects. This way, concrete implementations of status effects can be created and loaded from a different module -- likely to be from the game library consuming jrpg-system.

For that to happen, let's create a StatusEffectDefinition type which will be living in Jrpg.CharacterSystem.StatusEffects.Definitions.

StatusEffectDefinition.cs
using System;
namespace Jrpg.CharacterSystem.StatusEffects.Definitions
{
    public class StatusEffectDefinition
    {
        public string Id { get; set; }
        public string DisplayName { get; set; }
        public string Agent { get; set; }
    }
}
  • Id while not going to be used here, is something that is "nice-to-have" in that we can probably leverage that property in the future. Ex. using this property as a tag name for a game object in Unity.
  • DisplayName is the name of the status effect that will be rendered on screen, or referenced. For example Poison, or Regen.
  • Agent, this is the fully qualified assembly name for the concrete implementation of the status effect which will instantiated.

Again, let's use JSON as a method in defining status effects. Suppose we have a game called Jrpg.SampleGame. A typical file can look like:

[
  {
    "Id": "Status_Regen",
    "DisplayName": "Regen",
    "Agent": "Jrpg.SampleGame.StatusEffects.Regen, Jrpg.SampleGame"
  },
  {
    "Id": "Status_Poison",
    "DisplayName": "Poison",
    "Agent": "Jrpg.SampleGame.StatusEffects.Poison, Jrpg.SampleGame"
  },
  {
    "Id": "Status_Mini",
    "DisplayName": "Mini",
    "Agent": "Jrpg.SampleGame.StatusEffects.Mini, Jrpg.SampleGame"
  }
]

Then our concrete implementation of Regen, Poison, and Mini can live in the Jrpg.SampleGame library. That's really far away from jrpg-system. 😎

Now, Poison in Jrpg.SampleGame can just be implemented like this:

using System;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.StatusEffects;
using Jrpg.GameState;

namespace Jrpg.SampleGame.StatusEffects
{
    class Poison : StatusEffect
    {
        public override void PerformEffect(Character character, GameStateValue state)
        {
            if(state == GameStateValue.Menu)
            {
                return;
            }

            character.Statistics[StatisticType.HpCurrent].CurrentValue -= 2;
        }

        public override string GetStatusEffectName()
        {
            return "Poison";
        }
    }
}

If now you're asking:

Now, how does that change the way status effects are now instantiated? Surely our current way of reading the types statically isn't going to work anymore!

Then you're very smart! Yes, we'll have to change the way status effects are created. Instead of relying on a StatusEffectType like this before:

using System;	
namespace Jrpg.CharacterSystem.StatusEffects	
{	
    public enum StatusEffectType	
    {	
        Poison,	
        Mini,	
        Regen	
    }	
}

We'll now delete this entirely, and actually build a smarter system in creating status effect instances. Instead of having the concept of a StatusEffectType, we'll now refernece them by the "status effect name" -- which is essentially just a basic string type.

The basic StatusEffect class now just returns a string when getting the status effect name:

using System;
using Jrpg.GameState;
namespace Jrpg.CharacterSystem.StatusEffects
{
    public abstract class StatusEffect: IStatusEffect
    {
        public virtual void AfterEffect(Character character, GameStateValue state)
        {
            return;
        }

				// ...
				// Abridged!
				// ...

        public abstract string GetStatusEffectName();
    }
}

The StatusEffectFactory also changes pretty significantly in that we no longer have a bunch of if-else statements checking against the intended StatusEffectType to create.

Instead it:

  • Provides a registry of the status effect name to the StatusEffectDefinition which will be initialized upon start up with all the status effect definitions which can be used to instantiate a status effect instance.
  • Provides the Register method to register the aforementioned.
  • BuildStatusEffect changes in that it now takes the status effect name as a parameter, and references the registry to obtain the StatusEffectDefinition registered by the name and creates the instance dynamically.
StatusEffectFactory.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.StatusEffects.Definitions;

namespace Jrpg.CharacterSystem.StatusEffects
{
    class StatusEffectFactory
    {
        private Dictionary<string, StatusEffectDefinition> registered;

        public StatusEffectFactory()
        {
            registered = new Dictionary<string, StatusEffectDefinition>();
        }

        public void Register(string name, StatusEffectDefinition definition)
        {
            registered[name] = definition;
        }

        public StatusEffect BuildStatusEffect(string name)
        {
            if(!registered.ContainsKey(name))
            {
                throw new KeyNotFoundException("The status effect definition was not found with this name.");
            }

            var definition = registered[name];
            var statusEffect = (StatusEffect)Activator
                .CreateInstance(Type.GetType(definition.Agent), new object[] { });

            return statusEffect;
        }
    }
}

We will now also need to have StatusEffectManager accommodate to these changes. The changes aren't huge. Since StatusEffectType no longer exists, we'll just have to change the method signatures to receive a string for the name, and change some of the logic around for that. I'm not going to show everything here, but here's a basic example with StatusEffectManager::ApplyEffect

        public void ApplyEffect(Character character, string name)
        {
            if(!map.ContainsKey(character))
            {
                map.Add(character, new List<StatusEffect>());
            }

            // Cannot stack status effects
            if(map[character].Find(effect => 
            		effect.GetStatusEffectName().Equals(name)) != null)
            {
                return;
            }

            var statusEffect = factory.BuildStatusEffect(name);
            map[character].Add(statusEffect);
            statusEffect.OnApply(character, _state);
        }

The new method here is actually StatusEffectManager::RegisterStatusEffect. It does exactly what it says, it provides registration of the status effect name with the status effect definition to the registry found in StatusEffectFactory.

Since I'm writing this now, I realize that this could have just sat on top of StatusEffectManager -- leaving StatusEffectFactory to just handle instantiation of the status effect. Well, too late now -- I'll change it later. 😆

public void RegisterStatusEffect(string name, StatusEffectDefinition definition)
{
	factory.Register(name, definition);
}

Basic usage of this can look like:

var StatusEffectDefinitions = Newtonsoft.Json.JsonConvert
  .DeserializeObject<List<StatusEffectDefinition>>
  	(File.ReadAllText("Resources/StatusEffects.json"));

foreach (var definition in StatusEffectDefinitions)
{
  StatusEffectManager.RegisterStatusEffect(definition.DisplayName, definition);
}

....
  
GameStore.StatusEffectManager.ApplyEffect(myCharacter, "Poison");

// "myCharacter" is now Poisoned!

That's about it for the status effect changes, we'll have 1 more refactor discussion with techniques before go off implementing the character job classes... But, we're almmost there!!!

Almost there

... We're almost there... 💊

Techniques

Alright! I think by now you're starting to see a pattern. This is getting easier for me to explain. 😄 Let's make techniques dynamic and not generated statically. As with statistics, and status effects, we're going to make use of a definition file to define the basic attributes of techniques.

Technique definitions can resemble this in JSON:

[
  {
    "Id": "Tech_Fire",
    "DisplayName": "Fire",
    "MpCost": 10,
    "AttackPower": 0,
    "MagicPower": 5,
    "Agent": "Jrpg.SampleGame.Techniques.Fire, Jrpg.SampleGame"
  },
  {
    "Id": "Tech_Fira",
    "DisplayName": "Fira",
    "MpCost": 15,
    "AttackPower": 0,
    "MagicPower": 15,
    "Agent": "Jrpg.SampleGame.Techniques.Fira, Jrpg.SampleGame"
  },
  {
    "Id": "Tech_Firaga",
    "DisplayName": "Firaga",
    "MpCost": 30,
    "AttackPower": 0,
    "MagicPower": 45,
    "Agent": "Jrpg.SampleGame.Techniques.Firaga, Jrpg.SampleGame"
  },
  {
    "Id": "Tech_Regen",
    "DisplayName": "Regen",
    "MpCost": 18,
    "AttackPower": 0,
    "MagicPower": 45,
    "Agent": "Jrpg.SampleGame.Techniques.Regen, Jrpg.SampleGame"
  }
]

Again, we leave it up to the game developer to define the concrete implementations of techniques.

TechniqueDefinition is the type in code which represents a definition found in the above list of definitions. You're probably already familiar with the attributes, as I had discussed them in the previous article.

TechniqueDefinition.cs
using System;
namespace Jrpg.CharacterSystem.Techniques
{
    public class TechniqueDefinition
    {
        public string Id { get; set; }
        public string DisplayName { get; set; }
        public int MpCost { get; set; }
        public int AttackPower { get; set; }
        public int MagicPower { get; set; }
        public string Agent { get; set; }
    }
}

Similar to StatusEffectFactory, and with the same mistakes 😉, we have TechniqueFactory accommodate to the changes by taking in a TechniqueDefinition object, rather than TechniqueName. We will also introduce a helper method that will consume a JSON file and turn it into a list of definitions. We can possibly move this to CommonUtils or something down the road.

We still use reflection extensively to build out the instances of the Technique using Activator.

TechniqueFactory.cs
  
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.StatusEffects;

namespace Jrpg.CharacterSystem.Techniques
{
    public class TechniqueFactory
    {
        private StatusEffectManager StatusEffectManager;

        public TechniqueFactory(StatusEffectManager statusEffectManager)
        {
            StatusEffectManager = statusEffectManager;
        }

        public Technique CreateTechnique(TechniqueDefinition definition)
        {
            try
            {
                var instance = Activator.CreateInstance(
                    Type.GetType(definition.Agent),
                    new object[] { StatusEffectManager, definition }
                );

                return (Technique)(instance);
            } catch(TypeLoadException e)
            {
                Console.WriteLine("Couldn't load the technique");
                throw e;
            }
        }

        public Dictionary<string, TechniqueDefinition> FromJsonDefinition(string json)
        {
            var definitions = 
              Newtonsoft.Json.JsonConvert
              	.DeserializeObject<List<TechniqueDefinition>>(json);

            var results = new Dictionary<string, TechniqueDefinition>();

            foreach(var definition in definitions)
            {
                results.Add(definition.DisplayName, definition);
            }

            return results;
        }
    }
}

The base Technique type itself no longer needs the hardcoded properties Id, DisplayName, etc. Instead they are now found in TechniqueDefinition. So, let's rewrite Technique to hold an instance of TechniqueDefinition instead.

Technique.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.StatusEffects;

namespace Jrpg.CharacterSystem.Techniques
{
    public abstract class Technique : IPerformable
    {
        protected StatusEffectManager StatusEffectManager;
        public TechniqueDefinition Definition { get; protected set; }

        public abstract void Perform(Character source, List<Character> targets);

        public Technique(StatusEffectManager statusEffectManager, TechniqueDefinition definition) {
            StatusEffectManager = statusEffectManager;
            Definition = definition;
        }
    }
}

We have now made the process much easier to create and load new techniques! Jrpg.SampleGame can now implement its own Regen technique, and not have to rely on the implementation found in jrpg-system!

using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.StatusEffects;

namespace Jrpg.SampleGame.Techniques
{
    public class Regen : Technique
    {
        public Regen(StatusEffectManager statusEffectManager, TechniqueDefinition definition)
            : base(statusEffectManager, definition)
        {
        }

        public override void Perform(Character source, List<Character> targets)
        {
            foreach(var target in targets)
            {
                StatusEffectManager.ApplyEffect(target, "Regen");
            }

            source.Statistics[StatisticType.MpCurrent].CurrentValue -= Definition.MpCost;
        }
    }
}

😌 😤 Yes! That was short and effective! I just spent the past 4000 words or so setting us up for the best part... Implementing character job classes!

Character Job Classes

Before making the same mistakes as before, let's actually learn from what we have done and from the get-go, and have character job classes be loaded dynamically from definitions. 😄 Again, let's use a JSON format for this.

The typical definition is pretty basic. Character job classes will have the following properties:

  • Name - The name of the job class
  • Agent - The fully qualified assembly name which implements the job class derived fromBaseCharacterClass
  • Techniques - The list of techniques which
  • StartingStatistics - The default set of statistics to be applied to the character. This includes the appropriate scaling logic for each level up the character receives.
ClassDefinition.cs
using System;
using System.Collections.Generic;
namespace Jrpg.CharacterSystem.Classes.Definitions
{
    public class ClassDefinition
    {
        public string Name { get; set; }
        public string Agent { get; set; }
        public List<ClassTechniqueDefinition> Techniques;
        public List<ClassStatistic> StartingStatistics;
    }
}

ClassTechniqueDefinition also exists as a way to represent which technique should be made available at a specific level for the particular job class.

ClassTechniqueDefinition.cs
using System;
namespace Jrpg.CharacterSystem.Classes.Definitions
{
    public class ClassTechniqueDefinition
    {
        public int Level { get; set; }
        public string Name { get; set; }
    }
}

We can then easily represent a character job class through a JSON file which leverages the above definitions. A sample JSON for a Black Mage is shown below.

  {
    "Name": "Black Mage",
    "Agent": "Jrpg.SampleGame.Characters.JobClasses.BlackMage, Jrpg.SampleGame",
    "Techniques": [
      {
        "Level": 1,
        "Name": "Fire"
      },
      {
        "Level": 10,
        "Name": "Fira"
      },
      {
        "Level": 30,
        "Name": "Firaga"
      }
    ],
    "StartingStatistics": [
      {
        "Name": "Level",
        "Value": 1
      },
      {
        "Name": "HP Current",
        "Value": 30
      },
      {
        "Name": "HP Max",
        "Value": 30
      },
      {
        "Name": "MP Current",
        "Value": 10
      },
      {
        "Name": "MP Max",
        "Value": 10
      },
      {
        "Name": "Experience",
        "Value": 0
      },
      {
        "Name": "Strength",
        "Value": 1
      },
      {
        "Name": "Speed",
        "Value": 1
      },
      {
        "Name": "Stamina",
        "Value": 1
      },
      {
        "Name": "Magic",
        "Value": 1
      },
      {
        "Name": "Attack",
        "Value": 1
      },
      {
        "Name": "Defense",
        "Value": 1
      },
      {
        "Name": "Evasion",
        "Value": 1
      },
      {
        "Name": "Magic Defense",
        "Value": 1
      },
      {
        "Name": "Magic Evasion",
        "Value": 1
      }
    ]
  }

For now, everything looks pretty clean. We have separated the techniques away from the statistics, and it is clear what a job class provides.

One thing that might not make sense right now upon reading this is the intention of starting statistics being static. The approach I am taking is quite simple, and probably towards the naive side.

If we recall, statistics not only represent the attribute and current value, but also future values through the means of scalers. These scalers have different implementations to determine how the next value is calculated upon leveling up.

Here's how it can work:

  1. Obtain the current level of the character.
  2. Switch job classes, resetting the level to 1.
  3. Level the character up again until they are at the previous level. The scalers will then calculate each statistic attribute on each successive level.

In theory, it seems like it can work, but I haven't tried it out yet. 😛

Job Class Management

There are a lot of objects to juggle, and we'll need a way to manage the components involved in switching character job classes sanely. ClassManager is a super cheap way to do that. Like all our "manager" classes so far, it does basically more, or less the same conceptually.

  • Maintain a dictionary of registered job classes to their ClassDefinition.
  • Be a factory to instantiate new instances of a job class by using the definitions found in the registry. Use the fully qualified assembly name found in the ClassDefinition.Agent property to instantiate this object.
  • Deserialize the list of class definitions from a JSON file and populate the registry, if needed.
ClassManager.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.Classes.Definitions;

namespace Jrpg.CharacterSystem.Classes
{
    public class ClassManager
    {
        private Dictionary<string, ClassDefinition> registered;

        public ClassManager()
        {
            registered = new Dictionary<string, ClassDefinition>();
        }

        public void Register(string tag, ClassDefinition classDefinition)
        {
            if(registered.ContainsKey(tag))
            {
                registered[tag] = classDefinition;

                return;
            }

            registered.Add(tag, classDefinition);
        }

        public string GetCharacterClassAgent(string tag)
        {
            if (!registered.ContainsKey(tag))
            {
                throw new 
                  KeyNotFoundException("No registered job class found with this tag.");
            }

            return registered[tag].Agent;
        }

        public BaseCharacterClass GetCharacterClassInstance(string characterClassTag)
        {
            if(!registered.ContainsKey(characterClassTag))
            {
                throw new 
                  KeyNotFoundException($"Couldn't find the character class type with key {characterClassTag}.");
            }

            var classDefinition = registered[characterClassTag];
            var jobClassAgent = classDefinition.Agent;
            var jobClassStartingStatistics = new Dictionary<StatisticType, Statistic>();

            foreach(var classStatistic in classDefinition.StartingStatistics)
            {
                var statisticType = CommonUtils.ToStatisticType(classStatistic.Name);
                var statistic = CommonUtils.ToStatistic(classStatistic);

                jobClassStartingStatistics.Add(statisticType, statistic);
            }

            var jobClassInstance = (BaseCharacterClass)Activator
                .CreateInstance(
                Type.GetType(jobClassAgent),
                new object[] { jobClassStartingStatistics }
            );

            return jobClassInstance;
        }

        public Dictionary<string, ClassDefinition> FromJsonDefinition(string json)
        {
            var definitions = 
              Newtonsoft.Json.JsonConvert.DeserializeObject<List<ClassDefinition>>(json);

            var results = new Dictionary<string, ClassDefinition>();

            foreach(var definition in definitions)
            {
                results.Add(definition.Name, definition);
            }

            return results;
        }
    }
}

The class manager is something we can also get away with in just having a single instance living throughout the lifetime of the game. Having it be initialized within the GameStore is a good approach to have for now.

I've also come to realization that I've been using reflection quite heavily to dynamically instantiate a lot of my objects. The architecture of the jrpg-system is intended to be plugin-centric, but at this moment I am not sure how much performance I am taking away with this type of pattern I am abusing.

The only way to find out is to measure and take performance marks at each step of the game loop. I think when the time comes I will do that to see if all these concerns even matter. My gut feeling tells me it doesn't.

Unit Tests

Well, okay -- assuming you've already fixed all the other tests that may have broke 😵, let's try to write some tests for our character job class stuff to validate what we have written actually works. As usual, it is appropriate to first provide a set up by modifying our MockGameLoop.

The changes here to MockGameLoop mostly involve adding additional instance variables such as the new ClassManager and StatusManager classes which will be maintained for the duration of the tests.

  • Maintain a instances of ClassManager, and StatusManager.
  • Declare 2 different job classes BlackMage and WhiteMage
  • Then, register the class names against the provided definitions for the BlackMage and WhiteMage. These should be from JSON files found in the test project.
  • Warm up the StatusManager by loading all status definitions found in the JSON files.
  • Warm up the techniques by loading all deifnitions found in the JSON files.
  • Finally, instantiate BlackMage and WhiteMage to be used by the tests.
MockGameLoop.cs
using System;
using System.IO;
using System.Collections.Generic;
using Jrpg.CharacterSystem.StatusEffects;
using Jrpg.CharacterSystem.Classes;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.StatusEffects.Definitions;
using Jrpg.GameState;
using Jrpg.SampleGame.Characters.JobClasses;

namespace Jrpg.System.Tests
{
    public class MockGameLoop
    {
        private GameStateManager gameStateManager;
        public ClassManager ClassManager { get; private set; }
        public StatusEffectManager StatusEffectManager { get; private set; }

        public BlackMage BlackMage { get; private set; }
        public WhiteMage WhiteMage { get; private set; }

        public MockGameLoop()
        {
            gameStateManager = new GameStateManager(GameStateValue.World);
            StatusEffectManager = new StatusEffectManager();

            ClassManager = new ClassManager();
            var ClassDefinitions = ClassManager.FromJsonDefinition(
                File.ReadAllText("Resources/CharacterClasses.json")
            );
            foreach (var className in ClassDefinitions.Keys)
            {
                ClassManager.Register(className, ClassDefinitions[className]);
            }

            var StatusEffectDefinitions = Newtonsoft.Json.JsonConvert.DeserializeObject<List<StatusEffectDefinition>>(File.ReadAllText("Resources/StatusEffects.json"));
            foreach (var definition in StatusEffectDefinitions)
            {
                StatusEffectManager.RegisterStatusEffect(definition.DisplayName, definition);
            }

            var TechniqueDefinitions = new TechniqueFactory(StatusEffectManager).FromJsonDefinition(File.ReadAllText("Resources/Techniques.json"));


            BlackMage = (BlackMage)ClassManager.GetCharacterClassInstance("Black Mage");
            BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Fire"]);
            BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Fira"]);
            BlackMage.TechniqueDefinitions.Add(TechniqueDefinitions["Firaga"]);

            WhiteMage = (WhiteMage)ClassManager.GetCharacterClassInstance("White Mage");
            WhiteMage.TechniqueDefinitions.Add(TechniqueDefinitions["Regen"]);

            gameStateManager.Register(StatusEffectManager);
        }

        public void SetGameState(GameStateValue state)
        {
            gameStateManager.PublishStateUpdate(state);
        }

        public GameStateValue GetGameState()
        {
            return gameStateManager.CurrentState();
        }

        public void Step()
        {
            StatusEffectManager.BeforeEffects();
            StatusEffectManager.PerformEffects();
            StatusEffectManager.AfterEffects();
            StatusEffectManager.CleanUp();
        }
    }
}

The file itself is starting to beecome quite large, so it might be worth a "TODO" to refactor this down the line to reflect a more realistic game loop one would expect.

Now we'll write 3 different types of tests relating to character job classes.

  1. The first test is to assign a party member, Cloud to be a White Mage, and then check to see if Regen exists as a technique.
  2. The second test is to assign a party member, Cloud to be a Black Mage, and check to see if Fire exists as a technique. If so, then we can safely assume here (lazily) that Fira and Firaga also exist.
  3. Finally, the third test is to test hot-swap of job classes against a character. Cloud will initially begin as a White Mage, and Regen will be checked to see if it exists. Then a class switch will happen in which Cloud will turn into a Black Mage. From here, we whould expect that he no longer has Regen, but has Fire.
TestCharacterClasses.cs
using System;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.InventorySystem;
using Jrpg.PartySystem;

using Xunit;

namespace Jrpg.System.Tests
{
    public class TestCharacterClasses
    {
        private MockGameLoop gameLoop;

        public TestCharacterClasses()
        {
            Console.WriteLine("---- Character Job Classes Test ----");
        }

        [Fact]
        public void TestWhiteMageShouldHaveRegen()
        {
            InventoryManager inventoryManager = new InventoryManager();
            Party party = new Party(inventoryManager);

            gameLoop = new MockGameLoop();

            party.AddMember("Cloud", new Character("Cloud", gameLoop.WhiteMage));
            party.SetActiveCharacter("Cloud");

            var cloud = party.GetMember("Cloud");
            Assert.Equal("White Mage", cloud.CurrentClassName());

            Assert.True(cloud.TechniqueDefinitions().Exists(t => t.Equals("Regen")));
        }

        [Fact]
        public void TestBlackmageShouldHaveFire()
        {
            InventoryManager inventoryManager = new InventoryManager();
            Party party = new Party(inventoryManager);

            gameLoop = new MockGameLoop();

            party.AddMember("Cloud", new Character("Cloud", gameLoop.BlackMage));
            party.SetActiveCharacter("Cloud");

            var cloud = party.GetMember("Cloud");
            Assert.Equal("Black Mage", cloud.CurrentClassName());

            Assert.True(cloud.TechniqueDefinitions().Exists(t => t.Equals("Fire")));
        }

        [Fact]
        public void TestSwitchingClasses()
        {
            InventoryManager inventoryManager = new InventoryManager();
            Party party = new Party(inventoryManager);

            gameLoop = new MockGameLoop();

            party.AddMember("Cloud", new Character("Cloud", gameLoop.WhiteMage));
            party.SetActiveCharacter("Cloud");

            var cloud = party.GetMember("Cloud");

            Assert.Equal("White Mage", cloud.CurrentClassName());
            Assert.True(cloud.TechniqueDefinitions().Exists(t => t.Equals("Regen")));
            Assert.False(cloud.TechniqueDefinitions().Exists(t => t.Equals("Fire")));

            cloud.ChangeClass(gameLoop.BlackMage);

            Assert.Equal("Black Mage", cloud.CurrentClassName());
            Assert.False(cloud.TechniqueDefinitions().Exists(t => t.Equals("Regen")));
            Assert.True(cloud.TechniqueDefinitions().Exists(t => t.Equals("Fire")));
        }
    }
}

Conclusion

Thanks for sticking with me. Massive refactoring from the coder's perspective isn't always fun. Especially if it is work one has already done which has to be re-done. Even worse, going over changes as a result from refactoring is even more unpleasant as one has to recall and justify many of the decisions and thought processes that went through to perform these changes.

This chapter served as a way to document my changes, and to share with you my flawed assumptions I had made in the past. It was also a way to sort of transparently communicate "Hey, yeah, maybe that wasn't such a great idea.".

It took me a lot of time to write all this down, but I think it was worth it.

Our next discussion will be relating to how characters can maintain some of their state while switching job classes, and some introduction to NPCs, enemies and other goodies.

See ya!

Bye