Design Docs - Thinking about Characters - Part 3

Happy Thanksgiving, everyone!

Chocobo!

My Thanksgiving holiday this year isn't action-packed, but I did celebrate it early yesterday with a couple of friends. I ended up cooking a "traditional" Thanksgiving dinner and having good conversations over pasture raised turkey from Gunthrop farms, home-made mashed potatoes, pumpkin pie, and salad.

I bought mine from Crowd Cow. Since Thanksgiving is just about up, you'll have to wait until next year if you would like to order from them. 😄 I'm just lucky I pre-ordered mine in October!

Anyway, the turkey was amazing. It was not dry at all, and the meat was very tender and fell off the bone. It wasn't just because it was "pasture-raised". The turkey being pasture-raised was beneficial in that there wasn't too much grease after roasting, but the key was the preparation and cooking techniques I had used.

Roasting Turkey Well

Here's what I did for my 16 lbs bird:

  • After defrosting the turkey for a few days in my refrigerator, I had brined the bird with a simple salt and cold water solution. For aromatics, I added bay leaves and half a large white onion in the bath.

    • I brined for a full 24 hours. My container which the turkey sat in was not big enough, so I actually did 12 hours on its back side, and rotated for another 12 hours on the breast side.
  • Once a full day of brining was complete, I had patted the turkey down very dry and used a dry rub which I tend to use for all sorts of meat. It's my own custom rub and here's what I tend to put in various proportions (to your own taste), but aimed to be on the "sweeter" side.

    • Salt, pepper, cayenne, paprika, cinnamon, brown sugar, garlic powder, and OLD BAY, baby!

Old Bay

  • Apply the dry rub generously, everywhere! Leave the turkey sit the refrigerator again for about 12 hours before roasting. Usually, do it the night before.

  • Then the morning of roasting, create a basting/sauce mixture to be brushed entirely onto the turkey.

    • Sweet Baby Ray's Honey BBQ sauce, ketchup, Kikkoman low sodium soy sauce, Huy Fong Sriracha, and Worchestire sauce.
    • Again, aim for a tangy sweetness, so if things are a bit salty, use more Honey BBQ sauce.

Roasting

  • Preheat the oven to 440 degrees fahrenheit, +/- 10 depending on how good your oven is at temperature control.

  • The cooking process is 14 hours for a 16 lbs bird.

    • Roast initially for 45 minutes at at 440.

    • Bring the temperature down to 190 degrees fahrenheit, and roast for an additional 12 hours.

    • Finally, take the temperature up to 250 degrees fahrenheit and roast for 1 hour

      • In the final hour, check the temperature of both the leg, and the breast. The leg should be at 160 degrees fahrenheit and the breast area should be about 170 degrees fahrenheit. This is just to prevent any undercooked meat.
  • NOW, STAY PATIENT. When taking the bird out, wrap it in foil and let it sit for about 40 minutes. This part is crucial as it allows the juice to work its magic in the bird to maintain the moisture.

  • SERVE.

Turkey

Yeah, I kind of slipped a little putting the turkey in the oven and some of the sauce got got into the upper heating element of the oven. Hence, the "branding" you see on the breast. 😆

So, 14 hours?! Yes, 14 hours. I actually woke up right at 3:50 AM and got to work immediately to roast. This roasting technique isn't for everyone if you intend to serve the turkey for dinner. 😄

But it was worth it.

Dinner

Kudos to my wife, Alexa for taking the awesome photos.

JRPG Status Effects

Almost all JRPG games have had a form of character status effects in one form or another. When a character becomes Poisoned during battle, what is the expectation in which you, as the player is expected to observe and do?

By being a player, and party member of a JRPG:

  • I expect that by having I, or one of my party members being Poisoned in battle, the one who is poisoned will have some damage inflicted on themselves after each turn.
  • I expect that by having I, or one of my party members being Poisoned outside of battle, the one who is poisoned will continue to take damage as the party moves across the world map, or town.
  • I expect that by having I, or one of my party members being Poisoned, the only way to alleviate ourselves from this effect is to use an item such as an antidote to remove this effect.

My hope is that there is an agreement with most of what I had just concluded about being Poisoned in a JRPG.

Final Fantasy 12 Status effects

Character status effects can force your party to be in an uncomfortable state, which gives the player an extra challenge while spelunking through a dungeon, or fighting an enemy.

