Design Docs - Thinking About Inventory - Part 4

So far I've discussed about how we can design a JRPG "Rule of 99" inventory system which has been able to support the features of using, equipping, and unequipping items in Part 1, Part 2, and Part 3. An outstanding thought has been how can we design our system such that items can be acquired and dropped through various actions.

For example, acquiring an item can be through either loot from battle, a gift, or being bought at a shop in town. In the opposite manner, dropping an item can be from throwing away an item, or from selling it at a shop in town. In this chapter of our series, I'll attempt to introduce components into our inventory system which attempt to solve how an item is received and released.

Final Fantasy 7 Treasure

Finding treasure in Final Fantasy 7

 

Final Fantasy X-2 Shop

A shop in Final Fantasy X-2

Get the Code

For the lazy, if you just want to see code immediately, here is a link to all the code files in the GitHub respository: jrpg-inventory-system.

https://github.com/urbanspr1nter/jrpg-inventory-system

Note, there are some changes in the way I've organized code. The repository is now organized into releases which correspond to the state of the code for a particular chapter.

In this case, navigating to https://github.com/urbanspr1nter/jrpg-inventory-system/releases, you'll see the code for this post as Source files for Chapter 4. If you're interested in following code from Part 3, then Source files for Chapter 3 will reflect the state of the code that was discussed there.

Use Cases in Receiving and Releasing Items

