Design Docs - Thinking About Inventory - Part 2

Now that we have a basic inventory system to build off of using an InventoryManager class, let's try to extend it even further and establish some concrete functionality.

Before we start diving deeper, I'd like to take some time to fill you in on some changes in my thoughts that have happened since Part 1.

  • We've renamed destChar, and all terminology relating to a "destination" as "target". Therefore, destChar becomes targetChar, and so on.
  • BodyLocation is now BodyPart.

It's good for me to communicate all this now so that everyone reading this from the last chapter doesn't get confused. 😕

Item Effects and the Usage on Characters

In order to keep everything all generic and abstract, the concept of item and equipment are seen as the same generic IItem type.

Note to self: I might want to change the name of IItem down the line. The initial intention of it was to be an interface that should be implemented. It has instead, morphed into a base type. It might have to be something like BaseItem soon. I'll adjust as needed and will communicate accordingly!

In JRPGs items can not only be used as an effect on the character to alter their stats, or heal from a status effect (potions, antidotes, eye-drops, etc), but items can also be equipment such as weapons and armors to alter the statistics which can be leveraged in battle to gain an advantage against enemies in the field.

 

Final Fantasy 9 equipment screen

Final Fantasy IX equipment screen

 

A source character in our design is allowed to apply items onto a target character. In this case, a target character can really be the same source character, or another character in the party. Effects can then be executed through the item's Apply method. We need to answer the question on how the item is actually applied onto the "target character".

Character

In order for any of this stuff that will be discussed to make any sense, I think it is now appropriate to define some of the necessary components within our system relating to the Character. I have so far made multiple references to a BodyPart type, and in my mind, it is a enum type (strings for now) that map to specific parts of the character's body.

Again, in pseudo-pseudo code:

BodyPart {
	Default,
	Head,
	Chest,
	Legs,
	Arms,
	Hands,
	Accessory
}

Note that Default is intended for consumeables. A Default target means that the item will not be attached to a specific body part, but is intended for the item effect handler to apply onto the character and be disposed immediately.

Our basic model of a Character consists of: Name (e.g. Terra), and an object which manages the handles the coupling of BodyPart and ItemName. Yes, this second field, called Body is a new type called CharacterBody. Here's the definition:

class CharacterBody {
	private Dictionary<BodyPart, ItemName> body;
	public ItemName Get(BodyPart bodyPart) { ... }
	public void Set(BodyPart bodyPart, ItemName itemName) { ... }
	public ItemName Remove(BodyPart bodyPart) { ... }
}

The CharacterBody object type is another "manager" type class which will allow the book-keeping on what types of equipment is attached to the current character's body.

A Character can then choose to perform the following on items;

  • Use an item directly.
UseItem(ItemName name)
  • Equip an item.
EquipItem(ItemName name, BodyPart bodyPart)
  • Unequip an item.
UnequipItem(BodyPart bodyPart)

The methods above will basically serve as wrappers to manipulate the CharacterBody instances within the Character instance.

Let's not forget that a Character also has some statistics tied to it. We can just define a basic enum called CharacterStatistics to serve as a placeholder for now:

CharacterStatistics {
	HP,
	MP,
	LVL,
	EXP,
	ATK,
	MAG,
	DEF,
	SPR
}

These are your basic JRPG stats. It isn't complete, but whatever! It's for the sake of education!

Alright, now we have some sort of formal definition for our Character.

class Character {
	private CharacterBody Body;
	public string Name;
	public CharacterStatistics Statistics;
	public bool UseItem(ItemName name, Character targetChar, BodyPart targetBodyPart) { ... }
	public bool EquipItem(ItemName name, BodyPart targetBodyPart) { ... }
	public bool UnequipItem(BodyPart targetBodyPart) { ... }
}

As with before, regarding the return types... I have no idea what it'd be useful for, just good to have for now.

Applying and... Unapplying?

I've decided to just pass the item name around as that's all that is really needed to store the relationship of an item to the specific body part. The intention here is that the Apply method for the IItem had already applied the effects onto the character. When the item is equipped, the item is not only generated from the factory instance gathered from the registry of the InventoryManager, but the effect is applied, and then associated onto the body of the character.

Okay, well that's good for equipping, but what about "unequipping" the item? What do we do here? Remember our weird method that we had added last minute in IItem called "AfterRestore"? Well, that was sort of the intention, but the name of the method was in the tip of my tongue. To make things clearer, we'll rename that method to UndoApply, and this will actually be the method to undo any effects which equipping the item had done to the character.

This makes a useful method for UnequipItem to call from Character.

So now, in IItem, AfterRestore becomes this:

bool UndoApply(Character targetChar)

