Rogue C# – Adding Scrolls and Potions
Coming back around to the inventory side of the game, I decided to add some extra variety to the available items, starting with the scrolls and potions. Each one has a different effect so they need to be coded separately. I’ll be adding some of the really useful ones from the original game like the Identify and Mapping scrolls and might make up some of my own eventually.
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.
Originally, I wanted to use delegates as part of the Inventory class that would specify a procedure for each of the scrolls and potions and be automatically called. There were a number of problems with this.
- Different items might need different information which means different parameters for the procedure. I could just have a standard procedure signature that would accept the current Game object which contains references to all the other objects but it’s bad practice to provide that much access to a single function.
- Specifying the procedure in the Inventory class as part of one of the templates poses a couple of scope issues. If the handing procedure is in another class, like Game, it either has to be static which would prevent it from accessing the information for the current game and player, or the Inventory class would need a reference to the current Game object which I again didn’t want to do. Classes should be as independent of each other as possible and having to ensure that a Game object in the Inventory class is instantiated and current adds more potential for bugs.
After wrestling with it for awhile, I finally decided to go back to simply creating a ReadScroll() function in the Game class that would use a Switch statement to call different procedures as needed based on the name of the scroll. I might still come up with another way but, for right now, this works. As a general rule, delegates are the best tool for when you want to call different procedures based on different conditions; they just don’t work for this particular project.
Reading Scrolls
Again, we start with the Game.KeyHandler() method which maps the game’s key commands. I defined a KEY_R constant (ASCII value 82) so the ‘r’ key calls the new ReadScroll() boolean function which follows the same basic strategy as the Eat() function.
- Display the inventory and ask the player to select an item.
- Set the ReturnFunction class variable to the current fuction.
- When the player presses a letter, the game will return to the function.
- The function will resume and process the item selected.
This is generally known as a closure in C#.
if (GameMode != DisplayMode.Inventory)
{
// Verify the player has something they can read.
items = (from inv in CurrentPlayer.PlayerInventory
where inv.ItemCategory == InvCategory.Scroll
select inv).ToList();
if (items.Count > 0)
{
// If there are any scrolls, show the inventory
// and let the player select it. Set to return and exit.
DisplayInventory();
UpdateStatus("Please select an item to read.", false);
ReturnFunction = ReadScroll;
}
else
// Otherwise, notify the player.
UpdateStatus("You don't have any scrolls.", false);
}
else
{
// Get the selected item.
items = (from InventoryLine
in InventoryDisplay(CurrentPlayer.PlayerInventory)
where InventoryLine.ID == ListItem
select InventoryLine.InvItem).ToList();
After the user selects one of the scrolls, the game returns to the ReadScroll() function to verify that the item is actually a scroll and then routes to the appropriate procedure based on the scroll’s real name.
// Call the appropriate delegate and remove the item
// from inventory.
if (items[0].ItemCategory != InvCategory.Scroll)
{
UpdateStatus("There's nothing on it to read.", false);
retValue = false;
}
else
{
// Route to the appropriate routine based on the name of the scroll.
switch (items[0].RealName)
{
case "Identify":
UpdateStatus("This is a Scroll of Identify.", true);
if (!items[0].IsIdentified)
SetInventoryAsIdentified(items[0].PriorityId);
readScroll = ScrollOfIdentify(null);
break;
case "Magic Mapping":
UpdateStatus("This scroll has a map on it!", false);
if (!items[0].IsIdentified)
SetInventoryAsIdentified(items[0].PriorityId);
readScroll = CurrentMap.DiscoverMap();
RestoreMap();
break;
default:
break;
}
if (readScroll) CurrentPlayer.PlayerInventory.Remove(items[0]);
Some scrolls, like the Identify scroll, are automatically identified when used if they weren’t already. For these, the game calls the SetInventoryAsIdentified() method before calling the function that will actually carry out the scroll’s instructions. This method accepts the PriorityID property value of the inventory item and sets all the corresponding items in the player’s inventory along with the template in the Inventory class as identified so any items found in the future will show by their real name.
Finally, if the scroll function returns True, the program removes it from the player’s inventory.
Identify Scrolls
The Scroll of Identify has an added challenge that, after the player selects it, they have to select another item to be identified. The ScrollOfIdentify() function therefore follows the same closure strategy and uses the same ReturnFunction variable to set itself as the function to be called.
At first, I was going to check to see if the player actually had any unidentified items so they wouldn’t waste a scroll but the code got a little cumbersome and I decided it wasn’t necessary anyway. In all the years I’ve played the game, I was usually pretty deliberate about needing to identify a specific item before I used a scroll.
Magic Mapping
Another handy scroll to have is the Scroll of Magic Mapping which reveals the entire map structure. This was simpler than the Identify scroll although I decided to implement it by adding a DiscoverMap() function to the MapLevel class instead of the Game class.
public bool DiscoverMap()
{
bool retValue = false;
List<MapSpace> spaces = (from MapSpace space in levelMap
where MapDiscovery.Contains(space.MapCharacter)
select space).ToList();
spaces.ForEach(space => { space.Discovered = true; space.Visible = true;
space.SearchRequired = false; space.AltMapCharacter = null; });
return retValue;
}
This function doesn’t reveal any of the monsters or items in the rooms, just the walls and hallways, the characters for which are contained in the MapDiscovery list referenced above.
Other Scrolls
There’s a pretty good variety of scrolls in the original game. Here are some notes on implementing the more interesting ones:
- Enchant Armor / Enchant Weapon – This is a relatively easy one where the player’s current weapon or armor can have its Increment property increased. None of the other inventory items are affected.
- Food Detection / Gold Detection – There’s no shortage of food in the dungeon right now but, as I add other inventory items, it will become scarcer. The original game was notorious for starving the player and a scroll that lights up all the food on the current level can be handy, or very disappointing if it sees nothing. This will be another function for the Inventory class and that will accept a parameter to specify what is to be found.
- Monster Confusion – You gain the ability to confuse the next monster you hit so it moves around uncertainly. Monsters already have a property that can set them as Confused for a specific number of turns. The Player class will likely need a new property to indicate if one of these scrolls has been read and it will need to be worked into the Game.Attack() method.
- Teleportation – This can be a life saver during a tough fight and can be simply implemented with the MapSpace.GetOpenSpace() function which returns an space on the map with no inventory or monsters.
Drinking Potions
Potions and scrolls are basically the same except that you drink one and read the other, so now we also have a QuaffPotion() function that’s virtually identical to the ReadScroll() function except that it references potions instead of scrolls and the messages are updated to reflect this and it’s called by the ‘q’ key.
Here are some of the more common potions and how their effects can be worked into the game:
- Healing / Extra Healing – These simply adjust the player’s hit points by various amounts. They also cancel out the blindness and confusion so those properties would be blanked.
- Monster detection – As with the Food / Gold Detection, this just finds all the monsters on the level and sets them to visible. In this case, I’ll probably need to add another boolean property to the MapLevel class to indicate that monsters should remain visible so that MapLevel.MapText() can be adjusted.
- Blindness / Confusion / Paralysis / Poison – Not all potions are helpful. We already have the Blind, Confused and Immobile properties in the Player class that can be set for a given number of turns based on these potions and the Poison potion simply reduces the HP by a random number.
There are some other potions that are a little more involved in their effects and might require more coding but I’ll detail those as I come to them.
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