Press "Enter" to skip to content

Lesson 07.2: Adding Monsters to Locations

In this lesson, we will add the monsters to the locations.

Step 1: Create \Engine\Models\MonsterEncounter.cs

We want to have the ability to have different types of monsters at a Location, and different odds of encountering each type of monster. This will let us have “rare” monsters, that are difficult to find.

We will use this class to hold the ID of the monster, and the chance of encountering it. The “ChanceOfEncountering” values do not need to total up to a specific value. You can put in any numbers, and our “get random monster” function will handle it,

MonsterEncounter.cs
namespace Engine.Models
{
    public class MonsterEncounter
    {
        public int MonsterID { get; set; }
        public int ChanceOfEncountering { get; set; }

        public MonsterEncounter(int monsterID, int chanceOfEncountering)
        {
            MonsterID = monsterID;
            ChanceOfEncountering = chanceOfEncountering;
        }
    }
}

Step 2: Modify \Engine\Models\Location.cs

To store the MonsterEncounter objects, we add a new MonstersHere property – which is a List of MonsterEncounter objects.

I created an AddMonster function, to populate this property.

We could directly add values to the property. However, this function lets us check if the monster has already been added to the list. If it has, we change its ChanceOfEncountering value to the new value. If the monster has not already been added to the list, we create a new MonsterEncounter object, and add it to MonstersHere.

The GetMonster function will determine which monster the location has, and instantiate a new Monster object for the Player to fight.

If there are not any objects in the MonstersHere list, the function returns null.

Otherwise, if adds up all the ChancesOfEncountering, for the MonsterEncounter objects. It gets a random number between 1 and the total ChancesOfEncountering. Then, it loops through the MonsterEncounter objects until it finds the monster to select.

When it determines the random monster, it instantiates a new Mosnter object, through the MonsterFactory, and returns that object.

If the function hasn’t selected a monster, the final line of the function will return a Monster from the last MonsterEncounter object. This is just in case there is a problem. We want to make sure this function always returns a monster, if there are monsters at the location.

Location.cs
using System.Collections.Generic;
using System.Linq;
using Engine.Factories;

