Press "Enter" to skip to content

Lesson 16.3 – Functions to use weapons and potions

Lesson Objectives

At the end of this lesson, you will know…

  • Nothing new. We’re just finishing the program, using the same things you learned in the previous lessons.

 

Now we’ll write the functions the player will use when fighting monsters.

We have two things they can do (besides click on one of the direction buttons, to run away from the battle): use a weapon on the monster or use a healing potion on themselves – if they have one in their inventory.

Here is the pseudo-code for the two functions:

Use a weapon function

  • Get the currently selected weapon from the cboWeapons ComboBox
  • Determine the amount of damage the player does to the monster
  • Apply the damage to the monster’s CurrentHitPoints
    • Display message
  • If the monster is dead (zero hit points remaining)
    • Display a victory message
    • Give player experience points for killing the monster
      • Display message
    • Give player gold for killing the monster
      • Display message
    • Get loot items from the monster
      • Display message for each loot item
      • Add item to player’s inventory
    • Refresh player data on UI
      • Gold and ExperiencePoints
      • Inventory list and ComboBoxes
    • “Move” player to current location
      • This will heal the player and create a new monster
  • If the monster is still alive
    • Determine the amount of damage the monster does to the player
    • Display message
    • Subtract damage from player’s CurrentHitPoints
      • Refresh player data in UI
    • If player is dead (zero hit points remaining)
      • Display message
      • Move player to “Home” location

 

Use a potion function

  • Get currently selected potion from cboPotions ComboBox
  • Add healing amount to player’s CurrentHitPoints
    • CurrentHitPoints cannot exceed player’s MaximumHitPoints
  • Remove the potion from the player’s inventory
  • Display message
  • Monster gets their turn to attack
    • Determine the amount of damage the monster does to the player
    • Display message
    • Subtract damage from player’s CurrentHitPoints
      • Refresh player data in UI
    • If player is dead (zero hit points remaining)
      • Display message
      • Move player to “Home” location
  • Refresh player data in UI

 

These are much simpler functions than the MoveTo() function.

We’ll be able to use some of the smaller functions we created during the refactoring lesson – for example, the AddItemToInventory() function, from the Player class, if the player defeats the monster and receives loot items.

 

Adding functions for monster battles

Step 1: Start Visual Studio Express 2013 for Desktop, and open the solution.

 

Step 2: Right-click on the SuperAdventure.cs form in the SuperAdventure project, to start working with the code for the UI.

To add the ability to use weapons and potions in combat, you can add in the code below, or replace the code for SuperAdventure.cs with the code here: https://gist.github.com/ScottLilly/b20787650f2ab2a78362

 

