Design Docs - Thinking about Characters - Part 8

I am now noticing that I should probably start titling these blog posts differently.

Thinking

I'm not sure if these posts really qualify as "design docs" anymore. By the time I post these, most code has already been written. Should they still be considered "design docs"? The content also now tends to consist of discussions in justifying design, and the implementation that follows. Maybe it should now be titled Dev Diaries. What do you think?

New Year!

It's 2020! Happy New Year everyone! Time flies by way too fast for me. You know life is great, and it's hard to ask for more when you lose track of the days. I am a very, very lucky person to be able to say that.

I just spent a few days down in Southern California seeing my parents, and it was one of the more pleasant trips I've had. I think everyone can agree that parents can drive one nuts at times. However, I think this time, it wasn't as frustrating as other times in the past.

It was great to see them doing well. My dad is now into his 60s, and the main concern I've had with him is that I have been wanting for him to watch his health more.

For the past year or so, I've been nagging the man to eat better, and lose some weight. My step-mom has been helping out with that, but she can only do so much to fight his stubborness. 😆 Kudos to her for trying repeatedly though... it seems like something worked this time!

My dad has lost 8 pounds over the past six months! Woohoo! He says he has a goal to lose 14 lbs total as that was what his Doctor instructed him to do. I'm proud of him. He has just 6 more pounds to go, and I'm excited to see what he is going to do to maintain his body weight.

One of the reasons why I am so concerned is that I would like my future daughter to be able to grow up, and remember my dad.

I would like my dad to be healthy enough to be able to play with her. It's something that's important to me siince my biological mom passed away relatively young due to cancer.

Resolutions

With that being said, my dad isn't the only one with a few goals. With the new year, come things in which I want to achieve. These are not optional for me, and I will do whatever it takes to achieve them. 🚀

Resolutions

  • I will become better at diagramming, and use more diagrams to better explain my thoughts in my technical writing.
  • I am starting my first semester of my MS in Computer Science degree in mid-January, and I will achieve a 4.0 GPA average for the year.
  • I will support my wife's personal, and career development. She has done so for me for the past 5 years, and I want to do the same for her now.
  • I will make sure that when the day comes, mother, and daughter are both healthy during delivery!
  • I will learn as much as possible about being a great father.
  • I will make a damn game. 🤞
  • I will become a better code reviewer at work by participating in more reviews, and giving deeper insights as opposed to syntax-related comments.
  • I will finish Final Fantasy 6, and start on Final Fantasy 7. 😄
  • I will publish this book.
  • I will stay healthy, and keep my body weight within the range of 155-160 lbs, maintain my blood pressure, and avoid the dreaded "high cholesterol" talk with my Doctor (getting harder every year). Ugh, genetics, I guess.

Enemies!

We have implemented most of the essential features which our characters are allowed to function in a modular manner.

A basic character we have implemented can have:

  • Status effects applied onto them.
  • Techniques which can be used in any state of the game.
  • Unique disciplines which can help determine stat growth, and new techniques which can also be learned as the character progresses in leveling up.

I'm pretty proud of all the effort that has been put to get this far. It's taken us about 2 months now to get this far, and I'm just about to now start showing how we can use what we have developed to quickly demonstrate basic enemy entities.

Imp

Getting Code

As usual, get the code here: https://github.com/urbanspr1nter/jrpg-system, or if you want to navigate to the specific commit, feel free to head on over to here: https://github.com/urbanspr1nter/jrpg-system/commit/7598162fd2e25ae3bb716d2a46e54ed437dadd98

Jrpg.BattleSystem

Let's start a new project called Jrpg.BattleSystem. We will try implement some of this code in this new project as we are now starting to deviate away with abstract character management, and into more concrete stuff using the character system.

Designing Enemies

Enemies in a JRPG typically appear in battle. In my opinion, JRPGs are unique in that the state of the world, town, and battle are all exremely different. The Enemy object itself wouldn't typically appear outside of battle, but it would possess some of the basic attributes a regular Character has:

  • A name
  • A set of statistics
  • Techniques

Evidently, all these attributes can be found within the BaseCharacterClass object. This means we may be able to create an EnemyClass type which extends from BaseCharacterClass to add additional properties in which we can represent an enemy as.

