Press "Enter" to skip to content

Lesson 08.1: Completing Quests

In this lesson, we’re going to check if the player can complete any quests when they move to a location with quests. If they, we’ll remove the quest completion items from their inventory and give them the quest rewards.

Step 1: Modify Engine\Models\Player.cs

To complete a quest, the player must turn in some items. So, we will need to remove items from the player’s inventory.

We want to do this through a function, just like we do when we add items to the player’s inventory. This lets us notify the UI of the changes.

I’ve added a new function for this, RemoveItemsFromInventory (on lines 100 through 105).

We also need a function to check if the player has all the items required to complete the quest. The new function HasAllTheseItems (on lines 107 through 118) accepts a list of ItemQuantity objects and looks through the player’s inventory.

If the count of items is less than the number required in the parameter, the function returns “false” – the player does not have all the items. If the player has a large enough quantity, for all the items passed into the function, it will return “true”.

Player.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Engine.Models
{
    public class Player : BaseNotificationClass
    {
        #region Properties
        private string _name;
        private string _characterClass;
        private int _hitPoints;
        private int _experiencePoints;
        private int _level;
        private int _gold;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
        public string CharacterClass
        {
            get { return _characterClass; }
            set
            {
                _characterClass = value;
                OnPropertyChanged(nameof(CharacterClass));
            }
        }
        public int HitPoints
        {
            get { return _hitPoints; }
            set
            {
                _hitPoints = value;
                OnPropertyChanged(nameof(HitPoints));
            }
        }
        public int ExperiencePoints
        {
            get { return _experiencePoints; }
            set
            {
                _experiencePoints = value;
                OnPropertyChanged(nameof(ExperiencePoints));
            }
        }
        public int Level
        {
            get { return _level; }
            set
            {
                _level = value;
                OnPropertyChanged(nameof(Level));
            }
        }
        public int Gold
        {
            get { return _gold; }
            set
            {
                _gold = value;
                OnPropertyChanged(nameof(Gold));
            }
        }
        public ObservableCollection<GameItem> Inventory { get; set; }
        public List<GameItem> Weapons => 
            Inventory.Where(i => i is Weapon).ToList();
        public ObservableCollection<QuestStatus> Quests { get; set; }
        #endregion
        public Player()
        {
            Inventory = new ObservableCollection<GameItem>();
            Quests = new ObservableCollection<QuestStatus>();
        }
        public void AddItemToInventory(GameItem item)
        {
            Inventory.Add(item);
            OnPropertyChanged(nameof(Weapons));
        }
        public void RemoveItemFromInventory(GameItem item)
        {
            Inventory.Remove(item);
            OnPropertyChanged(nameof(Weapons));
        }
        public bool HasAllTheseItems(List<ItemQuantity> items)
        {
            foreach (ItemQuantity item in items)
            {
                if (Inventory.Count(i => i.ItemTypeID == item.ItemID) < item.Quantity)
                {
                    return false;
                }
            }
            return true;
        }
    }
}

Step 2: Modify Engine\ViewModels\GamesSession.cs

First, we will let the player know when they receive a new quest, and what the details of the quest are.

Inside the GivePlayerQuestsAtLocation function (line 176), we’ll raise messages (for the UI to display) letting the player know the name and description of the quest, the items they need to return with, and the rewards they will receive when they complete the quest.

Second, we’ll modify the CurrentLocation setter. On line 34, I’ve added a call to a new function – CompleteQuestsAtLocation(). So, when the player moves to a new location, this function will be called, and we will see if the player can complete the quests at that location.

Finally, we’ll write the code for CompletQuestsAtLocation (lines 130 through 174).

We loop through all the quests available at this location, starting on line 132. If this location does not have any quests, we will loop zero times.

On lines 134 and 135, we get the first quest from the player’s Quests property where the quest ID matches the location’s quest’s ID, and the player has not already completed.

If the player does not have a quest that matches this criteria, “questToComplete” will be null.

If “questToComplete” is not null (the player has this quest, and has not completed it), we check if the player has all the items needed to complete the quest (the “if” section, starting on line 140).

If the player has all the quest completion items, we remove them from the player’s inventory (lines 143 through 149). Then, we give the player the rewards for completing the quest (lines 155 through 167).

Finally, on line 170, we mark the quest as completed.

