Since we’ll eventually add levels to monsters, and (maybe) traders, I want to move the Level property to the LivingEntity base class.
We won’t move ExperiencePoints to LivingEntity, because Monsters and Traders won’t ever gain or lose experience – only Players. However, we will encapsulate the Player’s ExperiencePoints property, and use it to update their Level, as they gain experience.
Step 1: Modify Engine\Models\LivingEntity.cs
We’ll add the Level property and its backing variable to LivingEntity (line 16 and lines 58-66). You can cut-and-paste this code from the Player class.
Make the Level setter “protected” (line 61), so it can be modified by the Player class.
Add a “level” parameter to the constructor, and have it use a default value of “1” (lines 81-82). Eventually, we will pass in values for this parameter, when constructing monsters. But, for now, the default value is good enough to let us continue.
In the constructor, set the Level property from the parameter value (line 88).
We want to change the MaximumHitPoints value when the Player gains a level. So, we need to change the MaximumHitPoints setter to protected (line 41).
LivingEntity.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Engine.Models
{
public abstract class LivingEntity : BaseNotificationClass
{
#region Properties
private string _name;
private int _currentHitPoints;
private int _maximumHitPoints;
private int _gold;
private int _level;
public string Name
{
get { return _name; }
private set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public int CurrentHitPoints
{
get { return _currentHitPoints; }
private set
{
_currentHitPoints = value;
OnPropertyChanged(nameof(CurrentHitPoints));
}
}
public int MaximumHitPoints
{
get { return _maximumHitPoints; }
protected set
{
_maximumHitPoints = value;
OnPropertyChanged(nameof(MaximumHitPoints));
}
}
public int Gold
{
get { return _gold; }
private set
{
_gold = value;
OnPropertyChanged(nameof(Gold));
}
}
public int Level
{
get { return _level; }
protected set
{
_level = value;
OnPropertyChanged(nameof(Level));
}
}
public ObservableCollection<GameItem> Inventory { get; set; }
public ObservableCollection<GroupedInventoryItem> GroupedInventory { get; set; }
public List<GameItem> Weapons =>
Inventory.Where(i => i is Weapon).ToList();
public bool IsDead => CurrentHitPoints <= 0;
#endregion
public event EventHandler OnKilled;
protected LivingEntity(string name, int maximumHitPoints, int currentHitPoints,
int gold, int level = 1)
{
Name = name;
MaximumHitPoints = maximumHitPoints;
CurrentHitPoints = currentHitPoints;
Gold = gold;
Level = level;
Inventory = new ObservableCollection<GameItem>();
GroupedInventory = new ObservableCollection<GroupedInventoryItem>();
}
public void TakeDamage(int hitPointsOfDamage)
{
CurrentHitPoints -= hitPointsOfDamage;
if(IsDead)
{
CurrentHitPoints = 0;
RaiseOnKilledEvent();
}
}
public void Heal(int hitPointsToHeal)
{
CurrentHitPoints += hitPointsToHeal;
if(CurrentHitPoints > MaximumHitPoints)
{
CurrentHitPoints = MaximumHitPoints;
}
}
public void CompletelyHeal()
{
CurrentHitPoints = MaximumHitPoints;
}
public void ReceiveGold(int amountOfGold)
{
Gold += amountOfGold;
}
public void SpendGold(int amountOfGold)
{
if(amountOfGold > Gold)
{
throw new ArgumentOutOfRangeException($"{Name} only has {Gold} gold, and cannot spend {amountOfGold} gold");
}
Gold -= amountOfGold;
}
public void AddItemToInventory(GameItem item)
{
Inventory.Add(item);
if(item.IsUnique)
{
GroupedInventory.Add(new GroupedInventoryItem(item, 1));
}
else
{
if(!GroupedInventory.Any(gi => gi.Item.ItemTypeID == item.ItemTypeID))
{
GroupedInventory.Add(new GroupedInventoryItem(item, 0));
}
GroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
}
OnPropertyChanged(nameof(Weapons));
}
public void RemoveItemFromInventory(GameItem item)
{
Inventory.Remove(item);
GroupedInventoryItem groupedInventoryItemToRemove = item.IsUnique ?
GroupedInventory.FirstOrDefault(gi => gi.Item == item) :
GroupedInventory.FirstOrDefault(gi => gi.Item.ItemTypeID == item.ItemTypeID);
if(groupedInventoryItemToRemove != null)
{
if(groupedInventoryItemToRemove.Quantity == 1)
{
GroupedInventory.Remove(groupedInventoryItemToRemove);
}
else
{
groupedInventoryItemToRemove.Quantity--;
}
}
OnPropertyChanged(nameof(Weapons));
}
#region Private functions
private void RaiseOnKilledEvent()
{
OnKilled?.Invoke(this, new System.EventArgs());
}
#endregion
}
}
Step 2: Modify Engine\Models\Player.cs
Remove Level property and its backing variable.
When the player gains a level, we’ll notify the UI with an event – OnLeveledUp (line 42).
To encapsulate the ExperiencePoints property, change its setter to “private” (line 28).
Create a new public AddExperience() function, to let the game give the player experience (lines 67-70).
Create a new SetLevelAndMaximumHitPoints() function (lines 72-84).
This function saves the original Level value (line 74), re-calculates the Level (line 76), and handles leveling up by increasing the player’s MaximumHitPoints and raising the event to notify the UI of the level change (lines 78-83).
Finally, change the ExperiencePoints setter to call this function (line 34).
Player.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Engine.Models
{
public class Player : LivingEntity
{
#region Properties
private string _characterClass;
private int _experiencePoints;
public string CharacterClass
{
get { return _characterClass; }
set
{
_characterClass = value;
OnPropertyChanged(nameof(CharacterClass));
}
}
public int ExperiencePoints
{
get { return _experiencePoints; }
private set
{
_experiencePoints = value;
OnPropertyChanged(nameof(ExperiencePoints));
SetLevelAndMaximumHitPoints();
}
}
public ObservableCollection<QuestStatus> Quests { get; set; }
#endregion
public event EventHandler OnLeveledUp;
public Player(string name, string characterClass, int experiencePoints,
int maximumHitPoints, int currentHitPoints, int gold) :
base(name, maximumHitPoints, currentHitPoints, gold)
{
CharacterClass = characterClass;
ExperiencePoints = experiencePoints;
Quests = new ObservableCollection<QuestStatus>();
}
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;
}
public void AddExperience(int experiencePoints)
{
ExperiencePoints += experiencePoints;
}
private void SetLevelAndMaximumHitPoints()
{
int originalLevel = Level;
Level = (ExperiencePoints / 100) + 1;
if (Level != originalLevel)
{
MaximumHitPoints = Level * 10;
OnLeveledUp?.Invoke(this, System.EventArgs.Empty);
}
}
}
}
Step 3: Modify Engine\ViewModels\GameSession.cs
Update the GameSession class to call the new AddExperience function, since it cannot directly update the ExperiencePoints property any longer (lines 193 and 305).
I also changed the order of some lines in CompleteQuestsAtLocation (lines 193, 196, and 203), so they call RaiseMessage before making any property changes – the new pattern we want to use, in case the function to change the property also changes something else.
Update the CurrentPlayer setter to subscribe to, and unsubscribe from, the OnLeveledUp event (lines 29 and 37).
Finally, create the OnCurrentPlayerLeveledUp() function to raise a message when the OnLeveledUp event is raised (lines 317-320).
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 Player _currentPlayer;
private Location _currentLocation;
private Monster _currentMonster;
private Trader _currentTrader;
public World CurrentWorld { get; set; }
public Player CurrentPlayer
{
get { return _currentPlayer; }
set
{
if(_currentPlayer != null)
{
_currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
_currentPlayer.OnKilled -= OnCurrentPlayerKilled;
}
_currentPlayer = value;
if (_currentPlayer != null)
{
_currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
_currentPlayer.OnKilled += OnCurrentPlayerKilled;
}
}
}
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();
CurrentTrader = CurrentLocation.TraderHere;
}
}
public Monster CurrentMonster
{
get { return _currentMonster; }
set
{
if(_currentMonster != null)
{
_currentMonster.OnKilled -= OnCurrentMonsterKilled;
}
_currentMonster = value;
if(_currentMonster != null)
{
_currentMonster.OnKilled += OnCurrentMonsterKilled;
RaiseMessage("");
RaiseMessage($"You see a {CurrentMonster.Name} here!");
}
OnPropertyChanged(nameof(CurrentMonster));
OnPropertyChanged(nameof(HasMonster));
}
}
public Trader CurrentTrader
{
get { return _currentTrader; }
set
{
_currentTrader = value;
OnPropertyChanged(nameof(CurrentTrader));
OnPropertyChanged(nameof(HasTrader));
}
}
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;
public bool HasTrader => CurrentTrader != null;
#endregion
public GameSession()
{
CurrentPlayer = new Player("Scott", "Fighter", 0, 10, 10, 1000000);
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
RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
RaiseMessage($"You receive {quest.RewardGold} gold");
CurrentPlayer.ReceiveGold(quest.RewardGold);
foreach(ItemQuantity itemQuantity in quest.RewardItems)
{
GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
RaiseMessage($"You receive a {rewardItem.Name}");
CurrentPlayer.AddItemToInventory(rewardItem);
}
// 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
{
RaiseMessage($"You hit the {CurrentMonster.Name} for {damageToMonster} points.");
CurrentMonster.TakeDamage(damageToMonster);
}
if(CurrentMonster.IsDead)
{
// Get another monster to fight
GetMonsterAtLocation();
}
else
{
// Let the monster attack
int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage);
if (damageToPlayer == 0)
{
RaiseMessage($"The {CurrentMonster.Name} attacks, but misses you.");
}
else
{
RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points.");
CurrentPlayer.TakeDamage(damageToPlayer);
}
}
}
private void OnCurrentPlayerKilled(object sender, System.EventArgs eventArgs)
{
RaiseMessage("");
RaiseMessage($"The {CurrentMonster.Name} killed you.");
CurrentLocation = CurrentWorld.LocationAt(0, -1);
CurrentPlayer.CompletelyHeal();
}
private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
{
RaiseMessage("");
RaiseMessage($"You defeated the {CurrentMonster.Name}!");
RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points.");
CurrentPlayer.AddExperience(CurrentMonster.RewardExperiencePoints);
RaiseMessage($"You receive {CurrentMonster.Gold} gold.");
CurrentPlayer.ReceiveGold(CurrentMonster.Gold);
foreach(GameItem gameItem in CurrentMonster.Inventory)
{
RaiseMessage($"You receive one {gameItem.Name}.");
CurrentPlayer.AddItemToInventory(gameItem);
}
}
private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
{
RaiseMessage($"You are now level {CurrentPlayer.Level}!");
}
private void RaiseMessage(string message)
{
OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message));
}
}
}
Step 4: Test the game.
Fight some monsters and check that the player’s Level changes to “2” when they have 100 experience points. Continue fighting until the player dies and moves to their home. Check that CompletelyHeal healed them to 20 hit points – the new MaximumHitPoints for a level 2 player.
NEXT LESSON: Lesson 10.6: Clean up property setters and PropertyChanged notifications
PREVIOUS LESSON: Lesson 10.4: Bug Fix – Removing multiple items from GroupedInventory