Conversely, status effects can also give the player an upper hand in battle by improving their battle statistics, to enable a sense of power of the enemy. This is also an important gameplay mechanic which JRPGs should provide, in my opinion: the feeling of dominance over the baddie.

This status effect concept, and mechanic is so commonplace amongst this genre of games that it is very much worth discussing as a topic on its own, and worth a design discussion in trying to get an implementation of our own implemented.

What Does a Status Effect Do?

As hinted briefly in the previous section, status effects do not always have to impact a character negatively like Poison, Blind, Silence, or Mini. Status effects can be positive too, such as Regen, Shell, or Reflect.

  • Taking that into consideration, we need a system where status effects can be applied agnostically onto a character without the complication in considering what the status effect will do.
  • We must be very careful not to couple the status effect and character too tightly.

Status effects also have a wide variety of behaviors when they are initially applied onto a character, or removed.

  • Flexibility must be allowed and taking into consideration the behaviors and effects which mutate the internal properties of the character. For example, the statistics.
  • Status effects may also affect the character internally after each event performed. Taking the Poison status as an example, the current HP of the character is reduced after every turn in battle, or taking a step outside of battle.

Mini Status

Implementing Status Effects Into jrpg-character-system

Okay, now let's take a first stab in implementing the concept of status effects onto our character system. For now, to keep everything simple, we will extend the existing jrpg-character-system to add status effects, along with any supplemental code.

This means that for any additional code which may not be directly related to character management introduced, the code will still exist in this module for the sake of agility. My plan is to do another refactor to move code out which isn't directly related to character management in the future.

All code can still be accessed in the same repository jrpg-character-system on GitHub. For a direct link, click here: https://github.com/urbanspr1nter/jrpg-character-system.

I have tagged the state of the repository directly related to this discussion as ch03.

The Status Effect Lifecycle

Knowing these basic requirements, status effects can also carry on many types, and can be represented for now as a simple enum called StatusEffectType:

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

A StatusEffectType will just help indicate the specific type of status effect that is being applied, handled, and removed on the character. We can have many different types of status effects. The Final Fantasy series itself has many as detailed here. For now, in this discussion, we will just keep the effects to 3 for demonstration: 2 negative effects, Poison, and Mini, and 1 positive effect Regen.

A StatusEffect object can expose at a specific level, a set of predictable life cycle methods which can be executed at specific moments to operate on a character.

IStatusEffect.cs
using System;
using Jrpg.CharacterSystem.GameState;
namespace Jrpg.CharacterSystem.StatusEffects
{
    interface IStatusEffect
    {
        void OnApply(Character character, GameStateValue state);
        void BeforeEffect(Character character, GameStateValue state);
        void PerformEffect(Character character, GameStateValue state);
        void AfterEffect(Character character, GameStateValue state);
        void OnRemove(Character character, GameStateValue state);
        bool ShouldDestroy(Character character, GameStateValue state);
    }
}

Here, we have introduced an interface called IStatusEffect to ensure that all StatusEffect types will at least implement the life cycle methods that will be called within the game loop. We'll come back to discuss what GameStateValue is when state management needs to be considered.

Status Effect Life Cycle

Let's pretend we have a game loop. Architecturally, it doesn't matter what this game loop is like, but pretend that on every iteration, the game loop will call the status effect lifecycle methods for these purposes:

  • OnApply will be called when the status effect is initially applied onto the character. This method can be potentially be implemented as a set-up hook for a particular status effect.

    • One effect I can think of that would definitely use this type of life cycle method is a status effect such as Mini. We can interally save the current statistics of the character into its own instance variable, and overwrite the current statistics with a new set which will give statistic values of 1 all across the board.
  • BeforeEffect is called in each iteration of the game loop. It is a useful hook that will execute code that might be needed to be called before another type of game event not related to status effects.

  • PerformEffect is the main life cycle method that will perform the status effect logic. For something such as Poison, this method can have logic that can reduce the current HP of the character.

  • AfterEffect is called in each iteration of the game loop. It will execute code that is needed to be called after another type of game event not related to status effects.

    • This can be useful for cleaning up the state of the status effect after each iteration when PerformEffect is called.
  • OnRemove is called when the status effect is removed from the character. A common implementation will restore the character's internal state to what it was before the status effect was ever applied.

    • Going back to the Mini status effect example, this can be resetting the statistics of the character back to the original set of values.
  • ShouldDestroy returns a flag indicating whether or not the StatusEffect should be removed from the character at a specific event in the game loop iteration. This will usually be used for garbage collecting any remaining status effects when a state transition occurs such going from being in battle to being placed into the world map.

