Lesson 3.6: Adding Monsters to Locations

Being able to create monsters is great, but now we need to put them out into our game world and be able to interact with them. To do that, we are going to build some code that adds monsters and encounters to particular locations in the game world. Then, we will enable a MonsterComponent to show a Monster when we reach a location where one is located.

Create Monster Encounter

To tie monsters to locations randomly, we are going to introduce a new class, the MonsterEncounter. Let’s create MonsterEncounter in the SimpleRPG.Game.Engine and Models folder.

namespace SimpleRPG.Game.Engine.Models
{
    public class MonsterEncounter
    {
        public MonsterEncounter(int monsterId, int chanceOfEncountering)
        {
            MonsterId = monsterId;
            ChanceOfEncountering = chanceOfEncountering;
        }

        public MonsterEncounter()
        {
        }

        public int MonsterId { get; set; }

        public int ChanceOfEncountering { get; set; }
    }
}

This class is pretty simple. It holds a monster’s id and the probability (chance) of finding the monster at a particular location. This is just a data class that we will use in our Location and WorldFactory classes.

Now, we will update the Location class to have some broader capabilities. Namely, the location will be able to track the potential monsters at that location, the ability to add monster encounters that will be used in the WorldFactory, and then the GetMonster method used by the GameSession to retrieve monsters when the player’s location changes.