namespace Engine.Models
{
    public class Location
    {
        public int XCoordinate { get; set; }
        public int YCoordinate { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string ImageName { get; set; }
        public List<Quest> QuestsAvailableHere { get; set; } = new List<Quest>();

        public List<MonsterEncounter> MonstersHere { get; set; } =
            new List<MonsterEncounter>();

        public void AddMonster(int monsterID, int chanceOfEncountering)
        {
            if(MonstersHere.Exists(m => m.MonsterID == monsterID))
            {
                // This monster has already been added to this location.
                // So, overwrite the ChanceOfEncountering with the new number.
                MonstersHere.First(m => m.MonsterID == monsterID)
                            .ChanceOfEncountering = chanceOfEncountering;
            }
            else
            {
                // This monster is not already at this location, so add it.
                MonstersHere.Add(new MonsterEncounter(monsterID, chanceOfEncountering));
            }
        }

        public Monster GetMonster()
        {
            if(!MonstersHere.Any())
            {
                return null;
            }

            // Total the percentages of all monsters at this location.
            int totalChances = MonstersHere.Sum(m => m.ChanceOfEncountering);

            // Select a random number between 1 and the total (in case the total chances is not 100).
            int randomNumber = RandomNumberGenerator.NumberBetween(1, totalChances);

            // Loop through the monster list,
            // adding the monster's percentage chance of appearing to the runningTotal variable.
            // When the random number is lower than the runningTotal,
            // that is the monster to return.
            int runningTotal = 0;

            foreach(MonsterEncounter monsterEncounter in MonstersHere)
            {
                runningTotal += monsterEncounter.ChanceOfEncountering;

                if(randomNumber <= runningTotal)
                {
                    return MonsterFactory.GetMonster(monsterEncounter.MonsterID);
                }
            }

            // If there was a problem, return the last monster in the list.
            return MonsterFactory.GetMonster(MonstersHere.Last().MonsterID);
        }
    }
}

Step 3: Modify \Engine\Factories\WorldFactory.cs

Now we can add monsters to the locations, inside the CreateWorld function.

We will add rats (MonsterID = 2) to the farmer’s field (at coordinates -2, -1), snakes (MonsterID = 1) to the herbalist’s garden (at coordinates 0, 2), and spiders (MonsterID = 3) to the Spider Forest (at coordinates 2, 0).

For now, these will be the only locations with monsters, and they will only have one monster each.

WorldFactory.cs
using Engine.Models;

namespace Engine.Factories
{
    internal static class WorldFactory
    {
        internal static World CreateWorld()
        {
            World newWorld = new World();

            newWorld.AddLocation(-2, -1, "Farmer's Field",
                "There are rows of corn growing here, with giant rats hiding between them.",
                "/Engine;component/Images/Locations/FarmFields.png");

            newWorld.LocationAt(-2, -1).AddMonster(2, 100);

            newWorld.AddLocation(-1, -1, "Farmer's House",
                "This is the house of your neighbor, Farmer Ted.",
                "/Engine;component/Images/Locations/Farmhouse.png");

            newWorld.AddLocation(0, -1, "Home",
                "This is your home",
                "/Engine;component/Images/Locations/Home.png");

            newWorld.AddLocation(-1, 0, "Trading Shop",
                "The shop of Susan, the trader.",
                "/Engine;component/Images/Locations/Trader.png");

            newWorld.AddLocation(0, 0, "Town square",
                "You see a fountain here.",
                "/Engine;component/Images/Locations/TownSquare.png");

            newWorld.AddLocation(1, 0, "Town Gate",
                "There is a gate here, protecting the town from giant spiders.",
                "/Engine;component/Images/Locations/TownGate.png");

            newWorld.AddLocation(2, 0, "Spider Forest",
                "The trees in this forest are covered with spider webs.",
                "/Engine;component/Images/Locations/SpiderForest.png");

            newWorld.LocationAt(2, 0).AddMonster(3, 100);

            newWorld.AddLocation(0, 1, "Herbalist's hut",
                "You see a small hut, with plants drying from the roof.",
                "/Engine;component/Images/Locations/HerbalistsHut.png");

            newWorld.LocationAt(0, 1).QuestsAvailableHere.Add(QuestFactory.GetQuestByID(1));

            newWorld.AddLocation(0, 2, "Herbalist's garden",
                "There are many plants here, with snakes hiding behind them.",
                "/Engine;component/Images/Locations/HerbalistsGarden.png");

            newWorld.LocationAt(0, 2).AddMonster(1, 100);

            return newWorld;
        }
    }
}

Step 4: Modify \Engine\ViewModels\GameSession.cs

When the player moves to a new location, we want to see if the location has monsters. If so, we will get a monster object and display its information (and image) on the UI.

We’ll start by adding a new CurrentMonster property to hold the Monster object in the ViewModel. We want to use a property with a backing variable, so we can raise an OnPropertyChanged function – to notify the UI that there is a new monster (or no monster).

We also will add a boolean HasMonster property, so we can easily hide and show controls in the UI, depending on whether or not there is a monster for the player to fight.

Instead of using a “set” and “get” for this property, we will use an “expression body”. This is a calculated value – in this case, true (if CurrentMonster is not null) or false (if CurrentMonster is null).

Because this property does not have a backing variable, we need to raise its OnPropertyChanged event inside the CurrentMonster “set” code – like we did for the HasLocationTo properties.

When the player moves to a new location, the CurrentLocation “set” code already calls GivePlayerQuestsAtLocation(). We will add a new function “GetMonsterAtLocation”.

This new function only needs to call the GetMonster function on the CurrentLocation. If the location does not have any monsters, the function will return “null”, CurrentMonster will be set to “null”, and HasMonster will be “false”. If there are monsters at the location, GetMonster will instantiate a new Monster object, which will go into GameSession’s CurrentMonster property. When the CurrentMonster is set to a monster, HasMonster will be “true”.

GameSession.cs
using System.Linq;
using Engine.Factories;
using Engine.Models;

namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        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));

                GivePlayerQuestsAtLocation();
                GetMonsterAtLocation();
            }
        }

        public Monster CurrentMonster
        {
            get { return _currentMonster; }
            set
            {
                _currentMonster = value;

                OnPropertyChanged(nameof(CurrentMonster));
                OnPropertyChanged(nameof(HasMonster));
            }
        }

        public bool HasLocationToNorth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
            }
        }

        public bool HasLocationToEast
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public bool HasLocationToSouth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
            }
        }

        public bool HasLocationToWest
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public bool HasMonster => CurrentMonster != null;

        public GameSession()
        {
            CurrentPlayer = new Player
                            {
                                Name = "Scott",
                                CharacterClass = "Fighter",
                                HitPoints = 10,
                                Gold = 1000000,
                                ExperiencePoints = 0,
                                Level = 1
                            };

            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 GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                }
            }
        }

        private void GetMonsterAtLocation()
        {
            CurrentMonster = CurrentLocation.GetMonster();
        }
    }
}