Each StatusEffect should also expose a StatusEffectType to be read so external consumers may know what type the status effect is.

StatusEffects may also choose not to implement all the life cycle methods. Some status effects such as Poison only care about reduction of HP in each iteration, and require no setup, or clean-up. Due to this, it is easier to implement a base class called StatusEffect which provide a default implementation, and a requirement to return the StatusEffectType.

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

        public virtual void BeforeEffect(Character character, GameStateValue state)
        {
            return;
        }

        public virtual void OnApply(Character character, GameStateValue state)
        {
            return;
        }

        public virtual void OnRemove(Character character, GameStateValue state)
        {
            return;
        }

        public virtual void PerformEffect(Character character, GameStateValue state)
        {
            return;
        }
      
      	public virtual bool ShouldDestroy(Character character, GameStateValue state)
        {
            return false;
        }

        public abstract StatusEffectType GetStatusEffectType();
    }
}

The Relationship Between Characters and Status Effects

The characters being created should not be allowed to manipulate their own status effects. That is why the design the decision here may be to create a "manager" class to handle status effect behaviors on a character at each iteration of the game loop.

In the spirit of object-oriented programming, we can lazily solve this coupling problem by creating a StatusEffectManager class which will be responsible in the management of applying, handling, and removing of status effects on all characters in the game.

Status effect manager

The StatusEffectManager will internally consist of a Dictionary<Character, List<StatusEffect>> to track and manage all active status effects of characters within the game.

In pseudo-code stub, here is the definition of what StatusEffectManager may look like:

using System;
using Jrpg.CharacterSystem.GameState;
using System.Collections.Generic;

namespace Jrpg.CharacterSystem.StatusEffects
{
    public class StatusEffectManager
    {
        private Dictionary<Character, List<StatusEffect>> map;
        public void ApplyEffect(Character character, StatusEffectType statusEffectType);
        public void BeforeEffects();
        public void PerformEffects();
        public void AfterEffects();
        public void RemoveEffect(Character character, StatusEffectType statusEffectType);
      	public void CleanUp();
        public List<StatusEffectType> StatusEffectTypes(Character character);
    }
}

Take note that the API of the StatusEffectManager class will also be the only "useful" API that is exposed to manipulate status effects against a character. We do not want to directly expose the StatusEffect objects to the client code, so only exposing StatusEffectManager and only allowing its methods to be invoked as the only entry points to manipulating status will prevent any illegal behaviors from happening.

Here, we can achieve the above by changing the exposure level of the StatusEffectManager to public. Also, notice how StatusEffectManager never allows the client to control the StatusEffect directly? The status effects can only be applied, or removed based on their StatusEffectType to disallow illegal usage. Of course, this decision also applies to StatusEffectManager::BeforeEffects and StatusEffectManager::AfterEffects too.

The next section will discuss about game states, and how a single status effect may behave differently depending on the game state such as being in battle, in a town or the world map. The full implementation of StatusEffectManager will also be shown.

Game States

The last piece we are now missing is that status effects can behave differently depending on whether the player is in battle, or just walking around town.

Take for example, the status effects of Blind and Silence in JRPGs. In battle, these effects may affect the behavior of a character's battle prowess. Blind will cause the accuracy of the character's melee attacks to drop, while Silence prevents use of magic by the character altogether.

But what happens when the character is holding these status effects, but is not in battle? Well, these status effects shouldn't affect the interaction of NPCs, or any game events outside of the battle state.

Therefore, it is probably worth introducing some concept of state management. If you're not familiar with how JRPGs and state management can work, I suggest reading this article as it helped me with understanding a typical JRPG game loop with state management being considered.

How to Build a JRPG: A Primer for Game Developers

The above article's Managing Game State section was very interesting.

To keep our scope small and simple, our states can be limited and represented as an enum.

GameStateValue.cs
using System;
namespace Jrpg.CharacterSystem.GameState
{
    public enum GameStateValue
    {
        World,
        Map,
        Battle,
        Menu
    }
}

The state names themselves are self-explanatory, so I won't be descriptive here in this post.