Well, now, let's think about what the enemy can do, and if it is of an EnemyClass, what makes it special from all other disciplines? Well, from my personal experience in playing a lot of JRPGs, I can think that this special EnemyClass discipline having:

  • Items which can be dropped after being defeated
  • Gold which can be acquired after being defeated
  • Experience which the player can earn after being defeated
  • Elemental type associated with it to give advantages/disadvantages in battle.

These attributes all directly relate to characters being in battle. Thinking about it, in JRPGs, enemies are equivalent to NPCs when dealing with them outside of battle. Really, they are just NPCs with the context of the player knowing that they are an enemy!

I think a good example of this is Kefka from Final Fantasy 6. In the game, you see that Kefka was just a sprite like everyone else when not in battle. He can only do basic things the other characters do. The most obvious being.. speaking.

Kefka NPC

In game, Kefka was just a regular ol' NPC

Things do change once the player is placed into battle. Kefka not only looks radically different, but he also has many other abilities now -- such as being able to attack, and hold items just like the members of the party the player controls.

Kefka Battle

But he becomes different in battle...

It is then probably sensible to just really make an enemy an extension of Character, and have it contain the special EnemyClass discipline.

Architecture

The Enemy

An Enemy can then subclass Character with a few modifications:

  1. The enemy can not be initialized with any other type of class other than an EnemyClass instance. This means that our constructor should check that the BaseCharacterClass being passed in is of EnemyClass as the base type.
  2. The enemy can not be leveled up, and thus experience cannot be gained! We will need to override the AddExperience method to throw an exception if invocation is attempted.
  3. The enemy can not have its class be changed. We will have to override ChangeClass to throw an exception indicating that the class cannot be changed.
Enemy.cs
using System;
using Jrpg.InventorySystem.PgItems;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes;

namespace Jrpg.BattleSystem.Enemies
{
    public class Enemy : Character
    {
        public Enemy(string name)
            : base(name)
        {
            throw new NotSupportedException(
              "Constructor without the EnemyClass provided is not supported."
            );
        }

        public Enemy(string name, BaseCharacterClass defaultDiscipline)
            : base(name, defaultDiscipline)
        {
            if(defaultDiscipline.GetType().BaseType
                != Type.GetType("Jrpg.BattleSystem.Enemies.EnemyClass"))
            {
                throw new
                    NotSupportedException("Expected discipline type to be of EnemyClass.");
            }
        }

        public override bool AddExperience(int experience)
        {
            throw new
                NotImplementedException("Not implemented for Enemy");
        }

        public override void ChangeClass(BaseCharacterClass jobClass)
        {
            throw new
                NotImplementedException("Cannot change the class assigned to the Enemy.");
        }
    }
}

The above class is super basic right now, and note that going forward, we're going to add more features to it. Therefore, this isn't the final state of the class.

The EnemyClass

The EnemyClass extends from BaseCharacterClass, and is the default discipline an Enemy can contain. As mentioned earlier, our EnemyClass can supply additional properties relating to battle:

  • A collection of items which can be dropped. These are repreated by a List<ItemClassEdge> type found within Jrpg.InventorySystem. This also implicitly makes the enemy containing EnemyClass a DropSource.
  • The amount of gold to reward the player after being defeated in battle.
  • The amount of experience points given to the player after being defeated in battle.
  • Elemental type associated with the enemy. We won't go into this now, but it will be useful when we begin implementing elemental awareness in battle.

Red Dragon Poison Dragon Holy Dragon

What do these dragons all have in common? Well, they're all... Dragons.

We can then create a special definition to contain this type of data. Of course, it has to be called EnemyDefinition.

EnemyDefinition.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem.Classes.Definitions;
using Jrpg.InventorySystem.PgItems;

namespace Jrpg.BattleSystem.Enemies.Definitions
{
    public class EnemyDefinition : ClassDefinition
    {
        public string Id { get; set; }
        public string Elemental { get; set; }
        public List<ItemClassEdge> ItemClass { get; set; }
        public int Gold { get; set; }
        public int Experience { get; set; }
    }
}

Any special EnemyClass such as Goblin, Dragon, or Undead can be represented as a subclass of EnemyClass. The EnemyClass can now be described by JSON objects which will be deserialized into the EnemyDefinition object.

Now, here is the implemented EnemyClass which extends BaseCharacterClass.

EnemyClass.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes.Definitions;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.CharacterSystem.Classes;
using Jrpg.InventorySystem.PgItems;