For the btnUseWeapon_Click function, add this:

        private void btnUseWeapon_Click(object sender, EventArgs e)
        {
            // Get the currently selected weapon from the cboWeapons ComboBox
            Weapon currentWeapon = (Weapon)cboWeapons.SelectedItem;

            // Determine the amount of damage to do to the monster
            int damageToMonster = RandomNumberGenerator.NumberBetween(currentWeapon.MinimumDamage, currentWeapon.MaximumDamage);

            // Apply the damage to the monster's CurrentHitPoints
            _currentMonster.CurrentHitPoints -= damageToMonster;

            // Display message
            rtbMessages.Text += "You hit the " + _currentMonster.Name + " for " + damageToMonster.ToString() + " points." + Environment.NewLine;

            // Check if the monster is dead
            if(_currentMonster.CurrentHitPoints <= 0)
            {
                // Monster is dead
                rtbMessages.Text += Environment.NewLine;
                rtbMessages.Text += "You defeated the " + _currentMonster.Name + Environment.NewLine;

                // Give player experience points for killing the monster
                _player.ExperiencePoints += _currentMonster.RewardExperiencePoints;
                rtbMessages.Text += "You receive " + _currentMonster.RewardExperiencePoints.ToString() + " experience points" + Environment.NewLine;

                // Give player gold for killing the monster 
                _player.Gold += _currentMonster.RewardGold;
                rtbMessages.Text += "You receive " + _currentMonster.RewardGold.ToString() + " gold" + Environment.NewLine;

                // Get random loot items from the monster
                List<InventoryItem> lootedItems = new List<InventoryItem>();

                // Add items to the lootedItems list, comparing a random number to the drop percentage
                foreach(LootItem lootItem in _currentMonster.LootTable)
                {
                    if(RandomNumberGenerator.NumberBetween(1, 100) <= lootItem.DropPercentage)
                    {
                        lootedItems.Add(new InventoryItem(lootItem.Details, 1));
                    }
                }

                // If no items were randomly selected, then add the default loot item(s).
                if(lootedItems.Count == 0)
                {
                    foreach(LootItem lootItem in _currentMonster.LootTable)
                    {
                        if(lootItem.IsDefaultItem)
                        {
                            lootedItems.Add(new InventoryItem(lootItem.Details, 1));
                        }
                    }
                }

                // Add the looted items to the player's inventory
                foreach(InventoryItem inventoryItem in lootedItems)
                {
                    _player.AddItemToInventory(inventoryItem.Details);

                    if(inventoryItem.Quantity == 1)
                    {
                        rtbMessages.Text += "You loot " + inventoryItem.Quantity.ToString() + " " + inventoryItem.Details.Name +Environment.NewLine;
                    }
                    else
                    {
                        rtbMessages.Text += "You loot " + inventoryItem.Quantity.ToString() + " " + inventoryItem.Details.NamePlural + Environment.NewLine;
                    }
                }

                // Refresh player information and inventory controls
                lblHitPoints.Text = _player.CurrentHitPoints.ToString();
                lblGold.Text = _player.Gold.ToString();
                lblExperience.Text = _player.ExperiencePoints.ToString();
                lblLevel.Text = _player.Level.ToString();

                UpdateInventoryListInUI();
                UpdateWeaponListInUI();
                UpdatePotionListInUI();

                // Add a blank line to the messages box, just for appearance.
                rtbMessages.Text += Environment.NewLine;

                // Move player to current location (to heal player and create a new monster to fight)
                MoveTo(_player.CurrentLocation);
            }
            else
            {
                // Monster is still alive

                // Determine the amount of damage the monster does to the player
                int damageToPlayer = RandomNumberGenerator.NumberBetween(0, _currentMonster.MaximumDamage);

                // Display message
                rtbMessages.Text += "The " + _currentMonster.Name + " did " + damageToPlayer.ToString() + " points of damage." + Environment.NewLine;

                // Subtract damage from player
                _player.CurrentHitPoints -= damageToPlayer;

                // Refresh player data in UI
                lblHitPoints.Text = _player.CurrentHitPoints.ToString();

                if(_player.CurrentHitPoints <= 0)
                {
                    // Display message
                    rtbMessages.Text += "The " + _currentMonster.Name + " killed you." + Environment.NewLine;

                    // Move player to "Home"
                    MoveTo(World.LocationByID(World.LOCATION_ID_HOME));
                }
            }
        }

 

Then, for the btnUsePotion_Click function, add this:

        private void btnUsePotion_Click(object sender, EventArgs e)
        {
            // Get the currently selected potion from the combobox
            HealingPotion potion = (HealingPotion)cboPotions.SelectedItem;

            // Add healing amount to the player's current hit points
            _player.CurrentHitPoints = (_player.CurrentHitPoints + potion.AmountToHeal);

            // CurrentHitPoints cannot exceed player's MaximumHitPoints
            if(_player.CurrentHitPoints > _player.MaximumHitPoints)
            {
                _player.CurrentHitPoints = _player.MaximumHitPoints;
            }

            // Remove the potion from the player's inventory
            foreach(InventoryItem ii in _player.Inventory)
            {
                if(ii.Details.ID == potion.ID)
                {
                    ii.Quantity--;
                    break;
                }
            }

            // Display message
            rtbMessages.Text += "You drink a " + potion.Name + Environment.NewLine;

            // Monster gets their turn to attack

            // Determine the amount of damage the monster does to the player
            int damageToPlayer = RandomNumberGenerator.NumberBetween(0, _currentMonster.MaximumDamage);

            // Display message
            rtbMessages.Text += "The " + _currentMonster.Name + " did " + damageToPlayer.ToString() + " points of damage." + Environment.NewLine;

            // Subtract damage from player
            _player.CurrentHitPoints -= damageToPlayer;

            if(_player.CurrentHitPoints <= 0)
            {
                // Display message
                rtbMessages.Text += "The " + _currentMonster.Name + " killed you." + Environment.NewLine;

                // Move player to "Home"
                MoveTo(World.LocationByID(World.LOCATION_ID_HOME));
            }

            // Refresh player data in UI
            lblHitPoints.Text = _player.CurrentHitPoints.ToString();
            UpdateInventoryListInUI();
            UpdatePotionListInUI();
        }

 

