Rogue C# – Building the Monster Class

In the last chapter, we did an overview of what’s required to add monsters to our roguelike game; now it’s time to actually build the class. If you’ve been following the series, you’ve seen that the definition of the class can lay the groundwork for a lot of functionality within the program so it’s important to pay close attention to the details.

This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on ComeauSoftware.com. The current code for this project is also available on Github.

What are little monsters made of?

Let’s start with the basic properties. I mentioned last time that we might be able to setup a third class that will hold the properties and functions that the player has in common with monsters. If so, I’ll do it as a refactoring after I get everything working. For now, the properties in bold italic are the same as the Player class although a couple are named differently.

// Name - Aquator, Centaur, Griffin, etc..
public string MonsterName { get; set; }

// Min and max limits for starting HP to be determined randomly.
public int MinStartingHP { get; }
public int MaxStartingHP { get; }

// Actual starting HP and subtracted damage
public int MaxHP { get; }
public int HPDamage { get; set; } = 0;
public int CurrentHP { get { return MaxHP - HPDamage; } }

public int ArmorClass { get; set; }

// Minimum and maximum level for appearance
public int MinLevel { get; set; }
public int MaxLevel { get; set; }
public int AppearancePct { get; set; }

// Minimum and maximum amount of damage to be dealt by monster.
public int MinAttackDmg { get; set; }
public int MaxAttackDmg { get; set; }
public char DisplayCharacter { get; set; }

// Function for special attack
public Func<Player>? SpecialAttack { get; set; }

Rogue was inspired partly by tabletop role playing games so a lot of stats are randomized based on “dice rolls”. A monster’s starting HP is based on a simulated roll of 8-sided dice with a number of dice specific to the monster. For example, a centaur is based on a 4d8 roll or 4, 8 sided dice so the centaur can have anywhere from 4 to 32 HP at the start. To allow for this, the class has min and max starting HP properties which can then be used to generate the actual starting HP (MaxHP). When the monster takes damage, it will be added to the HPDamage property and then CurrentHP will be calculated, just as with the Player class.

Monsters have a set armor class and don’t use armor from inventory. In the original RPGs, a lower armor class actually meant better armor but later games have reversed that to make it more intuitive. I will be assuming that armor gets stronger as the number goes higher with a general max of 10.

Specific monsters have a range of levels on which they appear and I also added a probability property (AppearancePct) which I’ll keep at 50 for all monsters for now. It might be good to adjust later for the really tough monsters.

The attack damage dealt by monsters gets a little trickier. One source claimed that some monsters theoretically have multiple attacks per turn although the game doesn’t show them separately. For example, the Jabberwock’s damage is based on dice rolls of 2d12 and 2d4 for a total potential between 4 and 32 hit points. If this game was being played on a tabletop, separate dice rolls might actually make a difference because of the physics of actual throws but when it comes to coding, combining them into one random number generation between 4 and 32 will get the same result.

Now, let’s look back to the previous chapter and some of the requirements.

Monsters can be aggressive and go after the player as soon as they see them or they can just sit there. Then we need a separate property for whether the monster is in a fighting mood at the time, such as after being provoked. This could be turned off if the monster loses sight of the player. A couple of monsters can also regenerate after attacks. Just like the player, they can be confused, paralyzed or blinded.

// Behavior
public bool Aggressive { get; set; }
public bool Angered { get; set; } = false;
public bool CanRegenerate { get; set; }
// Condition
public int Confused { get; set; } = 0;
public int Immobile { get; set; } = 0;
public int Blind { get; set; } = 0;

Monsters can hold gold and inventory. Orcs will go straight for it if they see it in the same room, Leprechauns will steal it from the player and Nymphs will steal random inventory items. This often referred to as the monster being “greedy” and I could have a property for that but I’ll try to work it into the SpecialAttack function first.

// Inventory
public int Gold {get; set;} = 0;
public List<Inventory> MonsterInventory { get; set; }

// Location on map
public MapSpace? Location { get; set; }

The main constructor for the class focuses on the essential properties that don’t have defaults set for them.

public Monster(string monsterName, int minStartingHP, int maxStartingHP,  
    int armorClass, int minLevel, int maxLevel, int appearancePct, 
    int minAttackDmg, int maxAttackDmg, char displayCharacter,  
    int specialAttackPct, Func<Player>? specialAttack, bool aggressive, 
    bool canRegenerate)
{
    MonsterName = monsterName;
    MinStartingHP = minStartingHP;
    MaxStartingHP = maxStartingHP;
    MaxHP = Game.rand.Next(this.MinStartingHP, this.MaxStartingHP + 1);
    ArmorClass = armorClass;
    MinLevel = minLevel;
    MaxLevel = maxLevel;
    AppearancePct = appearancePct;
    MinAttackDmg = minAttackDmg;
    MaxAttackDmg = maxAttackDmg;
    DisplayCharacter = displayCharacter;
    SpecialAttackPct = specialAttackPct;
    SpecialAttack = specialAttack;
    Aggressive = aggressive;
    CanRegenerate = canRegenerate;
    MonsterInventory = new List<Inventory>();
}