namespace Jrpg.BattleSystem.Enemies
{
    public abstract class EnemyClass : BaseCharacterClass
    {
        public int Gold { get; private set; }
        public int Experience { get; private set; }
        public List<ItemClassEdge> ItemClasses { get; private set; }

        public EnemyClass(
            Dictionary<StatisticType, Statistic> statistics,
            List<TechniqueDefinition> techniqueDefinitions,
            List<ClassTechniqueDefinition> techniqueDefinitionMapping,
            List<ItemClassEdge> itemClasses,
            int gold,
            int experience
        )
            : base(statistics, techniqueDefinitions, techniqueDefinitionMapping)
        {
            Gold = gold;
            Experience = experience;
            ItemClasses = itemClasses;
        }


        public override abstract string ClassName();
    }
}

Notice it doesn't do too much. It is just a container for some of the additional attributes needed to express an enemy. Not only that, this class is also meant to be extended further to create concrete enemy classes. This will help in creating enemies which can have different behaviors relating to the statistics, or items they have.

An example extension would be a BasicGoblin.

GoblinClass.cs
using System;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes.Definitions;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.InventorySystem.PgItems;
using Jrpg.BattleSystem.Enemies;

namespace Jrpg.SampleGame.Characters.Enemies
{
    public class GoblinClass : EnemyClass
    {
        public GoblinClass(
            Dictionary<StatisticType, Statistic> statistics,
            List<TechniqueDefinition> techniqueDefinitions,
            List<ClassTechniqueDefinition> techniqueDefinitionMapping,
            List<ItemClassEdge> itemClasses,
            int gold,
            int experience
        ) : base(statistics, techniqueDefinitions, techniqueDefinitionMapping, itemClasses, gold, experience)
        {

        }

        public override string ClassName()
        {
            return "Basic Goblin";
        }
    }
}

Then we can make many different types of Goblins which could possibly behave differently, but all derive from this very basic class.

The EnemyManager

Enemies can then be instantiated in two steps.

  1. Instantiate the EnemyClass which the Enemy will be associated as.
  2. Create the instance of the Enemy with a provided name, and the object of EnemyClass type.

To make this process easier, let's create a new EnemyManager class to create a new Enemy given its name and class name.

The Enemymanager itself looks like just about any other "manager" class found within jrpg-system.

  1. It serves as a basic registry of EnemyDefinition objects to be used to dynamically initialize new Enemy objects.
  2. It includes methods to handle registration of EnemyDefinition.
  3. It also will have methods to create EnemyClass objects individually.
EnemyManager.cs
using System;
using System.IO;
using System.Collections.Generic;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Techniques;
using Jrpg.BattleSystem.Enemies.Definitions;
using Jrpg.InventorySystem.PgItems;

namespace Jrpg.BattleSystem.Enemies
{
    public class EnemyManager
    {
        private Dictionary<string, EnemyDefinition> enemies;
        private Dictionary<string, TechniqueDefinition> techniqueDefinitions;
        private ItemGenerator itemGenerator;

        public EnemyManager
            (Dictionary<string, TechniqueDefinition> techDefs, ItemGenerator itemGenerator)
        {
            enemies = new Dictionary<string, EnemyDefinition>();
            techniqueDefinitions = techDefs;
            this.itemGenerator = itemGenerator;
        }

        public void Register(string tag, EnemyDefinition enemyDefinition)
        {
            if(enemies.ContainsKey(tag))
            {
                return;
            }

            enemies.Add(tag, enemyDefinition);
        }

        public Enemy GetEnemyInstance(string name, string className)
        {
            var enemyClass = GetEnemyClass(className);

            return new Enemy(name, enemyClass, itemGenerator);
        }

        public EnemyClass GetEnemyClass(string name)
        {
            if(!enemies.ContainsKey(name))
            {
                throw new KeyNotFoundException("No registered enemy found.");
            }

            var enemyDefinition = enemies[name];
            var agent = enemyDefinition.Agent;
            var startingStatistics = new Dictionary<StatisticType, Statistic>();

            foreach(var enemyStatistic in enemyDefinition.StartingStatistics)
            {
                var type = CommonUtils.ToStatisticType(enemyStatistic.Name);
                var statistic = CommonUtils.ToStatistic(enemyStatistic);

                startingStatistics.Add(type, statistic);
            }

            var techniqueDefinitionMapping = enemyDefinition.Techniques;
            var techniqueDefinitionsForEnemy = new List<TechniqueDefinition>();

            foreach(var classTechniqueDefinition in techniqueDefinitionMapping)
            {
                techniqueDefinitionsForEnemy.
                    Add(techniqueDefinitions[classTechniqueDefinition.Name]);
            }

            var enemyClass = (EnemyClass)(Activator.CreateInstance(Type.GetType(agent),
                new object[]
            {
                startingStatistics,
                techniqueDefinitionsForEnemy,
                techniqueDefinitionMapping,
                enemyDefinition.ItemClass,
                enemyDefinition.Gold,
                enemyDefinition.Experience
            }));

            return enemyClass;
        }

