Tutorial Part 12: Inventory Management

Important note: These tutorials were written for SadConsole version 7.x. The version 8.x tutorials are here.

So a recap: we’ve got movement, monsters and combat. What’s next? We probably want those monsters to drop some loot, and have that loot appear on the map, then get picked up by the player when they walk over it. For that, we’re going to need a simple inventory system that lets Actors store all kinds of loot.

First off, let’s create an Item class. Items will be our most generic type of entity that allows itself to be picked up, damaged, and destroyed.

Overly complex inheritance hierarchies tend to rear their ugly heads when we start defining different types of Items. For that reason, I am going to keep the Item class as generic as possible. I’ll leave it up to you to make more specific types of items. But be warned: it’s tempting to abuse inheritance, and you’ll pay the price for your love of categorizing!

using System;
using Microsoft.Xna.Framework;

namespace SadConsoleRLTutorial.Entities
{
    // Item: Describes things that can be picked up or used
    // by actors, or destroyed on the map.
    public class Item : SadConsole.Entities.Entity
    {
        private int _condition;

        public int Weight { get; set; } // mass of the item
        // physical condition of item, in percent
        // 100 = item undamaged
        // 0 = item is destroyed
        public int Condition { get { return _condition; }
            set
            {
                _condition += value;
                if (_condition <= 0)
                    Destroy();
            }
         }

        // By default, a new Item is sized 1x1, with a weight of 1, and at 100% condition
        public Item(Color foreground, Color background, string name, char glyph, int weight = 1,  int condition = 100, int width = 1, int height = 1) : base(width, height)
        {
            // assign the object's fields to the parameters set in the constructor
            Animation.CurrentFrame[0].Foreground = foreground;
            Animation.CurrentFrame[0].Background = background;
            Animation.CurrentFrame[0].Glyph = glyph;
            Weight = weight;
            Condition = condition;
Name = name;
        }

        // Destroy this object by removing it from
        // the EntityManager's list of entities
        // and lets the garbage collector take it
        // out of memory automatically.
        public void Destroy()
        {
            GameLoop.EntityManager.Entities.Remove(this);
        }
    }
}

Sweet and simple. For fun, I’ve included a cute Condition getter and setter that has a bit of game logic in it. When the Condition falls at or below zero, the item gets destroyed. Because Item is a managed type, the C# garbage collector will automatically destroy the object once the object falls out of scope. We force the object to leave scope by removing it from the Entities list.

The list of parameters in the constructor is a little on the chubby side. I’ve added several defaults to save time when instantiating new Items.

Now we can spawn a few items on the map. This is something we’ll want to do with a more formal generator at a later time, but for now let’s manually create a few items on the map. Modify the World class accordingly:

        // Create some sample treasure
        // that can be picked up on the map
        private void CreateLoot()
        {
            // number of treasure drops to create
            int numLoot = 20;

            Random rndNum = new Random();

            for (int i=0; i < numLoot; i++)
            {
                int lootPosition = 0;
                Item newLoot = new Item(Color.Green, Color.Transparent, "fancy shirt", 'L', 2);
                while (CurrentMap.Tiles[lootPosition].IsBlockingMove)
                {
                    // pick a random spot on the map
                    lootPosition = rndNum.Next(0, CurrentMap.Width * CurrentMap.Height);
                }

                // set the loot's new position
                newLoot.Position = new Point(lootPosition % CurrentMap.Width, lootPosition / CurrentMap.Width);
                GameLoop.EntityManager.Entities.Add(newLoot);
            }

        }

There’s nothing new or surprising here. This is pretty much the same code we used when we created the CreateMonster method.

And don’t forget to call the method in the World constructor:

        public World()
        {
...
            // spawn some loot
            CreateLoot();
        }

Run the project and walk over the loot. Nothing much happens eh? We need to create some game logic to deal with picking up items. Some Roguelikes have a special key reserved for Picking up items, while others let your walk over the item and auto-pickup. You’ll have to decide what kind of gameplay you prefer. For now, let’s do the easy one: picking up involves walking over an item.

