Rogue C# – Displaying and Selecting Inventory
In the last chapter of the series, we enabled the inventory system in our roguelike game to store item specifications and create new objects from them on the map. The player is able to pick them up and add them to a limited inventory. Now the player needs to be able to view the inventory and manage its contents so they can have the specific items they need. At this point, the player is filling up their inventory with food since that’s the only thing defined so we’ll start by letting them select and drop some of it when needed.
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.
Displaying Inventory
Let’s see how we can display the inventory for the player to select a ration or a piece of fruit. This will be the first time we’ve displayed anything but the map itself on the main screen.
Currently, the main form references the MapText() function of the current Game object but this needs to be redirected. The Game class needs a new property.
public string ScreenDisplay { get; set; }
Then, at the end of the Game class constructor and the KeyHandler() method:
ScreenDisplay = CurrentMap.MapText();
Finally, the form can be changed to reference the new ScreenDisplay property.
lblArray.Text = currentGame.ScreenDisplay;
This means that the form is no longer limited to the output of MapLevel.MapText(). We can set ScreenDisplay to anything and the form will display it and it will still update with each turn because it’s being set at the end of the KeyHandler() method. Now we need a new method to construct the inventory screen and we need to reformat the player’s list of inventory items into the kind of grouped list that the game typically displays.
I finally decided to make this a static function in the Inventory class and pass a reference to the current player’s inventory list. I’ve added an abbreviated form of the function below and you can find the full version on Github.
public static List<InventoryLine> InventoryDisplay(List<Inventory> PlayerInventory)
{
char charID = 'a';
// Get the player's current inventory in a grouped format.
List<InventoryLine> lines = new List<InventoryLine>();
// Get groupable identified inventory.
var groupedInventory =
(from invEntry in PlayerInventory
where invEntry.IsGroupable && invEntry.IsIdentified
group invEntry by invEntry.RealName into itemGroup
select itemGroup).ToList();
// Add groupable non-identified inventory.
...
// Get non-groupable identified
...
// Create a unique list of grouped items and count of each.
foreach (var itemGroup in groupedInventory)
lines.Add(new InventoryLine
{ Count = itemGroup.Count(), InvItem = itemGroup.First() });
// Add non-grouped items.
foreach (var invEntry in individualItems)
lines.Add(new InventoryLine { Count = 1, InvItem = invEntry });
// Order new list by item category.
lines = lines.OrderBy(x => x.InvItem.ItemType).ToList();
// Call the ListingDescription function to get a finished description.
foreach (InventoryLine line in lines)
{
line.ID = charID;
line.Description = line.ID + ".) "
+ ListingDescription(line.Count, line.InvItem);
charID++;
}
return lines;
}
In summary, the function uses LINQ to get groupings and counts of the inventory items marked as groupable and then those marked as non-groupable. It also looks at whether they’re identified or not because we need to maintain the aliases for the non-identified ones. The function references a new InventoryLine class which is just a small collection of properties to strongly type the elements in each line.
internal class InventoryLine
{
public char ID;
public int Count;
public Inventory InvItem;
public string Description;
}
The InventoryLine class also includes a char ID property that will hold the letter assigned to the property. InventoryDisplay() makes use of the char type to increment the letter for each item that it adds to the listing. The class also holds a reference the next inventory object for each group which will hopefully come in handy later.
Finally, for each line in the new grouped list, the function references ListingDescription(), another new static function that looks at the item category and generates a finished description line with the proper formatting based on the number and whether the item has been identified.
public static string ListingDescription(int Number, Inventory Item)
Now, we have the basic functions to generate the display so we have to call them. The game also needs to respond differently if the inventory display is being shown. The KeyHandler() method doesn’t need to respond to movement and other keys if the inventory listing is showing, at least not in the same way. The letter keys will now represent inventory items for selection. The Game class gets another property.
public bool InvDisplay { get; set; }
After adding constants for the ‘i’ and ESC keys, KeyHandler() is updated to respond to them.
case KEY_I:
DisplayInventory();
cStatus = "Here is your current inventory. Press ESC to exit.";
break;
case KEY_ESC:
RestoreMap();
cStatus = "";
break;
The new Game.DisplayInventory() method sets the InvDisplay property and calls the Inventory.InventoryDisplay() function to get the lines.
private void DisplayInventory()
{
// Switch the screen to the player's inventory.
InvDisplay = true;
ScreenDisplay = "\n\n";
foreach(InventoryLine line in Inventory.InventoryDisplay(CurrentPlayer.PlayerInventory))
ScreenDisplay += line.Description + "\n";
}
I really wish the original game was that generous with food. We’ll see how this version responds when there are more inventory items to select from. I also expect there will be some debugging as the functions try to process other items but I can already tell that the inventory limit works.
Selecting the Items
Here’s the next challenge – we’ve already established that each inventory item is going to have a delegate function that applies whatever effects it’s supposed to have. You usually have to select an item in order to use it.
So the process starts by the user pressing a button such as ‘d’ to drop an item. The Game.KeyHandler() method will route the program to a new DropInventory() function that pulls up the inventory screen so the user can select an item to be dropped. The user selects the letter of the item they want to drop and then the program removes the item from their inventory and puts it back on the map.
Except that Windows Forms applications don’t have an easy way to wait for a keystroke in the middle of a method or function. We need two separate calls to a function, one when the user presses the first key and another when the select the inventory item, and we need a way for the program to remember what it’s actually supposed to do with the inventory item once it’s selected.
First, I added a new nullable property to the Game class.
public Func<char?, bool>? ReturnFunction { get; set; }
When the user presses ‘d’, the KeyHandler() method will still send it to the new Game.DropInventory() function matching the delegate in the ReturnFunction property.
private bool DropInventory(char? ListItem)
{
bool retValue = false;
Inventory? item;
if (!InvDisplay)
{
DisplayInventory();
cStatus = "Please select an item to drop.";
ReturnFunction = DropInventory;
}
...
So, if the InvDisplay mode is not set, the function will call DisplayInventory() which will set it to True and show the inventory screen. DropInventory() also sets itself as the ReturnFunction.
KeyHandler() now has a new section to handle keys pressed while InvDisplay = True.
char lowerCase = char.ToLower((char)KeyVal);
// If the inventory display screen is up
if (InvDisplay)
{
if (lowerCase >= 'a' && lowerCase <= 'z')
{
if (ReturnFunction != null)
ReturnFunction(lowerCase);
}
else if (KeyVal == KEY_ESC)
{
ReturnFunction = null;
RestoreMap();
cStatus = "";
}
keyHandled = true;
}
If the user presses any of the letters, it will send the value to whatever function is set in the ReturnFunction property. Notice the method for converting a char value to lower case as the inventory lines do use lower case letters and, in this case, the comparison is actually case-sensitive. The KeyVal value represents uppercase whether the SHIFT key is down or not.
The KeyHandler() function was also starting to get a little complex and I noticed while debugging that it had never been running through the key decisions right. CTRL key combinations would cause it to hit the options for when SHIFT was false because the SHIFT options were being handled first. Worse, setting a breakpoint was misleading because CTRL / SHIFT keys were being passed on their own and if I used CTRL-D for developer mode, the D key would be lost after the breakpoint activated. My solution was to update the main form so it doesn’t send CTRL and SHIFT keys solo but only if the following conditions are met.
if (currentGame != null && e.KeyValue > 18)
KeyHandler() also skips over sections if the code has already found a matching condition. Now, when the program calls DropInventory(), it passes the letter that the user selected and the function searches for that letter in the grouped list provided by Inventory.InventoryDisplay() which also provides the next Inventory object reference for that selection.
CurrentPlayer.Location.MapInventory = items[0];
CurrentPlayer.PlayerInventory.Remove(items[0]);
RestoreMap();
retValue = true;
cStatus = $"The item has been removed from inventory.";
The function moves the item back to the map, restores the map to the screen, turns off the InvDisplay mode and notifies the user. Then it clears the Game.ReturnFunction property so that the function doesn’t get called again.
If the user selects a letter that’s not in the list, the function simply goes back to the map and tells them to select an appropriate option.
Now we have a reliable way to gather and drop inventory and you’ve actually seen an example of the code calling a delegate function. In the next chapter, we’ll finally implement the use of food within the program and make it necessary to the player’s survival. Then we’ll have the foundation for creating even more inventory types.
Sign up for our newsletter to receive updates about new projects, including the upcoming book "Self-Guided SQL"!
We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.
0