        public void FromJsonDefinition(string filename)
        {
            List<EnemyDefinition> loaded
                = Newtonsoft.Json.JsonConvert.DeserializeObject<List<EnemyDefinition>>
                    (File.ReadAllText(filename));

            foreach(var enemy in loaded)
            {
                Register(enemy.Name, enemy);
            }
        }
    }
}

Since an EnemyClass contains more attributes than a regular BaseCharacterClass, initialization is much more verbose, and hence this type of factory class can be very useful in practice!

Items as a Reward

Let's actually start making our Enemy smarter and add some more features to it. The most interesting one I'd like to touch on here relate to item drops.

Since enemies can drop items, we will need to provide an ItemGenerator and DropSource to procedurally generate items defined by the EnemyClass.

Achieving this requires some modifications to the EnemyManager and Enemy classes.

Of course, since we now need anItemGenerator, the Enemy class should be modified to include a new constructor which takes in ItemGenerator as a parameter. This means that the previous constructor signature of public Enemy(string name, BaseCharacterClass defaultDiscipline) is no longer sufficient to instantiate an Enemy. Let's have this constructor throw an exception instead.

Enemy.cs
        public Enemy(string name, BaseCharacterClass defaultDiscipline)
            : base(name, defaultDiscipline)
        {
            throw new NotSupportedException(
                "Constructor without the ItemGenerator provided is not supported."
            );
        }

Now, let's write the new constructor.

Since the enemy now has items to generate, the enemy now must hold an instance of DropSource since it is now somewhat like a treasure chest!

The Name and Level of this DropSource can be the same as the Enemy itself for now.

        public DropSource dropSource;
        public ItemGenerator itemGenerator;

				// ...

        public Enemy(
            string name,
            BaseCharacterClass defaultDiscipline,
            ItemGenerator itemGenerator
            ) : base(name, defaultDiscipline)
        {
            if(defaultDiscipline.GetType().BaseType
                != Type.GetType("Jrpg.BattleSystem.Enemies.EnemyClass"))
            {
                throw new NotSupportedException(
                    "Expected discipline type to be of EnemyClass."
                );
            }

            dropSource = new DropSource()
            {
                Name = Name,
                Level = Statistics[StatisticType.Level].CurrentValue,
                ItemClass = ((EnemyClass)(defaultDiscipline)).ItemClasses
            };

            this.itemGenerator = itemGenerator;
        }

It is similar to our previous implementation, except now we now have an ItemGenerator handy to generate items found in the EnemyClass.

While we are thinking about enemies in the context of battle, let's take this time to add in additional helper methods which could be useful down the line in obtaining gold, and experience from the enemy. The methods IsAlive, Gold, and Experience will return information relating to rewards to the player.

        public bool IsAlive()
        {
            return Statistics[StatisticType.HpCurrent].CurrentValue > 0;
        }

        public int Gold()
        {
            return ((EnemyClass)currentClass).Gold;
        }

        public int Experience()
        {
            return ((EnemyClass)currentClass).Experience;
        }

Now, we can add the method GetItem to generate an item procedurally from the list of available items the enemy can drop.

        public Item GetItem()
        {
            return itemGenerator.GenerateItem(dropSource);
        }

Now, here is what the updated Enemy class looks like.

Enemy.cs
using System;
using Jrpg.InventorySystem.PgItems;
using Jrpg.CharacterSystem;
using Jrpg.CharacterSystem.Classes;

namespace Jrpg.BattleSystem.Enemies
{
    public class Enemy : Character
    {
        public DropSource dropSource;
        public ItemGenerator itemGenerator;

        public Enemy(string name) : base(name)
        {
            throw new NotSupportedException(
                "Constructor without the EnemyClass provided is not supported."
            );
        }