Now, in order for our StatusEffectManager to be aware of the current game state value, we need to introduce some mechanism to have the global game loop communicate its state value down to all objects which are interested in receiving any game state updates.

Well, if you know what pattern I'm going to use to solve this, you know me too well.

Yep, that's right some weird form of.... Publisher and Subscriber.

Assuming we have some sort of global game state which holds various instances of other objects, we can internally this global game state be managed and stored which will be published down to all aubscribers.

A subscriber interface can be defined like:

IGameStateSubscriber.cs
using System;
namespace Jrpg.CharacterSystem.GameState
{
    public interface IGameStateSubscriber
    {
        void ReceiveStateUpdate(GameStateValue state);
    }
}

Now, our StatusEffectManager can implement this common subscriber interface. We can now also plug in all the logic in our stub methods.

StatusEffectManager.cs
using System;
using Jrpg.CharacterSystem.GameState;
using System.Collections.Generic;

namespace Jrpg.CharacterSystem.StatusEffects
{
    public class StatusEffectManager : IGameStateSubscriber
    {
        private GameStateValue _state;
        private Dictionary<Character, List<StatusEffect>> map;

        public StatusEffectManager()
        {
            map = new Dictionary<Character, List<StatusEffect>>();
        }

        public void ReceiveStateUpdate(GameStateValue state)
        {
            _state = state;
        }

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

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

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

        public void BeforeEffects()
        {
            foreach (var character in map.Keys)
            {
                foreach (var statusEffect in map[character])
                {
                    statusEffect.BeforeEffect(character, _state);
                }
            }
        }

        public void PerformEffects()
        {
            foreach(var character in map.Keys)
            {
                foreach(var statusEffect in map[character])
                {
                    statusEffect.PerformEffect(character, _state);
                }
            }
        }

        public void AfterEffects()
        {
            foreach (var character in map.Keys)
            {
                foreach (var statusEffect in map[character])
                {
                    statusEffect.AfterEffect(character, _state);
                }
            }
        }

        public void RemoveEffect(Character character, StatusEffectType statusEffectType)
        {
            StatusEffect toRemove = null;
            foreach(var statusEffect in map[character])
            {
                if(statusEffect.GetStatusEffectType() == statusEffectType)
                {
                    toRemove = statusEffect;
                    break;
                }
            }

            toRemove.OnRemove(character, _state);

            map[character].Remove(toRemove);
        }
      
      	public void CleanUp()
        {
            foreach(var character in map.Keys)
            {
                map[character].RemoveAll(effect => effect.ShouldDestroy(character, _state));
            }
        }

        public List<StatusEffectType> StatusEffectTypes(Character character)
        {
            List<StatusEffectType> result = new List<StatusEffectType>();

            foreach(var statusEffect in map[character])
            {
                result.Add(statusEffect.GetStatusEffectType());
            }

            return result;
        }
    }
}

Walking through this code carefully, we have implemented the IGameStateSubscriber interface. StatusEffectManager now maintains its own instance of the game state value and will update the value accordingly when the publisher invokes the ReceiveStateUpdate method. This keeps the state variable of the StatusEffectManager up to date.

Then, all life cycle methods of the status effects can always be invoked with knowledge of the most current game state value. This means that status effects are always aware of the current state of the game, whether the player is currently in a menu, or battle. It is really up to the implementation of the StatusEffect to behave accordingly to this state value.

As we can see, each method of the StatusEffectManager operates on the internal map of characters to the list of status effects each character has. The appropriate life cycle methods of the status effects are called. So, there isn't much complicated logic here.

One key design decision I would like to call out here is that status effects cannot be stacked, so I prefer an early return on ApplyEffect if the status effect is already something that already exists in the character's list of active status effects.

Now that we have subscribers, we need publishers! The IGameStatePublisher is an interface which defines what a simple game state publisher should look like. Basically, a class implementing this interface should maintain a listof subscribers and have them be added and removed through calls of the Register and Unregister methods.

The publisher also allows any clients to receive the current global game state value through an accessor called CurrentState.

Finally, the PublishStateUpdate method will be responsible for iterating through the list of subscribers and calling the IGameStateSubscriber::ReceiveStateUpdate method to update their own references of the global game state.

Status effect updates

IGameStatePublisher.cs
using System;
namespace Jrpg.CharacterSystem.GameState
{
    public interface IGameStatePublisher
    {
        void Register(IGameStateSubscriber subscriber);
        void Unregister(IGameStateSubscriber subscriber);
        void PublishStateUpdate(GameStateValue state);
        GameStateValue CurrentState();
    }
}

