Rogue C# – The Inventory Class

At this point in the development of our roguelike game, there are a number of aspects to the game that can be added independently and not in any specific order. They don’t necessarily affect each other so I there’s some leeway in the order.

In the last chapter, we worked out how to track turns within the game. Now I want to add inventory collection to the game, starting with food, and have the character get hungry every so often. We need to put a system in place for collecting and managing not only food but all the other collectibles such as armor, weapons and potions which often have different requirements.

This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on ComeauSoftware.com. The current code for this project is also available on Github.

Requirements

Let’s take a look at the game’s inventory screen. This is an example where the inventory is maxed out and I can’t pick up anything else.

Rogue has many types of collectible items, not all of which are shown here.

  • Food – Mostly “rations” but also pieces of fruit like mangoes.
  • Rings – Provide various bonuses like slow digestion or increased damage.
  • Scrolls – Provide many advantages like mapping the level or identifying other objects.
  • Wands and Staffs – Mostly used for attack but can also project light to light up a room.
  • Potions – One-time bonuses like restoring strength. Some are toxic or debilitating.
  • Armor – Various levels of protection against attacks.
  • Melee, ranged and throwing weapons
  • Ammunition for ranged weapons
  • The Amulet

When the player starts the game, they already have a few items in inventory – usually a ration of food, the armor they’re wearing, a couple of weapons and some arrows.

Basic Requirements

The limit on the inventory can be handled in a couple different ways, either through individual items or by inventory “slots”. I think the classic game uses the latter method where the rogue’s storage includes a specific number of spaces and each type of item has an upper of how many of the item into that space. For all I know, it either allows one to a slot or an unlimited quantity because I’ve never collected more than four or five of any given thing before I was needing to use it or drop it to pick up something else.

Right away, we can see that we need some kind of collection to hold the player’s inventory, probably a generic list or array. Unless I think of another way to do it, each of the collectible items is going to be represented by an object based on an Inventory class. As you’ll see in this and following chapters, each of these items has many properties and each has a specific effect on the game so a class is justified but, as far as I can help it, I am not writing separate classes for each item type, much less each subtype like the scroll of Food Detection.

So, let’s start constructing the new Inventory class and see where it goes. The Inventory objects can be stored in a List<Inventory> collection and the program can enforce a limit on the number of items stored by refusing to add more when the player steps on a space with an item in it. Otherwise, items will be added automatically.

namespace RogueGame
{
    internal class Inventory
    {

    }
}

You might not immediately notice it by the listing above but items are listed in order by type; food, rings, scrolls, wands, etc.. Throwables like the spear are at the bottom. One way to do this would be to use an enumeration for the types which assigns them an integer. That enumeration type can be used in the Inventory class.

public enum InvCategory
{
    Food = 0,
    Ring = 1,
    Scroll = 2,
    Wand = 3,
    Staff = 4,
    Potion = 5,
    Armor = 6,
    Weapon = 7,
    Ammunition = 8,
    Amulet = 9
}

public InvCategory ItemCategory { get; set; }

The actual identities of many of the items need to be discovered either through identify scrolls or by using them so they initially have alternate appearances. Scrolls have nonsense names, rings are identified by their gemstone, potion by color, etc.. These descriptions are randomized so they’re not the same from one game to the next and we’ll get into how we’re going to store the actual descriptions later. For now, we need some properties that will enable us to store the anonymous and actual names.

public string CodeName { get; set; }
public string RealName { get; set; }
public bool IsIdentified { get; set; }

Some items, like scrolls and potions, can be grouped into the same inventory slot and others can’t. Individual weapons and armor are listed separately because of the bonuses assigned to them.

public bool IsGroupable { get; set; }

Weapons, ammunition and even potions can be either wielded or thrown at monsters from a distance. Potions like the potion of poison or blindness have a chance of shattering and applying their effect to the opponent. Wands and staffs discharge energy or magic in a specific direction. For these properties, I’m going to declare them as type Func which means that the property will accept a call to a function which they will then run. In C#, this is also known as a delegate. I could also use the Action type which would store a method but then, if I decide I want to return something, I’m out of luck.

public bool IsWieldable { get; set; }
public Func<Player, MapLevel.Direction, bool>? ThrowFunction { get; set; }
public Func<Player, MapLevel.Direction, bool>? ZapFunction { get; set; }