        public Enemy(string name, BaseCharacterClass defaultDiscipline)
            : base(name, defaultDiscipline)
        {
            throw new NotSupportedException(
                "Constructor without the ItemGenerator provided is not supported."
            );
        }

        public Enemy(
            string name,
            BaseCharacterClass defaultDiscipline,
            ItemGenerator itemGenerator
            ) : base(name, defaultDiscipline)
        {
            if(defaultDiscipline.GetType().BaseType
                != Type.GetType("Jrpg.BattleSystem.Enemies.EnemyClass"))
            {
                throw new NotSupportedException(
                    "Expected discipline type to be of EnemyClass."
                );
            }

            dropSource = new DropSource()
            {
                Name = Name,
                Level = Statistics[StatisticType.Level].CurrentValue,
                ItemClass = ((EnemyClass)(defaultDiscipline)).ItemClasses
            };

            this.itemGenerator = itemGenerator;
        }

        public override bool AddExperience(int experience)
        {
            throw new
                NotImplementedException("Not implemented for Enemy");
        }

        public override void ChangeClass(BaseCharacterClass jobClass)
        {
            throw new
                NotImplementedException("Cannot change the class assigned to the Enemy.");
        }

        public bool IsAlive()
        {
            return Statistics[StatisticType.HpCurrent].CurrentValue > 0;
        }

        public int Gold()
        {
            return ((EnemyClass)currentClass).Gold;
        }

        public int Experience()
        {
            return ((EnemyClass)currentClass).Experience;
        }

        public Item GetItem()
        {
            return itemGenerator.GenerateItem(dropSource);
        }
    }
}

EnemyManager now needs the additional ItemGenerator parameter to be used for passing into the Enemy constructor to initialize the instances.

        public EnemyManager(
            Dictionary<string, TechniqueDefinition> techDefs,
            ItemGenerator itemGenerator
        )
        {
            enemies = new Dictionary<string, EnemyDefinition>();
            techniqueDefinitions = techDefs;
            this.itemGenerator = itemGenerator;
        }

The GetEnemyInstance just needs this parameter passed down to create the Enemy.

        public Enemy GetEnemyInstance(string name, string className)
        {
            var enemyClass = GetEnemyClass(className);

            return new Enemy(name, enemyClass, itemGenerator);
        }

Tests!

We are now ready to write some test code! First, let's create a Basic Goblin EnemyClass with the following properties:

[
  {
    "Id": "Basic Goblin",
    "Name": "Basic Goblin",
    "Agent": "Jrpg.SampleGame.Characters.Enemies.GoblinClass, Jrpg.SampleGame",
    "Elemental": "Normal",
    "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
      }
    ],
    "Techniques": [],
    "ItemClass": [
      {
        "Name": "Tonic",
        "Weight": 10
      },
      {
        "Name": "Antidote",
        "Weight":  5
      }
    ],
    "Gold": 25,
    "Experience": 10
  }
]

Then, we can use this Basic Goblin class to generate new Goblin enemies on the fly.

My personal interest is to test the following:

  1. See if we can generate an Enemy instance from an EnemyClass definition.
  2. Does the Enemy give the expected amount of gold, and experience?
  3. Is the enemy aware it is "alive"?
  4. Can the enemy drop some of the items it current holds?
  5. Can a basic battle be simulated with our current implementation?

TestEnemyBattle will be our test class to test these 5 main cases. Additional changes to MockGameLoop is also needed to instantiate EnemyManager properly along with all necessary data components. Sigh, I should really refactor MockGameLoop.... soon!

To keep code somewhat clean, I am going to try to initialize EnemyManager separately from the rest of the other objets in MockGameLoop.

        private void InitializeEnemeyManager(
        	Dictionary<string, TechniqueDefinition> techDefs
        )
        {
            var tonicItem = new Item()
            {
                Name = "Tonic",
                ItemClass = new List<ItemClassEdge>(),
                Properties = new List<Property>() {
                        new Property() {
                            Name= "HP Current",
                            Value = 10
                        }
                    },
                Value = 200,
                BodyPart = "Default"
            };

            var antidoteItem = new Item()
            {
                Name = "Antidote",
                ItemClass = new List<ItemClassEdge>(),
                Properties = new List<Property>() {
                        new Property() {
                            Name= "HP Current",
                            Value = 10
                        }
                    },
                Value = 200,
                BodyPart = "Default"
            };

            var mockItemDefs = new List<Item>() { tonicItem, antidoteItem };


            var noPrefix = new Affix()
            {
                Name = "NoPrefix",
                ParentItemClass = new List<string>(),
                Properties = new List<Property>(),
                Weight = 15,
                Value = new ValueObject()
            };


            var mockPrefixDefs = new List<Affix>() { noPrefix };

            var noSuffix = new Affix()
            {
                Name = "NoSuffix",
                ParentItemClass = new List<string>(),
                Properties = new List<Property>(),
                Weight = 15,
                Value = new ValueObject()
            };


            var mockSuffixDefs = new List<Affix>() { noSuffix };

            EnemyManager
                = new EnemyManager(
                    techDefs,
                    new ItemGenerator(mockItemDefs, mockPrefixDefs, mockSuffixDefs)
                );
            EnemyManager.FromJsonDefinition("Resources/Enemies.json");
        }