GameStateManager is the main publisher which implements IGameStatePublisher that will communicate the game states across all subscribers. For now, this is only StatusEffectManager.

GameStateManager.cs
using System;
using System.Collections.Generic;
namespace Jrpg.CharacterSystem.GameState
{
    public class GameStateManager : IGameStatePublisher
    {
        private List<IGameStateSubscriber> subscribers;
        private GameStateValue currentState;

        public GameStateManager(GameStateValue initialState)
        {
            subscribers = new List<IGameStateSubscriber>();
            currentState = initialState;
        }

        public void PublishStateUpdate(GameStateValue state)
        {
            currentState = state;
            foreach(var subscriber in subscribers)
            {
                subscriber.ReceiveStateUpdate(state);
            }
        }

        public void Register(IGameStateSubscriber subscriber)
        {
            if(subscribers.Contains(subscriber))
            {
                return;
            }

            subscribers.Add(subscriber);
        }

        public void Unregister(IGameStateSubscriber subscriber)
        {
            if(!subscribers.Contains(subscriber))
            {
                return;
            }
            subscribers.Remove(subscriber);
        }

        public GameStateValue CurrentState()
        {
            return currentState;
        }
    }
}

Examples of Status Effects!

Okay, so we have all these nice interfaces, manager, base classes, and pub/sub stuff implemented for status effects. Do they work? What does a status effect even look like?

Let's actually work out an example by first implementing the 3 types of status effects defined in StatusEffectType, and then write some unit tests to demonstrate:

Given a party of 3 characters, Cloud, Tifa, and Aerith:

  1. We want to simulate the Poison effect being applied in battle.

    • Tifa becomes poisoned, and after a turn of battle, Tifa's HP should drop from the effects of being poisoned.
    • When the battle ends, and the player is taken back to the World Map, Tifa should still be poisoned, and her HP should continue to drop with each step taken.
  2. We want to simulate the Regen effect being applied in battle onto Aerith.

    • We will observe the effects of it being performed in battle. Aerith's current HP should steadily increase.
    • When out of battle, the Regen effect should not longer exist on Aerith, and subsequent game loop iterations should not have this effect be applied any longer.
  3. We want to simulate the Mini effect being applied in battle onto Cloud.

    • We will observe that Cloud's statistics in battle will be of 1 in all his base stats.
    • When out of battle, the statistics should still look like his original base stats.
    • When going back into battle the statistics should all reflect the value of 1 again.
    • The Mini status is removed in battle, and Cloud's base statistics should return back to the regular base stats he had before being afflicted with Mini.
Poison.cs
using System;
using Jrpg.CharacterSystem.GameState;
namespace Jrpg.CharacterSystem.StatusEffects.Effects
{
    class Poison : StatusEffect
    {
        public override void PerformEffect(Character character, GameStateValue state)
        {
            if(state == GameStateValue.Menu)
            {
                return;
            }

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

        public override StatusEffectType GetStatusEffectType()
        {
            return StatusEffectType.Poison;
        }
    }
}

With our Poison status effect, we want the status effect to reduce the HP by the character by 2 after each iteration of the game loop. I haven't considered 0 HP yet, but that can be implemented down the line.

Regen.cs
using System;
using Jrpg.CharacterSystem.GameState;
namespace Jrpg.CharacterSystem.StatusEffects.Effects
{
     class Regen : StatusEffect
    {
        public override void PerformEffect(Character character, GameStateValue state)
        {
            if (state != GameStateValue.Battle)
            {
                return;
            }

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

        public override bool ShouldDestroy(Character character, GameStateValue state)
        {
            if(state == GameStateValue.Battle)
            {
                return false;
            }

            return true;
        }

        public override StatusEffectType GetStatusEffectType()
        {
            return StatusEffectType.Regen;
        }
    }
}

We can think of Regen as the opposite of Poison. HP is increased by 2 at each iteration of the battle. However, when in battle, the ShouldDestroy method here takes care of inidcating that this status effect should be removed from the character. Regen usually doesn't exist outside of battle when walking around the world map, for example.

Mini.cs
using System;
using Jrpg.CharacterSystem.GameState;
using System.Collections.Generic;

namespace Jrpg.CharacterSystem.StatusEffects.Effects
{
    class Mini : StatusEffect
    {
        private Dictionary<StatisticType, Statistic> currentStats;
        private Dictionary<StatisticType, Statistic> miniStats;