The properties will hold a function that accepts a reference to the current player, a member of the MapLevel.Direction enumeration and then returns a boolean which will probably just indicate success or failure. By storing a function call, I can write a separate function for each type of potion, for example, to carry out the specific effect of that potion. I’ve also declared them as nullable so if the ThrowFunction property is not defined, that means the item can’t be thrown. I don’t want wands and staffs thrown so they’ll use the ZapFunction.

At least that’s how it should work – we’ll see if it holds up in practice.

Most items also have standard functions so I’ll add another delegate property for that. Some inventory items work on other items so we can pass a nullable inventory reference, just in case.

public Func<Player, Inventory?, bool> MainFunction { get; set; }

Rings

A player can wear a ring on each hand and have the effects of both at once. For this reason, the Player class needs a couple new properties.

public Inventory LeftHand { get; set; }
public Inventory RightHand { get; set; }

Each of these can now hold a ring. Most rings have continuous effects and speed up the digestion so the player has to eat more. This will need to be programmed into the turn management code, maybe in Game.MoveCharacter().

Some rings are cursed so they can’t be removed without a scroll of Remove Curse. This also applies to armor so it should probably be an Inventory property.

public bool IsCursed { get; set; }

Finally, rings and other items have an increment value which determines their strength so a +2 ring of Add Strength increases the player’s Strength value by 2 while it’s worn.

public int Increment { get; set; }

For items that don’t use it, this value can simply be 0 and whatever code references them simply won’t check it.

Potions and Scrolls

Potions and scrolls don’t have any properties we haven’t already explored but they do have some effects that will require extra properties in the Player and Monster classes if the following items are implemented in this game.

  • Monster confusion (scroll)
  • Sleep (scroll)
  • Hold monster (paralysis) (scroll)
  • Haste self (potion)
  • Blindness (potion)
  • Confusion (potion)
  • Paralysis (potion)

Confusion causes your player or the monster to move erratically and the rest are self-explanatory. All of them last for a random number of turns. We can probably reduce these to three new properties in the Player class.

public int Confused { get; set; }
public int Immobile { get; set; }
public int Blind { get; set; }

All of these will be set to 0 unless they’re active in which case they’ll have a turn number to indicate when the effect will end.

Weapons

Weapons have damage potentials for each attack which are usually expressed as dice rolls such as 4d4 (4, 4-sided dice) which would be a minimum of 4 (if you hit) and a maximum of 16. For this game, these rolls will be performed by the Random class and two Inventory class properties can be used to decide the limits.

public int MinDamage { get; set; } 
public int MaxDamage { get; set; }

All weapons have two extra increments, sometimes called enchantments – one for damage and one for accuracy – which will be figured into attacks. This includes a couple of the staffs and wands. We added a general Increment property earlier so now we can add two more which will be 0 when not used.

public int DmgIncrement { get; set; } 
public int AccIncrement { get; set; }

Also, the Player class should have a property to indicate the weapon that’s being held.

public Inventory? Wielding { get; set; }

Armor

In addition to the basic increment, armor has a class value that determines how much protection it offers. Leather armor is Class 2 and Plate mail is Class 7. This can be handled by a single integer property in the Inventory class.

public int ArmorClass { get; set; }

Inheritance

At this point we have a single Inventory class for any item that might be implemented. One potential issue is that it has four properties that will only be used for weapons; the rest of the time, they will be zeroed out. We could split them off into their own Weapon class like this:

internal class Weapon : Inventory
{
    public int DmgIncrement { get; set; }
    public int AccIncrement { get; set; }
    public int MinDamage { get; set; }
    public int MaxDamage { get; set; }
}

As you can see on the first line, this new class would reference the Inventory class. By doing this, it would inherit all the other properties and any functionality that we put in that class so if you declare a Weapon object, it will also have properties like CodeName and IsIdentified.

This would be useful if there was a lot of very specific functionality for weapons or functions that needed to specifically accept weapon objects. The new Player.Wielding property could use it. Inheritance is an important concept to understand in object oriented programming and it’s used throughout the .NET system.

In this particular case, however, this would probably over-complicate the program, especially since some potions and staffs are explicitly used as weapons but not all. It would also complicate the process of storing all the details for the various items that are to be implemented in the game. So, for now, I’ll leave everything in the Inventory class.

In the next chapter, we’ll see how our new class works as we implement food collection and player hunger.