Let’s modify the Actor class to add auto-pickup in the MoveBy method:

        // Moves the Actor BY positionChange tiles in any X/Y direction
        // returns true if actor was able to move, false if failed to move
        public bool MoveBy(Point positionChange)
        {
            // Check the current map if we can move to this new position
            if (GameLoop.World.CurrentMap.IsTileWalkable(Position + positionChange))
            {
                Monster monster = GameLoop.EntityManager.GetEntityAt<Monster>(Position + positionChange);
                Item item = GameLoop.EntityManager.GetEntityAt<Item>(Position + positionChange);

                // if there's a monster here,
                // do a bump attack
                if (monster != null)
                {
                    GameLoop.CommandManager.Attack(this, monster);
                    return true;
                }
                // if there's an item here,
                // try to pick it up
                else if (item != null)
                {
                    GameLoop.CommandManager.Pickup(this, item);
                    return true;
                }

                Position += positionChange;
                return true;
            }
            else
                return false;
        }

The modification is straightforward: we’re testing for the existence of an Item at the target location. If an item exists there, try to pick it up by calling the Pickup method in the CommandManager class.

Now, I don’t know about you, but I’m getting a little itchy in the britches seeing that much game logic packed into every Actor’s MoveBy method. If you walk away from your code for 6 months, would you remember that the Pickup and Attack commands are called from within the Actor class? It seems like a recipe for uninhabitable code. Worse, every time we add a different type of object to test for, that MoveBy method is going to become fatter and fatter. So let’s flag this class in the back of our minds for future refactoring. I don’t know how we’ll solve that problem yet – but there has to be a better location for it!

Actors need an inventory to store their possessions. This one’s simple. Add the following to the definition in the Actor class:

    public abstract class Actor : SadConsole.Entities.Entity
    {
...
        public List<Item> Inventory = new List<Item>(); // the player's collection of items
...
}

Simple, right? We’ll add plenty of functionality to that list later.

Okay, let’s finally add the Pickup command to our CommandManager:

        // Tries to pick up an Item and add it to the Actor's
        // inventory list
        public void Pickup(Actor actor, Item item)
        {
            // Add the item to the Actor's inventory list
            // and then remove it from the EntityManager
            actor.Inventory.Add(item);
            GameLoop.UIManager.MessageLog.Add($"{actor.Name} picked up {item.Name}");
            item.Destroy();
        }

Hey, neat! We found an alternate use for our Item.Destroy method. Keep in mind that it does not actually destroy the item – it simply removes it from the EntityManager. Urgh. Now I’m beginning to think we should have called the method Item.RemoveFromEntityManager. Oh well. It’s just that Destroy sounds so hot.

And for the piece de resistance – let’s give monsters actual loot, instead of that fake gold they’ve been lying about for days! Modify the Monster class:

            Random rndNum = new Random();

            //number of loot to spawn for monster
            int lootNum = rndNum.Next(1, 4);

            for (int i = 0; i < lootNum; i++)
            {
                // monsters are made out of spork, obvs.
                Item newLoot = new Item(Color.HotPink, Color.Transparent, "spork", 'L', 2);
                Inventory.Add(newLoot);
            }

Okay, all Monsters possess spork now. Pounds and pounds of spork. Now we need them to drop their spork when they croak. That’s a job handled by our CommandManager:

        // Removes an Actor that has died
        // and displays a message showing who died
        private static void ResolveDeath(Actor defender)
        {
            // dump the dead actor's inventory (if any)
            // on to the map position where it died
            foreach (Item item in defender.Inventory)
            {
                item.Position = defender.Position;
                GameLoop.EntityManager.Entities.Add(item);
            }

            GameLoop.EntityManager.Entities.Remove(defender);

            if (defender is Player)
            {
                GameLoop.UIManager.MessageLog.Add($" {defender.Name} was killed.");
            }
            else if (defender is Monster)
            {
                GameLoop.UIManager.MessageLog.Add($"{defender.Name} died.");
            }
        }

In the above snippet, we’re digging through the dead actor’s Inventory. For every item found in that inventory, set its position to the actor’s (previous) position, and add that item to the EntityManager’s list of Entities (so it will display on the console). 

Give that a go. If you’re not smiling when you pick up pounds upon pounds of gleaming pink monster spork, you’re not human enough.

In our next tutorial, I let’s build some UI to show off our prized collection of spork and fancy shirts!

Download the final source code for Tutorial Part 12 here.

Published on December 20, 2018