        public override void OnApply(Character character, GameStateValue state)
        {
            currentStats = new Dictionary<StatisticType, Statistic>(character.Statistics);
            miniStats = new Dictionary<StatisticType, Statistic>();
            foreach (var stat in StatisticTypeCollection.DefaultValues.Keys)
            {
                if (stat == StatisticType.HpCurrent || stat == StatisticType.HpMax ||
                    stat == StatisticType.MpCurrent || stat == StatisticType.MpMax ||
                    stat == StatisticType.Level || stat == StatisticType.Experience)
                {
                    miniStats[stat] = character.Statistics[stat];
                    continue;
                }

                Statistic miniStat = new Statistic(stat, 1);
                miniStat.CurrentValue = 1;

                miniStats.Add(stat, miniStat);
            }
        }

        public override void PerformEffect(Character character, GameStateValue state)
        {
            if(state == GameStateValue.Battle)
            {
                character.Statistics = miniStats;
            } else
            {
                character.Statistics = currentStats;
            }
        }

        public override void OnRemove(Character character, GameStateValue state)
        {
            character.Statistics = currentStats;
        }

        public override StatusEffectType GetStatusEffectType()
        {
            return StatusEffectType.Mini;
        }
    }
}

Definitely a more complex status effect to implement here. When Mini is first applied, the OnApply method is invoked to save the current statistics of the character in its instance so that it can be reapplied outside of battle. PerformEffect will do the switching of the statistics based on the current game state. We only want miniStats to be referenced when the game is in the battle state, and otherwise currentStats should be applied.

When Mini is removed, the OnRemove method will set the character statistics back to the original base stats.

Okay cool, now it's time to write some tests to do some of this simulation!

Unit Tests

Before writing the specific tests, a mock object to simulate iterations of a game loop is needed. Here, a class called MockGameLoop serves as a way to create a game loop object which houses a the GameStateManager publisher instance, and the StatusEffectManager subscriber instance.

The constructor will instantiate these objects and will register StatusEffectManager as a subscriber of GameStateManager.

The MockGameLoop::Step function will simulate an iteration of a game loop. For now, it is simple. We just force all of the life cycle methods of the status effects to execute through StatusEffectsManager. This simple implementation is sufficient the tests we will need to write.

MockGameLoop.cs
using System;
using Jrpg.CharacterSystem.GameState;
namespace Jrpg.CharacterSystem.Tests
{
    public class MockGameLoop
    {
        private GameStateManager gameStateManager;
        public StatusEffects.StatusEffectManager StatusEffectManager { get;  }

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

            gameStateManager.Register(StatusEffectManager);
        }

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