Also, notice that in the API definition of Character, EquipItem and UnequipItem in character does not take in a target character? It is beecause I've intentionally assumed that a character can only put on, and take off weapons, armor, and accessories on their own body. This not only simplifies implementation, but is proper to your typical JRPG. Coincidentally, both are rules to the Rule of 99.

Using and Item - Consumeable and Equippable Example

Alright now, let's now see how awkward all of this is so far, and let's try an example implementation in pseudo-code if all of this can convincingly work thus far. Let's use our source character as Terra and our target character as Locke.

Terra Branford

A picture of Terra, because my posts tend to be a wall of text, and I want to have a little more color. 😎

Our item here will be a potion. 🏥

method call: Terra.UseItem(ItemName.Potion, Locke, BOdyPart.Default)

// Character
public bool UseItem(...) {
	return inventoryManager.Use(ItemName.Potion, this, Locke, BodyPart.Default)
}

// InventoryManager
public bool Use(ItemName itemName, Character srcChar, Character targetChar, BodyPart targetBodyPart) {
	if(this.registry[itemName].quantity == 0) {
		return false;
	}
	
	IItem item = this.registry[itemName].FactoryInstance.get();
	
	if(!item.CanApply(targetCharacter, targetBodyPart)) {
		return false;
	}
	
	item.Apply(targetCharacter, targetBodyPart);
	
	if(targetBodyPart != BodyPart.Default) {
		targetCharacter.EquipItem(itemName, targetBodyPart);
	}
	
	this.registry[itemName].Quantity--;
}


// Potion Item 
public class Potion : IItem {
	public bool CanApply( ... ) {
		if(targetCharacter.Statistics.HP >= 100) {
			return false;
		}
		return true;
	}
	
	// Heal 10 HP
	public bool Apply(...) {
		targetCharacter.Statistics.HP += 10;
		return true;
	}
	
	public bool UndoApply() {
		return false;
	}
}

 

Notice that I have moved some code around since the last time I had explained how UseItem would work.

So in summary here's what happened with the above:

  1. We check if an item can be used implicitly through inventory manager. A short circuit return of false will occur if this item cannot be used. Otherwise, the item is Applied directly, and if the item isn't a consumeable, then the item name is registered to the target character's body.
  2. Book-keeping operations relating to the registry stays within the InventoryManager. This is very important as my intention is to matain a unidirectional data flow as much as possible.

From what we see above, Character.UseItem() just becomes a wrapper to the inventory manager's Use method. Let's just leave it as that way for now.

Unequipping an Item

As you can probably now see, equipping armors, and weapons are similar. Effects are applied using the same Apply call, but now with a given body target. The name of the item is assigned to a body part of the character.

Well, okay, that's great. Now, how about unequipping an item? Suppose Terra wants to unequip a helmet. I envision the process being somewhere along the lines of:

Terra.UnequipItem(BodyPart.Head);

// Character
public bool UnequipItem(...) {
	ItemName itemName = this.body.Get(BodyPart.Head);
	if(itemName != null) {
		inventoryManager.Restore(itemName, this);
		this.body.Remove(BodyPart.Head);
		return true;
	}
	return false;
}

// InventoryManager
public bool Restore(...) {
	IItem item = this.registry[itemName].FactoryInstance.get();
	this.registry[itemName].quantity++;
	item.UndoApply(targetCharacter);
	return true;
}

The process will then disassociate the particular item away from the character. This seems to work so far... in theory.

What the Factory?!?!

If you're following closely and all this sort of makes sense, you're probably noticing that we are only ever using ItemName and using that to refer to item factories within the inventory manager's registry.

We seem to be constructing instances using these factories, but not doing much else aside from applying and undoing effects.

In a way, these factories are useless. Okay, "not in a way", lol. They are useless. I had initially thought it would be useful to have them, but at this point, I haven't really proved to you how they can be useful.

So now, let's refactor the design of ItemData to consist of just Quantity and an instance to IItem.

Keep in mind, I am walking into dangerous territory here. I am now making a huge assumption that there will never need to be a reliance upon the IItem state. The assumption is that IItem is now immutable. Let's see how long I can go before having to change this design!

class ItemData {
	public int Quantity;
	public IItem Item;
}

Our Story So Far...

Wow! Alright! Now at this point we have a potential inventory system designed at the minimal level. I am not done yet as I still intend to go even further in detail on how items are implemented. I also want to discuss how we can initialize these instances. However, we don't really need this info for the next part of our discussion.

Here is a summary of what we have made so far, in an improper UML sort of way.

The inventory system modeled in UML

Ok! Let's finally dig into some code and see if what we have so far truly works in practice!

But that will have to wait until...

Next time! 😥