You will see here that I've mocked the items and affixes instead of reading them directly from files.

The first four (4) tests are very easy to write. They basically consist of creating the Enemy instance, and asserting its properties to see if correct data exists.

TestEnemyBattle.cs
using System;
using Jrpg.CharacterSystem;
using Jrpg.InventorySystem;
using Jrpg.PartySystem;

using Xunit;

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

        public TestEnemyBattle()
        {
            Console.WriteLine("---- Enemy Tests ----");
        }

        [Fact]
        public void TestGenerateEnemy()
        {
            gameLoop = new MockGameLoop();

            var enemy = gameLoop.EnemyManager.GetEnemyInstance("Goblin", "Basic Goblin");

            Assert.NotNull(enemy);
            Assert.Equal("Goblin", enemy.Name);
            Assert.Equal("Basic Goblin", enemy.CurrentClassName());
        }

        [Fact]
        public void TestGetGoldAndExperienceFromEnemy()
        {
            gameLoop = new MockGameLoop();

            var enemy = gameLoop.EnemyManager.GetEnemyInstance("Goblin", "Basic Goblin");

            Assert.Equal(25, enemy.Gold());
            Assert.Equal(10, enemy.Experience());
        }

        [Fact]
        public void TestEnemyAlive()
        {
            gameLoop = new MockGameLoop();

            var enemy = gameLoop.EnemyManager.GetEnemyInstance("Goblin", "Basic Goblin");

            Assert.True(enemy.Statistics[StatisticType.HpCurrent].CurrentValue > 0);
            Assert.True(enemy.IsAlive());

            enemy.Statistics[StatisticType.HpCurrent].CurrentValue = 0;
            Assert.False(enemy.IsAlive());
        }

        [Fact]
        public void TestEnemyDropItem()
        {
            gameLoop = new MockGameLoop();

            var enemy = gameLoop.EnemyManager.GetEnemyInstance("Goblin", "Basic Goblin");
            var item = enemy.GetItem();

            Assert.NotNull(item);
            Assert.True(item.Name.Equals("Tonic") || item.Name.Equals("Antidote"));
        }

        [Fact]
        public void TestBattle()
        {
            gameLoop = new MockGameLoop();

            InventoryManager inventoryManager = new InventoryManager();
            Party party = new Party(inventoryManager);

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

            var cloud = party.GetActiveCharacter();
            var enemy = gameLoop.EnemyManager.GetEnemyInstance("Goblin", "Basic Goblin");

            gameLoop.SetGameState(GameState.GameStateValue.Battle);

            int i = 0;
            while(enemy.IsAlive())
            {
                enemy.Statistics[StatisticType.HpCurrent].CurrentValue -= GetDamageToDeal(cloud);
                gameLoop.Step();
                i++;

                if (i > enemy.Statistics[StatisticType.HpMax].CurrentValue)
                    Assert.True(false);
            }

            Assert.False(enemy.IsAlive());

        }

        private int GetDamageToDeal(Character active)
        {
            return active.Statistics[StatisticType.Attack].CurrentValue * 2;
        }
    }
}

Conclusion

Okay! Well, notice that we didn't have to add that many new components to our system to be able to support enemies! 😄

Enemy support was achieved by extending our existing character components, and implementing them in a new project, Jrpg.BattleSystem.

I will hold off in building a battle system for now as we are still on character system design. We have one more small piece left to do. THis is just the task in adding NPC support to the system. That is what we will be looking into togeter in Part 9 of the series.

I will see you all very soon.

Kefka, again