How are items received and released? Well, I can think of a few for receiving in a JRPG 😎:

  • Enemy drops after a battle. This is synonymous with "loot".
  • Stealing from an enemy during battle.
  • Treasure
  • Purchase from a shop in town.
  • Gift from an NPC (could be a special item too, but we won't discuss this for now) *

 

Releasing items also has a few cases to consider:

  • Intentional discard (organizing inventory)
  • Usage of a special item (won't discuss this for now) *
  • Selling the item to a shop owner in town

 

Our focus for now is on all of the above except for the cases where an item is "special" in that usage of it is required to progress through the game by triggering some state. We'll save this for a later discussion. For now, let's keep the scope in managing items in the context of inventory management.

JRPG Party

Up until now, we've focused on applying items individually through characters. In a JRPG, characters are organized into a party. Typically the number of characters range from 3-4 for a full party of characters in the JRPG. (Of course, there can be less at any given point in time within the story.)

An approach to think about a JRPG party is that it is the top-most level in how supplies and wealth are stored and managed. All party members share the same inventory, and "wallet". It makes sense that we should now begin gto think about encapsulating our characters into a Party.

Heck, even Pokemon had the concept of a party:

 

Pokemon Yellow Party Members

Pokemon Yellow party members.

 

What should our Party class first consist of?

  • We'll need members, which is a list of characters, or maybe even a dictionary which contains the name of the character as the key, and the Character object itself:
Dictionary<string, Character> members;
  • We'll also need a shared inventory. The purpose is to provide the player access to manage inventory through the Party instead.
InventoryManager inventoryManager;

As a consequence to the change above, we might not need Character to contain references to the inventory manager anymore, and instead leave itself to manage it's own Character-type operations and behavior. We'll definitely need to refactor UseItem, EquipItem, and UnequipItem away from Character and move it up to Party now.

  • Finally, every party needs some money! A simple wallet will do:
int wallet;

At this point, we have a basic class definition, in Roger's Pseudo-Pseudo Code ™, of course.

class Party {
	private Dictionary<string, Character> members;
	private InventoryManager inventoryManager;
	private int wallet;
}

Managing Active Characters

If we're working with the Party when it comes to inventory operations now, how do we know who the source character and target characters are when it comes to using and equipping items?

Our "target" character is always the character in which the item is being applied on. This one is fairly trivial to notice. However, the "source" character is not as obvious. The source character can be chosen in the following scenarios:

  • In battle, the source character is the active character (current character in turn), and the target character is explicitly chosen. We can then use our items through a specific menu flow of:

    1. Setting the source character as the character which is active.
    1. Choosing the item.
    1. Setting the target character as the character for the item to be applied on after choosing the item.
  • In the menu screen such as using an item, or equipping an item, we must navigate or choose a character explicitly. Here, we can assume that the source and target characters are by default, the same!

Perhaps then in Party, we can maintain the active character and once an action on inventory is invoked, we can use that active character as the source character.

I envision the flow as something like the following:

> SetActiveCharacter(Terra);
>> Equip Helmet onto Head
> ClearActiveCharacter();
> Navigate away from menu

We can then be flexible with inventory actions whether in battle, or menu, or in a shop.

Updating our API of Party, we'll now have:

class Party {
	private Dictionary<string, Character> members;
	private InventoryManager inventoryManager;
	private int wallet;
	private Character activeCharacter;
	
	public void SetActiveCharacter(Character targetCharacter) { ... }
	public void ClearActiveCharacter() { ... }
}

Refactoring Inventory Management from Character into Party

We no longer want to have Character be directly managing the inventory now that an instance of InventoryManager exists in Party. Let's start moving up the existing methods in Character: UseItem, EquipItem, and UnequipItem into Party and tweak it to be simpler to use.

Character::UseItem to Party::UseItem

Moving UseItem into Party, we now realize that the sourceChar parameter is no longer needed! We can just rely on the activeCharacter set in Party to determine that! Therefore, we can alter the signature of the method to this:

public UseItem(ItemName name, Character targetCharacter) { ... }

Character:: EquipItem to Party::EquipItem

We can also use the same activeCharacter as the sourceCharacter for EquipItem. We just need to make sure that the UI has follows a specific pattern to set and clear the active character in context.

public EquipItem(ItemName name, BodyPart bodyPart) { ... }

Character::UnequipItem to Party::UnequipItem

Same as EquipItem, we'll need to just assume that the source character is the activeCharacter.

public UnequipItem(BodyPart bodyPart) { ... }

 

Now that we have moved some methods around, and if you've been following, the Party class is starting to become rather stateful! We can definitely explore how we can make this better down the line, but for now so far, everything seems to be working quite well for us.

Receiving and Releasing Items

Now that we have done some refactoring, at this point, we are ready to come up with some sort of logic in how our party can receive, and release items.

In my opinion, the easest area to begin building this logic is within InventoryManager. We can start by implementing the Acquire and Drop methods found within this class in which we had previously stubbed.

Whenever we gain, or lose an item, the calls can simply be treated as updating the ItemData entries within the registry dictionary internal to the inventory manager.

To receive an item, a call to InventoryManager::Acquire is needed, and this is probably the more difficult piece of logic to implement. But it isn't too complicated. The current stub method takes in an ItemName, as the only parameter. This is okay, and we should try to make it work. The intention is to avoid clients having to perform inventory operations using IItem types directly.

Given an ItemName, the InventoryManager should be able to create an ItemData object into the registry. However, we have a bit of a problem. Notice that ItemData holds an IItem reference. How would we obtain that under our current system?

Funnily enough, what I thought was unneeded is actually needed, but with a slightly different twist.

We need a factory-type class to create our items! For now, let's keep it as simple as possible and implement a class that will just create our items in a hard-coded manner by the use of a switch statement. The name of this factory class shall be... ItemFactory

class ItemFactory {
	public IItem CreateItem(ItemName) { ... }
}

In pseudo-code flow, the InventoryManager::Acquire method will:

  1. Check to see if the ItemName exists in the registry.
  2. If not, use the ItemFactory instance to generate an item instance, and use that for the ItemData object to be registered against the inventory registry. By default, the quantity is 0.
  3. We can always then increment the Quantity in all cases.

To release an item, we can just decrement the Quantity value within the registry given an item name.

Party Utils: QueryFor, GetWalletAmount, Add/Remove Members

  • One method which turned out to be handy during unit testing was QueryFor. Let's actually put that in our Party class because you never know when we're going to need it. 😉 (Hint: When we purchase, and sell our items, we'll need to obtain the information relating to the value of the item!)
public ItemInfo QueryFor(ItemName name) { ... }
  • Adding and removing members is a must for our JRPG party. This is something we would like to do if we're planning a game which will have characters entering and exiting in various acts of the story. AddMember and RemoveMember will handle "CRUD-like" operations on the members dictionary.
public void AddMember(string name, Character member) { ... }
public void RemoveMember(string name) { ... }
  • Finally, we'll want to be able to get the current wallet amount of the Party.
public int GetWalletAmount() { ... }

Party::ReceiveItem and Party::ReleaseItem

Alright it now seems like we hve enough to work with for InventoryManager. Now, it's time to build out the logic for our JRPG party to pick up, or drop an item!

Seems like we also need to have some methods available for our party to ReceiveItem and ReleaseItem. 😄 Pun.

Party::ReceiveItem for now, will serve as a wrapper around InventoryManager::Acquire. Similarly for Party::ReleaseItem, we can wrap around InventoryManager::Drop.

public void ReceiveItem(ItemName name) { ... }
public void ReleaseItem(ItemName name) { ... }

At this point, we have updated InventoryManager to implement Acquire, and Drop. We have also created the two methods in Party to call these inventory management methods.

Transactions

It looks like we can conveniently have the party receive and release Items now. That still doesn't really support what we want though. Our inventory system still needs to be able to perform transactions. In order to do so, our items must have some sort of value attached to them.

To support having transactions in the actions of purchasing and selling items, we need to not only reflect changes to the inventory, but also the wallet.

We are now definitely at a point where it may make sense to define a BaseItem type that implements IItem. The BaseItem type will hold a Value property, and declare Apply and CanApply as abstract, so that any clients extending BaseItem are guaranteed to have some implementation.

UndoApply isn't always needed, so we can provide a virtual default implementation for now.

abstract class BaseItem {
	public int Value { get; set; }
	public abstract bool Apply(Character targetCharacter, BodyPart bodyPart);
	public abstract bool CanApply(Character targetCharacter BodyPart bodyPart);
	public virtual bool UndoApply(Character targetCharacter) { ... }
}

We'll modify our existing items now to inherit BaseItem with some hard-coded values. For example, Potion will now look like:

    public class Potion : BaseItem
    {
        public Potion()
        {
            Value = 3;
        }

        public override bool Apply(Character targetChar, BodyPart targetBodyPart)
        {
            targetChar.Statistics.HP += 10;

            return true;
        }

        public override bool CanApply(Character targetChar)
        {
            if(targetChar.Statistics.HP >= 100)
            {
                return false;
            }

            return true;
        }
    }

Additionally, in order to obtain the Value properly from queries made through QueryFor, the ItemInfo definition must be updated to include this new property.

Since there are multiple ways to receive an item (through treasure, loot, purchase, etc) and release an item (drop, sell) through the party, let's keep things simple. We will try to maintain a single method signature for the actions.

For receiving and releasing items, we can define an enum and include a new parameter within ReceiveItem and ReleaseItem to determine what type of receipt and release action was performed.

enum ItemReceiveAction {
	Treasure,
	Loot,
	Purchase,
	Reward
}

enum ItemReleaseAction {
	Drop,
	Sell
}

We can then read in those types and perform logic suitable for handling the change in inventory. With all these additions to Party, here's a quick summary of what we have done so far:

class Party {
	private Dictionary<string, Character> members;
	private InventoryManager inventoryManager;
	private int wallet;
	private Character activeCharacter;
	
	public void SetActiveCharacter(Character targetCharacter) { ... }
	public void ClearActiveCharacter() { ... }
	public UseItem(ItemName name, Character targetCharacter) { ... 
	public EquipItem(ItemName name, BodyPart bodyPart) { ... }
	public UnequipItem(BodyPart bodyPart) { ... }
	public List<ItemInfo> QueryAllItems() { ... }
	public ItemInfo QueryFor(ItemName name) { ... }
	public void AddMember(string name, Character member) { ... }
	public void RemoveMember(string name) { ... }
	public int GetWalletAmount() { ... }
	
	public void ReceiveItem(ItemName name, ItemReceiveAction action) { ... }
	public void ReleaseItem(ItemName name, ItemReleaseAction action) { ... }
}

Wow, we've done quite a bit! I think it's time to write some code and tests to validate these ideas! First, here is a big picture view of what we have done so far!

Big Picture

UML diagram so far

UML Diagram Full View

Implementation and Test

Before I walk through the entire implementation in what we have covered so far, if you want to see the code, just go here: https://github.com/urbanspr1nter/jrpg-inventory-system.

Our unit tests will test the following scenario:

  1. Test acquiring an item through loot/treasure/reward. These are fundamentally equivalent.
  2. Test acquiring an item through purchasing. The wallet of the party should be updated to reflect the loss. The item should also exist in the inventory.
  3. Test the scenario of dropping an item. The inventory should be updated to exclude the item, or have less quantity.
  4. The scenario of selling an item should also be tested. The party's wallet should be updated to contain the earnings, while the inventory should also be updated.

 

In addition to the above, the previous unit tests have also broke! So, we will now need to modify them in the context of Party, and not Character.

Rubs hand for fun

Let's have some fun now!

Item Code Modifications

FIrst, let's create a new BaseItem class, and have this be the parent class of all items going forward.

BaseItem.cs
namespace InventorySystem.Items
{
    public abstract class BaseItem : IItem
    {
        public int Value { get; set; }

        public abstract bool Apply(Character targetChar, BodyPart targetBodyPart);

        public abstract bool CanApply(Character targetChar);

        public virtual bool UndoApply(Character targetChar)
        {
            return false;
        }
    }
}

Now, let's update our 3 exisiting items to extend BaseItem. I won't show all those changes here, but let's use Potion as an example:

Potion.cs
namespace InventorySystem.Items
{
    public class Potion : BaseItem
    {
        public Potion()
        {
            Value = 3;
        }

        public override bool Apply(Character targetChar, BodyPart targetBodyPart)
        {
            targetChar.Statistics.HP += 10;

            return true;
        }

        public override bool CanApply(Character targetChar)
        {
            if(targetChar.Statistics.HP >= 100)
            {
                return false;
            }

            return true;
        }
    }
}

We'll also need to create an ItemFactory class to generate and return BaseItem instances.

ItemFactory.cs
using InventorySystem.Items;
namespace InventorySystem
{
    public class ItemFactory
    {
        public BaseItem CreateItem(ItemName name)
        {
            switch(name)
            {
                case ItemName.Potion:
                    return new Potion();
                case ItemName.LeatherHelmet:
                    return new LeatherHelmet();
                case ItemName.IronHelmet:
                    return new IronHelmet();
                default: return null;
            }
        }
    }
}

Simple enough for now. 😅 I think we're doing rather well so far!

We will also need to update ItemInfo to include the new Value property so that results being returned from queries will contain the worth of the item.

ItemInfo.cs
namespace InventorySystem
{
    public class ItemInfo
    {
        public ItemName Name { get; set; }
        public int Quantity { get; set; }
        public string Description { get; set; }
        public int Value { get; set; }
    }
}

ItemData will be edited to return a BaseItem type instead of IItem for the Item property.

ItemData.cs
using InventorySystem.Items;
namespace InventorySystem
{
    public class ItemData
    {
        public int Quantity { get; set; }
        public BaseItem Item { get; set; }
    }
}

Party

First, let's implement our handy enums indicating the type of receive and release actions:

ItemReceiveAction.cs
namespace InventorySystem
{
    public enum ItemReceiveAction
    {
        Treasure,
        Loot,
        Purchase,
        Reward
    }
}

ItemReleaseAction.cs
namespace InventorySystem
{
    public enum ItemReleaseAction
    {
        Drop,
        Sell
    }
}

Let's now implement the Party class. This one's going to be big! B

Party.cs
using System.Collections.Generic;

namespace InventorySystem
{
    public class Party
    {
        private Character activeCharacter;
        private Dictionary<string, Character> members;
        private InventoryManager inventoryManager;
        private int wallet;

        public Party(InventoryManager gameInventoryManager)
        {
            activeCharacter = null;
            members = new Dictionary<string, Character>();
            inventoryManager = gameInventoryManager;
            wallet = 0;
        }

        public void SetActiveCharacter(string name)
        {
            activeCharacter = members[name];
        }

        public void ClearActiveCharacter()
        {
            activeCharacter = null;
        }

        public bool UseItem(ItemName name, Character targetCharacter)
        {
            return inventoryManager.Use(name, targetCharacter, BodyPart.Default);
        }

        public bool EquipItem(ItemName name, BodyPart bodyPart)
        {
            activeCharacter.Body.Set(bodyPart, name);
            return true;
        }

        public bool UnequipItem(BodyPart bodyPart)
        {
            ItemName? itemName = activeCharacter.Body.Get(bodyPart);

            if (itemName != null)
            {
                inventoryManager.Restore((ItemName)itemName, activeCharacter);
                activeCharacter.Body.Remove(bodyPart);
                return true;
            }
            return false;
        }

        public void AddMember(string name, Character member)
        {
            members.Add(name, member);
        }

        public void RemoveMember(string name)
        {
            members.Remove(name);
        }

        public int GetWalletAmount()
        {
            return wallet;
        }

        public List<ItemInfo> QueryAllItems()
        {
            return inventoryManager.QueryAll();
        }

        public ItemInfo QueryFor(ItemName name)
        {
            List<ItemInfo> data = inventoryManager.QueryAll();
            ItemInfo info = data.Find(x => x.Name == name);

            return info;
        }

        public void ReceiveItem(ItemName name, ItemReceiveAction action)
        {
            switch(action)
            {
                case ItemReceiveAction.Purchase:
                    var item = QueryFor(name);
                    wallet -= item.Value;
                    inventoryManager.Acquire(name);
                    break;
                default:
                    inventoryManager.Acquire(name);
                    break;
            }
        }

        public void ReleaseItem(ItemName name, ItemReleaseAction action)
        {
            switch(action)
            {
                case ItemReleaseAction.Sell:
                    var item = QueryFor(name);
                    wallet += item.Value;
                    inventoryManager.Drop(name);
                    break;
                default:
                    inventoryManager.Drop(name);
                    break;
            }
        }
    }
}

Let's now refactor Character to remove the unnecessary methods.

Character.cs
namespace InventorySystem
{
    public class Character
    {
        public CharacterBody Body { get; }
        public CharacterStatistics Statistics { get; }
        public string Name { get; }

        public Character(string name)
        {
            Body = new CharacterBody();
            Name = name;
            Statistics = new CharacterStatistics();
        }
    }
}

We're almost there! We just need to modify InventoryManager now. The first thing we'll want to do there is actually remove that nasty circular reference to Character which we had introduced in our previous parts. 😄

InventoryManager Changes

We will want to get rid of the circular reference in InventoryManager::Use where we used the character reference to "equip the item" in the same call as Use. Now that equipping an item is happening up at the party level, let's have the Party::Equipitem logic explicitly handle that part. This leaves InventoryManager::Use to be a more predictable operation.

Party.cs
...

        public bool EquipItem(ItemName name, BodyPart bodyPart)
        {
            if (UseItem(name, activeCharacter))
            {
                activeCharacter.Body.Set(bodyPart, name);
            }

            return true;
        }
...

As we make changes to Use in InventoryManager, let's also implement Acquire and Drop. Everything else stays the same. For the sake of brevity, I'll only show the relevant changes. Just remember that InventoryManager now holds an ItemFactory instance!

InventoryManager.cs
...
        public bool Use(ItemName name, Character targetChar,
            BodyPart targetBodyPart)
        {
            if(registry[name].Quantity <= 0)
            {
                return false;
            }

            IItem item = registry[name].Item;

            if(!item.CanApply(targetChar))
            {
                return false;
            }

            item.Apply(targetChar, targetBodyPart);

            registry[name].Quantity--;

            return true;
        }
...
        public bool Drop(ItemName name)
        {
            if(registry[name].Quantity <= 0)
            {
                return false;
            }

            registry[name].Quantity--;
            return true;
        }
...
        public bool Acquire(ItemName name)
        {
            BaseItem item = itemFactory.CreateItem(name);

            if(!registry.ContainsKey(name))
            {
                registry.Add(name, new ItemData { Quantity = 0, Item = item });
            }

            registry[name].Quantity++;

            return true;
        }
...

Fixing Unit Tests

All the unit tests have broke due to moving lots of code around. Let's first fix up the existing unit tests and make sure the project is building again before we write more tests!

To show that our Party API is more intuitive in handling inventory, our unit test TestPotionOnSelfAndTarget actually simulated a party of two characters in battle healing each other during their turns. I've refactored this specific test with comments to show you all the new flow. 😄

[Fact]
public void TestPotionOnSelfAndTarget()
{
  Dictionary<ItemName, ItemData> initialRegistry = GetInitialRegistry();

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

  Character terra = new Character("Terra");
  Character locke = new Character("Locke");

  party.AddMember(terra.Name, terra);
  party.AddMember(locke.Name, locke);

  Assert.Equal(0, terra.Statistics.HP);
  Assert.Equal(0, locke.Statistics.HP);

  bool useResult;

  // Pretend we're in battle, and Terra is the character in turn.
  party.SetActiveCharacter(terra.Name);

  // Terra chooses to heal Locke
  useResult = party.UseItem(ItemName.Potion, locke);

  // Turn ends
  party.ClearActiveCharacter();

  Assert.True(useResult);
  Assert.Equal(10, locke.Statistics.HP);
  Assert.Equal(0, terra.Statistics.HP);
  Assert.Equal(2, party.QueryFor(ItemName.Potion).Quantity);

  // Pretend we're in battle, and Locke is the character in turn.
  party.SetActiveCharacter(locke.Name);

  // Locke uses Potion on Terra
  useResult = party.UseItem(ItemName.Potion, terra);

  // Turn ends
  party.ClearActiveCharacter();

  Assert.True(useResult);
  Assert.Equal(10, terra.Statistics.HP);
  Assert.Equal(10, locke.Statistics.HP);
  Assert.Equal(1, party.QueryFor(ItemName.Potion).Quantity);

  // Pretend we're in battle, and Locke is the character in turn.
  party.SetActiveCharacter(terra.Name);

  // Terra uses potion on Self!
  useResult = party.UseItem(ItemName.Potion, terra);

  // Turn ends
  party.ClearActiveCharacter();

  Assert.True(useResult);
  Assert.Equal(20, terra.Statistics.HP);
  Assert.Equal(10, locke.Statistics.HP);
  Assert.Equal(0, party.QueryFor(ItemName.Potion).Quantity);

  // Pretend we're in battle, and Locke is the character in turn.
  party.SetActiveCharacter(locke.Name);

  // Locke uses Potion on Terra
  useResult = party.UseItem(ItemName.Potion, terra);

  // Turn ends
  party.ClearActiveCharacter();

  Assert.False(useResult);
  Assert.Equal(20, terra.Statistics.HP);
  Assert.Equal(10, locke.Statistics.HP);
  Assert.Equal(0, party.QueryFor(ItemName.Potion).Quantity);
}

All tests are passing again after patching up the existing tests to now accommodate our changes. Let's now write some tests for our new code.

Unit Tests for Receiving and Releasing Items

Reiterating the new tests that should be written, we will create a new file in the InventorySystemTests project called TestReceiveAndReleaseItems.cs. To serve as a reminder, here are the scenarios of interest:

Our unit tests will test the following scenario:

  1. Test acquiring an item through loot/treasure/reward. These are fundamentally equivalent.
  2. Test acquiring an item through purchasing. The wallet of the party should be updated to reflect the loss. The item should also exist in the inventory.
  3. Test the scenario of dropping an item. The inventory should be updated to exclude the item, or have less quantity.
  4. The scenario of selling an item should also be tested. The party's wallet should be updated to contain the earnings, while the inventory should also be updated.

TestSellingAndPurchaseItem

[Fact]
public void TestSellingAndPurchaseItem()
{
  Dictionary<ItemName, ItemData> initialRegistry = GetInitialRegistry();

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

  Character terra = new Character("Terra");
  Character locke = new Character("Locke");

  party.AddMember(terra.Name, terra);
  party.AddMember(locke.Name, locke);

  Assert.Equal(0, party.GetWalletAmount());

  ItemInfo potionInfo;

  party.ReleaseItem(ItemName.Potion, ItemReleaseAction.Sell);

  potionInfo = party.QueryFor(ItemName.Potion);
  Assert.Equal(potionInfo.Value, party.GetWalletAmount());
  Assert.Equal(2, potionInfo.Quantity);

  party.ReceiveItem(ItemName.Potion, ItemReceiveAction.Purchase);

  potionInfo = party.QueryFor(ItemName.Potion);
  Assert.Equal(0, party.GetWalletAmount());
  Assert.Equal(3, potionInfo.Quantity);
}

The above is a simple scenario that will test the sale and purchase of an item as if the player was in a shop in town. We test this buy first selling a Potion and seeing the sale go through by checking the wallet amount and remaining quantity within our inventory.

Then our party purchases a Potion and check to see if the wallet has been changed along with a gain in quantity within our inventory.

TestAcquireItem

[Fact]
public void TestAcquireItem()
{
  Dictionary<ItemName, ItemData> initialRegistry = GetInitialRegistry();

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

  Character terra = new Character("Terra");
  Character locke = new Character("Locke");

  party.AddMember(terra.Name, terra);
  party.AddMember(locke.Name, locke);

  ItemInfo ironHelmetInfo;

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(1, ironHelmetInfo.Quantity);

  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Loot);

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(2, ironHelmetInfo.Quantity);

  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Treasure);

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(3, ironHelmetInfo.Quantity);

  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Reward);

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(4, ironHelmetInfo.Quantity);
}

This test will test various scenarios acquiring items through a non-purchase action. We expect to see new Iron helmets appear in our inventory for free! We should also be careful not to get too greedy, now! 💰

TestDropitem

[Fact]
public void TestDropItem()
{
  Dictionary<ItemName, ItemData> initialRegistry = GetInitialRegistry();

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

  Character terra = new Character("Terra");
  Character locke = new Character("Locke");

  party.AddMember(terra.Name, terra);
  party.AddMember(locke.Name, locke);

  ItemInfo ironHelmetInfo;

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(1, ironHelmetInfo.Quantity);

  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Loot);
  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Loot);
  party.ReceiveItem(ItemName.IronHelmet, ItemReceiveAction.Loot);

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(4, ironHelmetInfo.Quantity);

  party.ReleaseItem(ItemName.IronHelmet, ItemReleaseAction.Drop);
  party.ReleaseItem(ItemName.IronHelmet, ItemReleaseAction.Drop);

  ironHelmetInfo = party.QueryFor(ItemName.IronHelmet);
  Assert.Equal(2, ironHelmetInfo.Quantity);
}

First, we will acquire multiple items of the same type, then we'll just drop a couple and see if the quantity is reflected properly.

Now, let's run all the tests and see if we're passing all 7. 😄

All tests passing

All tests are now passing!

Summary

Phew! This was a long one. I bet creating these hard-coded items through a hard-coded factory will get old after a while!

In the next chapter, we'll have some fun in experimenting with dynamic instantiation of these items along with exploring the possibilities in procedurally generating unique items. This reminds me of Diablo!