using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Services;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Models
{
    public class Location
    {
        public int XCoordinate { get; set; }

        public int YCoordinate { get; set; }

        public string Name { get; set; } = string.Empty;

        public string Description { get; set; } = string.Empty;

        public string ImageName { get; set; } = string.Empty;

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

        public void AddMonsterEncounter(int monsterId, int chanceOfEncountering)
        {
            if (MonstersHere.Any(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 bool HasMonster() => MonstersHere.Any();

        public Monster GetMonster()
        {
            if (HasMonster() == false)
            {
                throw new InvalidOperationException();
            }

            // 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).
            var result = DiceService.Instance.Roll(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 (result.Value <= runningTotal)
                {
                    return MonsterFactory.GetMonster(monsterEncounter.MonsterId);
                }
            }

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

To store the MonsterEncounter objects, we added the MonstersHere property. Then we added the AddMonsterEncounter method. Rather than directly manipulating the MonstersHere list, we use the AddMonsterEncounter method to make additional checks. We only want to add one monster type to the list per location. If we add duplicate monster type ids, then we just change the ChanceOfEncountering to the new value. If the monster id is not already in the list, then we create a new MonsterEncounter and add it to the list.

The GetMonster method will determine which monster the location contains, and will instantiate a new Monster object for the player to fight. It generates a random number and picks the appropriate monster from the list.

If the method hasn’t selected a monster, the final line of the method will return the last MonsterEncounter object in the list.

Adding Monsters to the Game World

Now we need to add monsters to various locations in the WorldFactory.CreateWorld method. We will add rats to the farmer’s field, snakes to the herbalist’s garden, and spiders to the forest.

using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class WorldFactory
    {
        internal static World CreateWorld()
        {
            var locations = new List<Location>
            {
                new Location
                {
                    XCoordinate = -2,
                    YCoordinate = -1,
                    Name = "Farmer's Field",
                    Description = "There are rows of corn growing here, with giant rats hiding between them.",
                    ImageName = "/images/locations/FarmFields.png"
                },
                new Location
                {
                    XCoordinate = -1,
                    YCoordinate = -1,
                    Name = "Farmer's House",
                    Description = "This is the house of your neighbor, Farmer Ted.",
                    ImageName = "/images/locations/Farmhouse.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = -1,
                    Name = "Home",
                    Description = "This is your home.",
                    ImageName = "/images/locations/Home.png"
                },
                new Location
                {
                    XCoordinate = -1,
                    YCoordinate = 0,
                    Name = "Trading Shop",
                    Description = "The shop of Susan, the trader.",
                    ImageName = "/images/locations/Trader.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 0,
                    Name = "Town Square",
                    Description = "You see a fountain here.",
                    ImageName = "/images/locations/TownSquare.png"
                },
                new Location
                {
                    XCoordinate = 1,
                    YCoordinate = 0,
                    Name = "Town Gate",
                    Description = "There is a gate here, protecting the town from giant spiders.",
                    ImageName = "/images/locations/TownGate.png"
                },
                new Location
                {
                    XCoordinate = 2,
                    YCoordinate = 0,
                    Name = "Spider Forest",
                    Description = "The trees in this forest are covered with spider webs.",
                    ImageName = "/images/locations/SpiderForest.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 1,
                    Name = "Herbalist's Hut",
                    Description = "You see a small hut, with plants drying from the roof.",
                    ImageName = "/images/locations/HerbalistsHut.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 2,
                    Name = "Herbalist's Garden",
                    Description = "There are many plants here, with snakes hiding behind them.",
                    ImageName = "/images/locations/HerbalistsGarden.png"
                },
            };

            var newWorld = new World(locations);

            // add monsters at their particular location.
            newWorld.LocationAt(-2, -1).AddMonsterEncounter(2, 100);
            newWorld.LocationAt(2, 0).AddMonsterEncounter(3, 100);
            newWorld.LocationAt(0, 2).AddMonsterEncounter(1, 100);

            return newWorld;
        }
    }
}

With the game world set, we need to update the GameSession view model to keep track of a current monster (like we do with the CurrentPlayer). When the player moves to new location, we will check if the location has monsters. If it does, we get a monster object and set it to the CurrentMonster property. Then we will use that monster to display its information on the game screen.

First, let’s update the IGameSession interface to have the CurrentMonster property and a HasMonster property.

using SimpleRPG.Game.Engine.Models;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public interface IGameSession
    {
        Player CurrentPlayer { get; }

        Location CurrentLocation { get; }

        Monster? CurrentMonster { get; }

        bool HasMonster { get; }

        MovementUnit Movement { get; }

        void OnLocationChanged(Location newLocation);
    }
}

And then we implement the interface changes in GameSession:

using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Models;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public class GameSession : IGameSession
    {
        private readonly World _currentWorld;

        public Player CurrentPlayer { get; private set; }

        public Location CurrentLocation { get; private set; }

        public Monster? CurrentMonster { get; private set; }

        public bool HasMonster => CurrentMonster != null;

        public MovementUnit Movement { get; private set; }

        public GameSession()
        {
            CurrentPlayer = new Player
            {
                Name = "DarthPedro",
                CharacterClass = "Fighter",
                CurrentHitPoints = 10,
                MaximumHitPoints = 10,
                Gold = 1000,
                ExperiencePoints = 0,
                Level = 1
            };

            _currentWorld = WorldFactory.CreateWorld();

            Movement = new MovementUnit(_currentWorld);
            CurrentLocation = Movement.CurrentLocation;
            GetMonsterAtCurrentLocation();

            CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItem(1001));
        }

        public void OnLocationChanged(Location newLocation)
        {
            CurrentLocation = newLocation;
            GetMonsterAtCurrentLocation();
        }

        private void GetMonsterAtCurrentLocation() =>
            CurrentMonster = CurrentLocation.HasMonster() ? CurrentLocation.GetMonster() : null;
    }
}

The GetMonsterAtCurrentLocation method sets the CurrentMonster if the location has one; otherwise if gets set to null. And, when the CurrentMonster is null, the HasMonster calculated property will be false.

When we handle the OnLocationChanged event, we then call the GetMonsterAtCurrentLocation, so that when the player moves locations CurrentMonster gets set accordingly. This allows us to now update the UI and setup the combat system.

Displaying The MonsterComponent

Now that we know when there is a monster at a location, we want to show the monster at that location. If you remember the Monster class we defined in the last lesson, it has a Name and ImageName that we will display.

Let’s create the MonsterComponent in the SimpleRPG.Game and Shared folder.

@if (Monster != null)
{
<div style="border: 1px solid gainsboro; text-align: center">
    <div>@Monster.Name</div>
    <Figure>
        <FigureImage Source="@Monster.ImageName" />
        <FigureCaption><strong>Current Hit Points:</strong> @Monster.CurrentHitPoints</FigureCaption>
    </Figure>
</div>
}

@code {
    [Parameter]
    public Monster? Monster { get; set; }
}

This component is very similar to the LocationComponent we created in Lesson 2.8. We place a container <div> with a light-gray border, then display the centered monster name, image, and caption with monster’s hit points. And, we use the Blazorise <Figure> component to show an image and caption. This component is wrapped around a check of whether the Monster property is set. If there is no monster, then this component does not display anything.

In this component, we expose the Monster parameter to allow callers to specify which monster to display.

Now, we need to add the MonsterComponent to the main game screen:

@page "/"
@inject IGameSession ViewModel

<Row Margin="Margin.Is0" Style="height: 5vh; min-height: 32px">
    <Column ColumnSize="ColumnSize.Is12" Style="background-color: lightgrey">
        <Heading Size="HeadingSize.Is3">Simple RPG</Heading>
    </Column>
</Row>
<Row Margin="Margin.Is0" Style="height: 60vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Style="background-color: aquamarine">
        <PlayerComponent Player="@ViewModel.CurrentPlayer" />
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: beige">
        <Row Margin="Margin.Is2.OnY">
            <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                Game Data
            </Column>
            <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                <LocationComponent Location="@ViewModel.CurrentLocation" />
                <MonsterComponent Monster="@ViewModel.CurrentMonster" />
            </Column>
        </Row>
    </Column>
</Row>
<Row Margin="Margin.Is0" Style="height: 33vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Padding="Padding.Is2.OnY"
            Style="background-color: burlywood">
        <PlayerTabs Player="@ViewModel.CurrentPlayer" />
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: lavender">
        <Row Margin="Margin.Is2.OnY">
            <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                Combat Controls
            </Column>
            <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                <MovementComponent Movement="@ViewModel.Movement" LocationChanged="@ViewModel.OnLocationChanged" />
            </Column>
        </Row>
    </Column>
</Row>

Now when we build and run the game, and move to a location with a monster (like the herbalist’s garden), we will see a snake on the screen.

Fig 1 – Game screen with monster

With our monsters now available, we can move onto fighting these creatures and saving the village.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s