Step 5: Modify \WPFUI\MainWindow.xaml

The last step of this lesson is to display the monster information on the screen.

Beneath the Location section, we will display the CurrentMonster’s name, image, and current hit points. We do this by creating a grid (like the one for the location section).

The StackPanel control lets us display multiple values on the same line, or in the same column. To display the monster’s current hit points, we want a label that says “Current Hit Points” on the same line (horizontally) as the monster’s current hit points. The StackPanel lets us easily display the two TextBlocks on the same line.

To make it easier to identify the different sections of the XAML file, I added comments. A XAML comment starts with “” (that’s two hyphens/dashes in there).

I also added two new lines in the “Window” section, at the top of the page.

The line with “xmlns:ViewModels” tells the Window that we are using objects in the Engine project (assembly), and in the Engine.ViewModels namespace (folder structure).

The line with “d:DataContext” lets the XAML editor know what type of object is in this window’s DataContext. With this line, we can use IntelliSense, when binding properties.

MainWindow.xaml
<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:Engine.ViewModels;assembly=Engine"
        d:DataContext="{d:DesignInstance viewModels:GameSession}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="Scott's Awesome Game" Height="768" Width="1024">

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Menu -->
        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Menu" Background="AliceBlue"/>

        <!-- Player stats -->
        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>

            <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="Class:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CharacterClass}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Hit points:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.HitPoints}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="5" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="5" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
        </Grid>

        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>

            <!-- Location information -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName}"/>

                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>

            </Border>

            <!-- Monster information -->
            <Border Grid.Row="1" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Height="Auto"
                               Text="{Binding CurrentMonster.Name}" />

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName}" />

                    <StackPanel Grid.Row="2"
                                Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock>Current Hit Points:</TextBlock>
                        <TextBlock Text="{Binding CurrentMonster.HitPoints}" />
                    </StackPanel>

                </Grid>

            </Border>

        </Grid>

        <!-- Inventory and Quests -->
        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">

            <TabControl>
                <TabItem Header="Inventory">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Inventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Price}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

                <TabItem Header="Quests">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>

        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="1"
              Background="Lavender">

            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>

            <Grid Grid.Row="0" Grid.Column="1">

                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="1"
                        Height="25" Width="65" Margin="10"
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0"
                        Height="25" Width="65" Margin="10"
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="2"
                        Height="25" Width="65" Margin="10"
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1"
                        Height="25" Width="65" Margin="10"
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>

            </Grid>

        </Grid>

    </Grid>
</Window>

Now, if you run the game, you should see monsters when the player moves to a location with a monster.

In the next lesson, we will let the player fight the monster and (hopefully) collect loot from them.

4 Comments

  1. Vlad
    Vlad 2021-07-31

    Hi! Thank you for this tutorial!
    I cant get the monsters images to appear in the game 🙁
    I’ve combed over the code and I cant find the issue.
    I fear that on the first few lessons I’ve used camelCase for a bunch of variables but I’m not sure if that’s the reason. (so far i had 0 errors following the guides)
    How can I help you help me? 😀

    • SOSCSRPG
      SOSCSRPG 2021-07-31

      Hi Vlad,

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

      • Vlad
        Vlad 2021-08-03

        The issue was not in the code but in the monster images I’ve downloaded from you 🙂
        for some reason visual studio 2019 didn’t like the alpha channel on those specific monster images (the location images work just fine)
        Thank you for the reply! and this amazing project

        • SOSCSRPG
          SOSCSRPG 2021-08-03

          Thanks for letting me know the source of the problem, Vlad!

          I’ll try to fix the alpha channel and re-upload the images.

Leave a Reply

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