GameSession.cs
using System;
using System.Linq;
using Engine.EventArgs;
using Engine.Factories;
using Engine.Models;
namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        public event EventHandler<GameMessageEventArgs> OnMessageRaised;
        #region Properties
        private Location _currentLocation;
        private Monster _currentMonster;
        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;
                OnPropertyChanged(nameof(CurrentLocation));
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));
                CompleteQuestsAtLocation();
                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();
            }
        }
        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                _currentMonster = value;
                OnPropertyChanged(nameof(CurrentMonster));
                OnPropertyChanged(nameof(HasMonster));
                if (CurrentMonster != null)
                {
                    RaiseMessage("");
                    RaiseMessage($"You see a {CurrentMonster.Name} here!");
                }
            }
        }
        public Weapon CurrentWeapon { get; set; }
        public bool HasLocationToNorth =>
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
        public bool HasLocationToEast => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
        public bool HasLocationToSouth => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
        public bool HasLocationToWest => 
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
        public bool HasMonster => CurrentMonster != null;
        #endregion
        public GameSession()
        {
            CurrentPlayer = new Player
                            {
                                Name = "Scott",
                                CharacterClass = "Fighter",
                                HitPoints = 10,
                                Gold = 1000000,
                                ExperiencePoints = 0,
                                Level = 1
                            };
            if (!CurrentPlayer.Weapons.Any())
            {
                CurrentPlayer.AddItemToInventory(ItemFactory.CreateGameItem(1001));
            }
            CurrentWorld = WorldFactory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }
        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }
        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }
        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }
        private void CompleteQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
                                                             !q.IsCompleted);
                if(questToComplete != null)
                {
                    if(CurrentPlayer.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        // Remove the quest completion items from the player's inventory
                        foreach (ItemQuantity itemQuantity in quest.ItemsToComplete)
                        {
                            for(int i = 0; i < itemQuantity.Quantity; i++)
                            {
                                CurrentPlayer.RemoveItemFromInventory(CurrentPlayer.Inventory.First(item => item.ItemTypeID == itemQuantity.ItemID));
                            }
                        }
                        RaiseMessage("");
                        RaiseMessage($"You completed the '{quest.Name}' quest");
                        // Give the player the quest rewards
                        CurrentPlayer.ExperiencePoints += quest.RewardExperiencePoints;
                        RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
                        CurrentPlayer.Gold += quest.RewardGold;
                        RaiseMessage($"You receive {quest.RewardGold} gold");
                        foreach(ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
                            CurrentPlayer.AddItemToInventory(rewardItem);
                            RaiseMessage($"You receive a {rewardItem.Name}");
                        }
                        // Mark the Quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }
        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                    RaiseMessage("");
                    RaiseMessage($"You receive the '{quest.Name}' quest");
                    RaiseMessage(quest.Description);
                    RaiseMessage("Return with:");
                    foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                    RaiseMessage("And you will receive:");
                    RaiseMessage($"   {quest.RewardExperiencePoints} experience points");
                    RaiseMessage($"   {quest.RewardGold} gold");
                    foreach(ItemQuantity itemQuantity in quest.RewardItems)
                    {
                        RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                }
            }
        }
        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }
        public void AttackCurrentMonster()
        {
            if (CurrentWeapon == null)
            {
                RaiseMessage("You must select a weapon, to attack.");
                return;
            }
            // Determine damage to monster
            int damageToMonster = RandomNumberGenerator.NumberBetween(CurrentWeapon.MinimumDamage, CurrentWeapon.MaximumDamage);
            if (damageToMonster == 0)
            {
                RaiseMessage($"You missed the {CurrentMonster.Name}.");
            }
            else
            {
                CurrentMonster.HitPoints -= damageToMonster;
                RaiseMessage($"You hit the {CurrentMonster.Name} for {damageToMonster} points.");
            }
            // If monster if killed, collect rewards and loot
            if(CurrentMonster.HitPoints <= 0)
            {
                RaiseMessage("");
                RaiseMessage($"You defeated the {CurrentMonster.Name}!");
                CurrentPlayer.ExperiencePoints += CurrentMonster.RewardExperiencePoints;
                RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points.");
                CurrentPlayer.Gold += CurrentMonster.RewardGold;
                RaiseMessage($"You receive {CurrentMonster.RewardGold} gold.");
                foreach(ItemQuantity itemQuantity in CurrentMonster.Inventory)
                {
                    GameItem item = ItemFactory.CreateGameItem(itemQuantity.ItemID);
                    CurrentPlayer.AddItemToInventory(item);
                    RaiseMessage($"You receive {itemQuantity.Quantity} {item.Name}.");
                }
                // Get another monster to fight
                GetMonsterAtLocation();
            }
            else
            {
                // If monster is still alive, let the monster attack
                int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage);
                if (damageToPlayer == 0)
                {
                    RaiseMessage("The monster attacks, but misses you.");
                }
                else
                {
                    CurrentPlayer.HitPoints -= damageToPlayer;
                    RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points.");
                }
                // If player is killed, move them back to their home.
                if (CurrentPlayer.HitPoints <= 0)
                {
                    RaiseMessage("");
                    RaiseMessage($"The {CurrentMonster.Name} killed you.");
                    CurrentLocation = CurrentWorld.LocationAt(0, -1); // Player's home
                    CurrentPlayer.HitPoints = CurrentPlayer.Level * 10; // Completely heal the player
                }
            }
        }
        private void RaiseMessage(string message)
        {
            OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
        }
    }
}

Step 3: Run the program, and test the changes.

NEXT LESSON: Lesson 09.1: Creating Traders

PREVIOUS LESSON: Lesson 07.5: Monster and Combat Refactoring