Drawing up the guest list

Next, I’ll add the list of monster templates and a read-only list for outside the class. I’ll just call it the “Monster Incubator” and add a couple of minor monsters here to start. You can see the full list in the official code on Github.

/// <summary>
/// Monster templates - program grabs these at random to 
/// spawn new monsters on map.
/// </summary>
private static List<Monster> monsterIncubator = new List<Monster>()
{
    new Monster("Kestral", 1, 8, 2, 1, 6, 50, 1, 4, 'K', 0, 
        null, true, false),
    new Monster("Snake", 1, 8, 2, 1, 6, 50, 1, 3, 'S', 0, 
        null, true, false)
};

/// <summary>
/// Read-only collection of monster templates.
/// </summary>
public static ReadOnlyCollection<Monster> Monsters 
    => monsterIncubator.AsReadOnly();

Finally, there’s a second constructor for creating new objects from these templates when monsters are spawned.

public Monster(Monster original)

Since specific monsters are dependent on the level and continue to spawn every 80 turns or so, the map now needs to be aware of what level the game is at. The easiest way to do this is probably to just add a MapLevel property and then pass the value when each map is created.

In the MapLevel class:

public int CurrentLevel { get; set; }

public MapLevel(int levelNumber)
{
    CurrentLevel = levelNumber;
    do
    {
        MapGeneration();
    } while (!VerifyMap());            
}

We need one or two functions to select a monster from the incubator when needed and we can pattern these after the ones in the Inventory class.

public static Monster? SpawnMonster(int LevelNumber)
{
    int itemSelect = 0;

    List<Monster> retList = (from Monster item in Monsters
                                where item.MinLevel <= LevelNumber 
                                && item.MaxLevel >= LevelNumber
                                select item).ToList();

    if (retList.Count > 0)
    {
        itemSelect = Game.rand.Next(0, retList.Count);
        return new Monster(retList[itemSelect]);
    }
    else return null;
                
}

There’s plenty of room for everyone.

So, that’s the basic Monster class. I’m sure we’ll be adding to it later. Now, we need to add the monsters to the map to keep our player company. First, we need a List in the MapLevel class to store all current monsters on the map.

public List<Monster> ActiveMonsters = new List<Monster>;

We probably don’t need more than one monster per room at the beginning of a level and the MapLevel.RoomGeneration() method can add the monster just after it adds inventory. There should be a decision about whether each room gets a monster, though.

// Probability of a monster appearing at any given point.
public const int SPAWN_MONSTER = 90;

Then, the extra code can be added to RoomGeneration() after the inventory items are populated. I also took the opportunity to make some improvements to the method overall.

// Add a monster to room based on probability.
if(rand.Next(1, 101) < SPAWN_MONSTER)
{
    do
    {
        spawned = Monster.SpawnMonster(CurrentLevel);
    } while (spawned != null && rand.Next(1, 101) <= spawned.AppearancePct);

    if (spawned != null)
    {
        // Look for an interior space. Monsters can sit on top of
        // anything within the room.
        do
        {
            openX = rand.Next(westWallX + 1, eastWallX);
            openY = rand.Next(northWallY + 1, southWallY);
            itemSpace = levelMap[openX, openY];
        } while (itemSpace.MapCharacter != ROOM_INT);

        ActiveMonsters.Add(spawned);
        itemSpace.DisplayCharacter = spawned.DisplayCharacter;
        spawned.Location = itemSpace;
    }
}

RoomGeneration() now places gold, inventory and monsters in that order. The openX and openY variables above are set to the room’s northwest corner before anything is placed and itemSpace is set to the corresponding MapSpace object. The method then changes the space as needed based on requirements. Gold and inventory cannot occupy the same space but monsters can sit on top of anything within the room. Still, the monster placement code immediately looks for a new space so it doesn’t always put a monster on the last space that was assigned inventory.

Let’s get this party started!

The monsters are in the house but they’re not doing anything besides looking ominous. Our player at least knows not to step on one since the code prevents the character from moving into the same space.

For the next step, I can either get the monsters moving or setup basic fight mechanics so the player can do some sparring. It doesn’t seem really fair to be fighting monsters that can’t move so I think I’ll see if I can get the monsters wandering the map on their own.