Tutorial Part 12: Inventory Management & Loot Drops (SadConsole v8)

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 : Entity
    {
        // backing field for Condition
        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(foreground, background, glyph)
        {
            // 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 MultiSpatialMap's list of entities
        // and lets the garbage collector take it
        // out of memory automatically.
        public void Destroy()
        {
            GameLoop.World.CurrentMap.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();

            // Produce lot up to a max of numLoot
            for (int i = 0; i < numLoot; i++)
            {
                // Create an Item with some standard attributes
                int lootPosition = 0;
                Item newLoot = new Item(Color.Green, Color.Transparent, "fancy shirt", 'L', 2);

                // Let SadConsole know that this Item's position be tracked on the map
                newLoot.Components.Add(new EntityViewSyncComponent());

                // Try placing the Item at lootPosition; if this fails, try random positions on the map's tile array
                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);

                // add the Item to the MultiSpatialMap
                CurrentMap.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))
            {
                // if there's a monster here,
                // do a bump attack
                Monster monster = GameLoop.World.CurrentMap.GetEntityAt<Monster>(Position + positionChange);
                Item item = GameLoop.World.CurrentMap.GetEntityAt<Item>(Position + positionChange);
                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
...
}

(You may need to add a using System.Collections.Generic to the top of your class, to let you use Lists). Simple, right? We’ll add plenty of functionality to the Inventory 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 destroy it
            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 tells SadConsole to stop tracking it as an onscreen Entity, and GoRogue to remove it from its MultiSpatialMap. Urgh. Now I’m beginning to think we should have called the method Item.RemoveFromMap. 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:

        public Monster(Color foreground, Color background) : base(foreground, background, 'M')
        {
            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);
                newLoot.Components.Add(new SadConsole.Components.EntityViewSyncComponent());
                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. I rewrote the ResolveDeath method to take advantage of more verbose logging:

        // Removes an Actor that has died
        // and displays a message showing
        // the actor that has died, and they loot they dropped
        private static void ResolveDeath(Actor defender)
        {
            // Set up a customized death message
            StringBuilder deathMessage = new StringBuilder($"{defender.Name} died");

            // dump the dead actor's inventory (if any)
            // at the map position where it died
            if (defender.Inventory.Count > 0)
            {
                deathMessage.Append(" and dropped");

                foreach (Item item in defender.Inventory)
                {
                    // move the Item to the place where the actor died
                    item.Position = defender.Position;

                    // Now let the MultiSpatialMap know that the Item is visible
                    GameLoop.World.CurrentMap.Add(item);

                    // Append the item to the deathMessage
                    deathMessage.Append(", " + item.Name);
                }

                // Clear the actor's inventory. Not strictly
                // necessary, but makes for good coding habits!
                defender.Inventory.Clear();
            }
            else
            {
                // The monster carries no loot, so don't show any loot dropped
                deathMessage.Append(".");
            }

            // actor goes bye-bye
            GameLoop.World.CurrentMap.Remove(defender);

            // Now show the deathMessage in the messagelog
            GameLoop.UIManager.MessageLog.Add(deathMessage.ToString());
        }

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 Map.

Give that a go. If you’re not smiling when you pick up pounds upon pounds of gleaming pink monster spork giblets, you’re slowly turning into John Carmack. If you giggled maniacally, you’ve turned into John Romero.

In our next tutorial, let’s build some UI to show off our prized collection of spork and fancy shirts! Oh – and some door generation code. >_>

Download the final source code for Tutorial Part 12 here.

Published on March 10, 2019