8 Comments

  1. James
    James 2022-02-15

    When I get my rewards for completing a quest, I get an Error in Visual Studio output window.
    System.Windows.Data Error: 4 : Cannot find source for binding with reference ‘RelativeSource FindAncestor, AncestorType=’System.Windows.Controls.ItemsControl’, AncestorLevel=’1”. BindingExpression:Path=VerticalContentAlignment; DataItem=null; target element is ‘ComboBoxItem’ (Name=”); target property is ‘VerticalContentAlignment’ (type ‘VerticalAlignment’)

    • SOSCSRPG
      SOSCSRPG 2022-02-15

      Hi James,

      Is the program working correctly, even with this error message? If not, please let me know and I can investigate this problem deeper.

      These types of messages are normally not really “errors”. XAML tends to show these when it doesn’t have a default value to use. There is more information on how to get rid of these messages here: https://stackoverflow.com/questions/47391020/cannot-find-source-for-binding-with-reference-relativesource-findancestor

      I made a note to possibly go through code that may have these errors and change them.

      • James
        James 2022-02-16

        program seems to run but it is the OCD in me that wants to dig in and figure this out.

        I did see another error in Lesson 7.4 but that was fixed by putting the correct name in the MainWindow.xaml file ComboBox SelectedValuePath from “ID” to “ItemTypeID” as it is labeled in GameItem.
        runtime System.Windows.Data Error: 40 : BindingExpression path error: with how the combo-box is initialized

        Is there a way to use binding to get the NameOf() like function in XAML to get a value of property in a class, ie CurrentWeapon.Name?

        Thanks

        • SOSCSRPG
          SOSCSRPG 2022-02-16

          Hi James,

          I saw your other comment with the code you added. Unfortunately, code usually gets removed from comments – to prevent security problems.

          If you want to post the code, the best way is probably something like posting it at https://gist.github.com/ or https://pastebin.com/ and providing a link to the gist in your comment.

  2. Mark
    Mark 2023-08-10

    Hi,

    Everything seems to be working but the quest “Done?” column is not updating when QuestStatus.IsCompleted is set to true.

    I have ran through it with the debugger and verified that IsCompleted is indeed being set to true. I can’t think of what else I have missed.

    Thanks,

    Mark

    • SOSCSRPG
      SOSCSRPG 2023-08-11

      Hi Mark,

      I think this might be something we fix in a later lesson, but the XAML needs to know how it it bound to the property, so it knows to expect receiving changes from the property, and not the default of the UI sending changes to the property.

      This is done with the binding mode. In this case, we can use TwoWay, so the XAML knows it can get changes from the backing object/property. You can read about this here: https://stackoverflow.com/questions/8557433/bind-xaml-elements-to-entities-twoway

      Please let me know if that wasn’t clear, or if it doesn’t solve the problem.

  3. Alex
    Alex 2024-02-02

    Hi! Regarding the real-time mode. How is the linking of a “engine part” to the View happens. Lets take for example not a game but very simple Stopwatch app. In the “Engine assembly” there is a timer logic that just mesures elapsed time. And Start and Stop actions.
    And the view gets the elapsed time 60 times per second.

    I tried to do it sometime ago. But was a bit confused most sources said something about DispatcherObject, but also stated it was outdated.
    How would you do it in modern WPF? Could you tell in short what steps and what objects to use for logice in separated thread?

    • SOSCSRPG
      SOSCSRPG 2024-02-04

      Since I’m not a game programmer, there may be better ways to do this than what I’m thinking of. But, here is how I’d start.

      The Engine class would have a static (or something that uses the Singleton design pattern to only ever have one instance) class that holds a collection of GameEvent objects. Anything in the Engine project can add to the collection, do something with an object in the collection, or remove something from the collection. Since this is in the Engine project, the UI project could also use this GameEventCollection object. This might be done with the pub/sub (publish/subscribe) design pattern. The engine would publish a GameEvent (add it to the collection) and the UI would subscribe to them (watch them to view for updates).

      A GameEvent object would implement the Command design pattern. It’s an object that does some sort of action. We add a Command object later in this project, to handle attacking.

      As an example, think of cooking something in an oven in Minecraft. You would create a Cook Command class. It’s properties are things like the oven, the food in the oven, and the fuel in the oven. When the player wants to cook something, the game adds this Cook object to the GameEventCollection, with its properties. The Cook class (and all GameEvent classes) would have an OnGameTick function. The GameEvent classes would probably also have an integer NumberOfTicksPassed property that starts out as zero.

      So, every time the game timer ticks, the game loop looks at all the GameEvent objects in the GameEventCollection and runs their OnGameTick function. The first thing it would do is add 1 to the NumberOfTicksPassed property, then it would run the specific code for its OnGameTick function. The Cook class would probably check to see if enough ticks have passed for the food to be finished cooking. If so, it might raise a GameEventCompleted notification for the UI to handle. Then, the GameEvent object would probably be removed from the GameEventCollection (there might be some timing issues with this). If enough ticks have not passed, OnGameTick might decrease the amount of fuel in the oven. If the fuel quantity reaches zero, it stops “cooking” until the player adds more fuel to the oven.

      I think that’s roughly how you would do it, but it might be best to see if you can find code where someone has actually done this. Their are probably things I didn’t think of that would also need to be handled.

Leave a Reply

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