There isn’t really anything new in these two functions. Just more “if”s and “foreach”s to handle the player’s actions in battle.

 

Summary

Now you have a working game. The player can move around in the world, get quests, battle monsters, receive loot, and complete quests.

These new functions could use some refactoring, since they are long and do several things. I’ll leave that to you to figure out what refactoring you’d do.

 

Source code for this lesson

Source code on GitHub

Source code on Dropbox

 

 Next lesson: Lesson 17.1 – Running the game on another computer

Previous lesson: Lesson 16.2 – Refactoring the player movement function

All lessons: Learn C# by Building a Simple RPG Index

255 Comments

  1. Ilkka Kemppainen
    Ilkka Kemppainen July 29, 2019

    Hi Scott,

    First, thank you for the tutorial. It has been very helpful for me.

    I’ve followed the lessons up to this point and the game works otherwise, but the monsters don’t spawn at the locations. When debugging I noticed that the “newLocation.MonsterLivingHere” always stays at “null” at every location and I cannot figure out why. I’ve copied your code to World.cs, Location.cs and SuperAdventure.cs while troubleshooting, but to no avail. I’d appreciate any tips or help you can give me. Thanks.

    By the way, I use Visual Studio 2019, if that matters.

    Br,
    Ilkka

    • Scott Lilly
      Scott Lilly July 29, 2019

      You’re welcome Ilkka,

      The program should work with Visual Studio 2019.

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?

      • bilal
        bilal May 20, 2021

        i have same problem. can you tell me how did you solved the problem?

        • Scott Lilly
          Scott Lilly May 20, 2021

          Hi Bilal,

          I don’t think that person ever sent me their code to look at. If you can upload your solution files somewhere I can get them, I can look at the source code and let you know how to fix the problem.

  2. Jack Price
    Jack Price October 13, 2019

    Hello Scott. Firstly, I wanted to say thank you for making such a great tutorial for C#, I’m actually very new to OOP and coding in general, and I’m getting use to how everything works (Though I still have a long way to go.)

    But I have a problem in regards to this project. I’ve gotten to 16.3 with no errors, and seemingly everything is running just fine. However, there is one problem, no text is appearing in the rich text boxes! The UI and everything else seems to be fine, it’s just that the text boxes are completely blank. Did I mistakenly skim over a section without seeing something, or is it just a rookie mistake?

    I’m looking forward to hearing back to you. I got this far and don’t want to give up just yet 🙂

    • Scott Lilly
      Scott Lilly October 13, 2019

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?

  3. Florian Schmid
    Florian Schmid October 20, 2019

    Hello Scott, I got a problem with my Quests. I dont get the RewardGold/RewardPoints from the quests. When I fight against monsters, everything is working fine.. I tried to find a solution by myself, but I failed…

    Would be cool if you can help me out there 🙂

    • Scott Lilly
      Scott Lilly October 20, 2019

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?

      • Florian Schmid
        Florian Schmid October 21, 2019

        Ok I think I found the problem. Its not updating the counter. If I kill a monster after that, the gold+experience gets updated.. Never even thought of that… xD

        • Scott Lilly
          Scott Lilly October 23, 2019

          Hi Florian,

          Let me know if there’s anything you still want me to check the program for. I downloaded your code and can look for anything else.

  4. Isaac
    Isaac November 19, 2019

    i’ve followed every step and now copy pasted from your’s but i still have 89 errors in my code..

    • Scott Lilly
      Scott Lilly November 26, 2019

      Hi Isaac,

      I got your code and it looks like some of the classes in the Engine project are not set to “public”. For example, line 9 of Item.cs needs to be “public class Item”. Check that in HealingPotion.cs, Item.cs, Location.cs, Monster.cs, Quest.cs, RandomNumberGenerator.cs, and Weapon.cs.

      Let me know if you have any questions, or if that doesn’t eliminate the errors.

  5. Anurag Kotamraju
    Anurag Kotamraju July 22, 2020

    Line 148 throws error since it requires two parameters passed into it.
    _player.Quests.Add(new PlayerQuest(newLocation.QuestAvailableHere));

    Could you please ensure it is right?

    • Scott Lilly
      Scott Lilly July 22, 2020

      Check your PlayerQuest class against the one in lesson 10.1. The PlayerQuest class there only has one parameter in the constructor.

  6. Anurag Kotamraju
    Anurag Kotamraju July 22, 2020

    When I try to run the game in debug mode, it throws a warning saying that in PlayerQuest.cs the variable questAvailableHere is never used.

    private Quest questAvailableHere;

    • Scott Lilly
      Scott Lilly July 22, 2020

      If you still get this error after fixing the PlayerQuest class, can you let me know what class and line number the warning is pointing to?

  7. Darren
    Darren July 22, 2020

    Hi Scott, thanks for all of this … I’ve picked up a Games Design and Programming class and this project has helped speed me up in C# (I haven’t done any C-based stuff in years).

    Most things work fine, but I’m getting an error when I kill a monster and loot is generated:
    // Add the looted items to the player’s inventory
    foreach (InventoryItem inventoryItem in lootedItems)
    {
    _player.AddItemToInventory(inventoryItem.Details);

    The error is:
    An unhandled exception of type ‘System.InvalidOperationException’ occurred in mscorlib.dll
    Additional information: Collection was modified; enumeration operation may not execute.

    I’m sure it’s something small but I can’t see it … any ideas?

    • Scott Lilly
      Scott Lilly July 22, 2020

      You’re welcome, Darren!

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it? I know what the error is, but I need to see your code to see exactly why it’s happening for you.

  8. Wolf
    Wolf July 28, 2020

    Hi Scott,
    i wanted to ask this this before but…
    all people here in comment are writing how they are playing the game. How is that going?
    i have everything like you have but i cant play the screen show me the textboxes an the other things
    it sounds very stupid i know but can you help me?

    • Scott Lilly
      Scott Lilly July 28, 2020

      Hi Wolf,

      Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?

  9. JB
    JB August 17, 2020

    Added this to keep the text scrolling properly
    private void rtbMessages_TextChanged(object sender, EventArgs e)
    {
    rtbMessages.SelectionStart = rtbMessages.Text.Length;
    // scroll it automatically
    rtbMessages.ScrollToCaret();
    }

    • Scott Lilly
      Scott Lilly August 18, 2020

      Thanks for sharing that JB!

      I think we do something similar in one of the future clean-up lessons.

  10. Joseph Loya
    Joseph Loya August 21, 2020

    Hey there Scott i was following the guide and got thrown an exception a the very end of this tutorial and im really not sure what to do. I (think i) uploaded the solution to github, here is the link in case you find yourself finding the time to look into this for me, thank you. https://github.com/HermansGrave/SuperAdventure.git

    The exception states,
    “System.TypeInitializationException: ‘The type initializer for ‘Engine.World’ threw an exception.’
    Inner Exception
    NullReferenceException: Object Reference not set to an instance of an object.

    This exception was originally thrown at this call stack:
    Engine.World.PopulateQuests() in World.cs
    Engine.World.World() in World.cs”

    • Scott Lilly
      Scott Lilly August 21, 2020

      Hi Joseph,

      I don’t see any files from the solution in the GitHub repository. Can you check them, or put them on another file sharing location?

    • Scott Lilly
      Scott Lilly August 22, 2020

      Hi Joseph,

      Check line 24 of the Quest.cs class and compare it with the code in Lesson 10.1. You create a list of QuestCompletionItems, but it isn’t being populated into the QuestCompletionItems property. This cause a problem in the static World class when the Quest objects are being populated. The QuestCompletionItems property must have an empty list, in order to add items. But, right now, it’s null – which causes the error.

      Let me know if you have any questions, or if that doesn’t help fix the error.

  11. idiotnewb
    idiotnewb November 30, 2020

    i managed to stumble my inept way through your tutorials up to here, tossing out my own code to copy yours, and i couldn’t get the buttons other than “north” to do anything. i poked around at everything i could until i cound in SuperAdventureDesigner.cs there were a lot of missing system event handler calls for click, like
    this.btnUseWeapon.Click += new System.EventHandler(this.btnUseWeapon_Click);

    i managed to edit them in and it looks like everything works now! but i’m confused about how it got that way. is there something obvious that i screwed up?

    • Scott Lilly
      Scott Lilly December 2, 2020

      In the lesson when we added the buttons, I believe there was a comment to double-click them. That would have created the eventhandlers and empty functions, thank to Visual Studio being helpful. If something happened there, that might be the source of the problem.

      Eventhandlers are a tricky concept that I didn’t want to introduce early in the lessons, and that’s caused problems for many people – especially if they accidentally double-clicked something else on the form, creating events we don’t want. Now that you’ve solved the problem, you probably know what you need to know, and it will probably stick in your brain 🙂 But, there is some more on eventhandlers in lesson 21.3.

  12. Kaguyya
    Kaguyya May 21, 2021

    Hi Scott its me Kaguyya again,
    I wanted to ask for ur opinion on my function
    Is it ok to put this function for lvling and having a endless lvl cap
    Are there problems i could run into
    Or are there better ways
    I would gladly learn from it

    IN PLAYER CLASS
    public void LvlUp(Player player)
    {
    bool playerHasLvlUp = false;
    if (player.experiencePoints >amountExpToLvLUp)
    {
    playerHasLvlUp = true;

    }
    if(playerHasLvlUp)
    {
    amountExpToLvLUp *= 2;
    player.level = player.level + 1;
    }
    }
    IN SUPERADVENTUREPROGRAMM

    after //GIVE THE PLAYER EXPERIENCE POINTS

    _player.LvlUp(_player);
    lblLevel.Text = _player.level.ToString();

    Best Regards
    Kaguyya

    • Scott Lilly
      Scott Lilly May 23, 2021

      Hi Kaguyya,

      I would probably do that a little differently. If you have a public LevelUp function like that, then every time you give the play experience, it would need to be two steps: first updating the Player’s experience, and second updating the level. When you make future changes, you have to remember to do those two steps every time.

      You could make the ExperiencePoints and Level properties so the “set” is private – it can only be called from inside functions of the Player class. Then, have one public function in the Player class to manage experience points. That function will handle updating the ExperiencePoints and Level properties. And, it’s the only way any other part of the program can update ExperiencePoints and Level. Then you are sure both properties are always updated.

      Here is an example of how that could look:

      public int ExperiencePoints { get; private set; }
      public int Level { get; private set; }

      public void AddExperience(int experiencePointsToAdd)
      {
      ExperiencePoints += experiencePointsToAdd;

      Level = // Logic for calculating level would go here
      }

      Let me know if that is clear, or if you have questions.

      • Kaguyya
        Kaguyya May 23, 2021

        Hi

        I changed it as u recommended and find it alot better thanks,

        I have a question regarding the private

        What is the advtangage of having something like that private?
        Is it possible that it could get acessed randomly ?
        and is the logic part atlest fine or should i also consider making changes

        i would probably add something at at start of the function like
        if(level >= //LvL CAP)
        {return}

        Kaguyya

        • Scott Lilly
          Scott Lilly May 23, 2021

          Keeping your setters private is a way to implement “encapsulation“.

          It is helpful because if a setter is public, any class in the program can modify it. When you run the program, and when you debug it, you might have 20 places that modify the property – and you would need to look at all of them to find which one causes the problem. If the property can only be set by the functions in the class, it is easier to find where the problem might be coming from.

          This technique also makes it easier if you ever need to change the logic in the future. If the ExperiencePoints property was public, and you had a LevelUp() function, you might need call those two things in 10-20 places in your program. What if you also want to add a new thing that happens when you change the player’s ExperiencePoints? You would need to find the 10-20 places where the ExperiencePoints was updated and LevelUp() was called. Then you would have to add the new logic in those 10-20 places.

          With everything managed by one AddExperience() function, you make the change in that function and it runs every time the player’s experience points are updated.

          For the function logic, you could reduce it a little. The “playerHasLvlUp” variable is only used in one place, so you can “inline” the logic and get rid of the variable, like this:

          if (player.experiencePoints > amountExpToLvLUp)
          {
          amountExpToLvLUp *= 2;
          player.level = player.level + 1;
          }

  13. Aaron Pascua
    Aaron Pascua June 4, 2021

    Hello!

    I’m not sure if my comment posted, so I’ll do this one again just in case. But my game runs fine, except that the quests nor the monsters are spawning at all. My code is uploaded at https://github.com/aaroncpascua/CharlotteAdventures. Some variable names are different, but not that different and the monsters, items, and locations are different because I tweaked the game so it’s more personal to me. It helps me learn that way.

    Thanks for the great tutorial!

    • Scott Lilly
      Scott Lilly June 4, 2021

      You’re welcome, Aaron!

      It looks like GitHub does not have the .sln file, the .csproj files, and the files for the UI project. Can you check that? If you have a different place to upload the files, please let me know and I can get them there.

Leave a Reply

Your email address will not be published. Required fields are marked *