            // Changing the state should also invoke a clean-up routine
            StatusEffectManager.CleanUp();
        }

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

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

Test Poison Effect

We create 3 party members here, and immediately set the game state to be in Battle. We force Tifa to be poisoned, and run a single iteration of the game loop. This iteration will simulate some sort of event occuring in the battle.

The expectation here is that after this single battle event, Tifa's HP should be reduced by 2. Finally, when the party is out of battle, and into the World state, Tifa should still be poisoned and should still continue to take damage as the game loop executes.

[Fact]
public void TestPoisonEffect()
{
  InventoryManager inventoryManager = new InventoryManager();
  Party party = new Party(inventoryManager);
  gameLoop = new MockGameLoop();

  party.AddMember("Cloud", new Character("Cloud"));
  party.AddMember("Tifa", new Character("Tifa"));
  party.AddMember("Aerith", new Character("Aerith"));

  var cloud = party.GetMember("Cloud");
  var tifa = party.GetMember("Tifa");
  var aerith = party.GetMember("Aerith");

  gameLoop.SetGameState(GameStateValue.Battle);

  gameLoop.StatusEffectManager.ApplyEffect(tifa, StatusEffectType.Poison);
  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.Step();
  Assert.Equal(28, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.SetGameState(GameStateValue.World);

  gameLoop.Step();
  Assert.Equal(26, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.Step();
  Assert.Equal(24, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);
}

Test Regen Effect

The Regen effect should only be active during battle. Here, we cast Regen on Aerith in battle. During each even that occurs in battle, Aerith should have her HP steadily increase from 20. Ultimately, her HP ends up being at 24 after the battle.

The most important thing here is that after battle, Aerith should no longer have Regen as it is a battle-state only status effect.

Therefore, when back in the World state, it is expected that Aerith's HP should no longer increase.

[Fact]
public void TestRegenEffect()
{
  InventoryManager inventoryManager = new InventoryManager();
  Party party = new Party(inventoryManager);
  gameLoop = new MockGameLoop();

  party.AddMember("Cloud", new Character("Cloud"));
  party.AddMember("Tifa", new Character("Tifa"));
  party.AddMember("Aerith", new Character("Aerith"));

  var cloud = party.GetMember("Cloud");
  var tifa = party.GetMember("Tifa");
  var aerith = party.GetMember("Aerith");

  gameLoop.SetGameState(GameStateValue.Battle);

  aerith.Statistics[StatisticType.HpCurrent].CurrentValue = 20;

  gameLoop.StatusEffectManager.ApplyEffect(aerith, StatusEffectType.Regen);
  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(20, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.Step();

  Assert.Equal(1, gameLoop.StatusEffectManager.StatusEffectTypes(aerith).Count);

  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(22, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.Step();
  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(24, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  gameLoop.SetGameState(GameStateValue.World);
  gameLoop.Step();

  Assert.Equal(0, gameLoop.StatusEffectManager.StatusEffectTypes(aerith).Count);

  gameLoop.Step();
  gameLoop.Step();

  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(24, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);
}

Test Mini Effect

I have defined the Mini status effect here to be an effect where all base statistics of the afflicted character should be reduced by 1 except HP, MP, Level and Experience.

These statistics should only at these values in battle. Outside of battle, the character is still afflicted with Mini, however, their statistics are not reduced in these cases.

Here, we force our party members to level up to 7 so that their base statistics are higher than 1. Then we cast Mini on Cloud and check his stats after being afflicted.

Outside of battle, Cloud should still have Mini, but his statistics are not affected until the party is back into the battle state.

Eventually, Cloud's Mini status is removed and his statistics are returned to normal.

[Fact]
public void TestMiniEffect()
{
  InventoryManager inventoryManager = new InventoryManager();
  Party party = new Party(inventoryManager);
  gameLoop = new MockGameLoop();

  party.AddMember("Cloud", new Character("Cloud"));
  party.AddMember("Tifa", new Character("Tifa"));
  party.AddMember("Aerith", new Character("Aerith"));

  var cloud = party.GetMember("Cloud");
  var tifa = party.GetMember("Tifa");
  var aerith = party.GetMember("Aerith");

  cloud.AddExperience(100);
  tifa.AddExperience(100);
  aerith.AddExperience(100);

  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  Assert.Equal(7, cloud.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, tifa.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, aerith.Statistics[StatisticType.Attack].CurrentValue);

  gameLoop.SetGameState(GameStateValue.Battle);
  gameLoop.StatusEffectManager.ApplyEffect(cloud, StatusEffectType.Mini);
  gameLoop.Step();

  Assert.Equal(30, tifa.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, cloud.Statistics[StatisticType.HpCurrent].CurrentValue);
  Assert.Equal(30, aerith.Statistics[StatisticType.HpCurrent].CurrentValue);

  Assert.Equal(1, cloud.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, tifa.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, aerith.Statistics[StatisticType.Attack].CurrentValue);

  gameLoop.SetGameState(GameStateValue.World);
  gameLoop.Step();
  Assert.Equal(7, cloud.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, tifa.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, aerith.Statistics[StatisticType.Attack].CurrentValue);

  gameLoop.SetGameState(GameStateValue.Battle);
  gameLoop.Step();
  Assert.Equal(1, cloud.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, tifa.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, aerith.Statistics[StatisticType.Attack].CurrentValue);

  gameLoop.Step();
  gameLoop.StatusEffectManager.RemoveEffect(cloud, StatusEffectType.Mini);
  gameLoop.Step();
  Assert.Equal(7, cloud.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, tifa.Statistics[StatisticType.Attack].CurrentValue);
  Assert.Equal(7, aerith.Statistics[StatisticType.Attack].CurrentValue);
}

Conclusion

That was a nice detour, wasn't it? With all that being said, we've fulfilled the physical and mental condition requirement of the basic JRPG character. Next time, we'll dive into talents.