Tag Archives: Log

RogueSharp V3 Tutorial – Simple Combat

Next Tutorial Post – Scheduling System
Previous Tutorial Post – Monster Stats

Goal

During this tutorial we will setup a basic system for combat. At this point only the Player will be able to attack and they will be limited to a single type of attack when trying to move into a space that is occupied by a monster. Dice will be rolled and all the results will be printed to the message log. This is often referred to as “bump attack” because the player bumps into a monster while moving.

SimpleCombat

Simple Combat

Combat Overview

The combat system was covered in the past but I’m going to repeat some of that here for reference. For this tutorial I decided combat would be a percentage based opposing roll system. First the attacker will roll a number of 100 sided dice equal to their attack value. Each die in the roll will have a percentage chance to be a success. Each success has the potential to inflict 1 point of damage. Thus the maximum amount of damage an actor can do is their Attack value, if they get all successes and their is no defense.

So how about defense? It will work the same way. The defender rolls a number of 100 sided dice equal to their Defense value. Each die has a percentage chance to be a success. The number of defender successes is subtracted from the number of attacker successes and if the remaining number is positive then that much damage is applied to the defender.

Important Stats for Combat

  • Attack – int – Number of dice to roll when performing an attack. Also represents the maximum amount of damage that can be inflicted in a single attack.
  • AttackChance – int – Percentage chance that each die rolled is a success. A success for a die means that 1 point of damage was inflicted.
  • Defense– int – Number of dice to roll when defending against an attack. Also represents the maximum amount of damage that can blocked or dodged from an incoming attack.
  • DefenseChance – int – Percentage chance that each die rolled is a success. A success for a die means that 1 point of damage was blocked.
  • Health – int – The current health total of the actor. If this value is 0 or less then the actor is killed.
  • MaxHealth – int – How much health the actor has when fully healed.

Additional DungeonMap Methods

Before we start rolling dice we need to add a couple more methods to DungeonMap.cs to handle getting a monster at a particular position, and removing a monster from the map. We already have a method AddMonster() so lets put the new methods directly below that. Open DungeonMap.cs and add the following code.

public void RemoveMonster( Monster monster )
{
  _monsters.Remove( monster );
  // After removing the monster from the map, make sure the cell is walkable again
  SetIsWalkable( monster.X, monster.Y, true );
}

public Monster GetMonsterAt( int x, int y )
{
  return _monsters.FirstOrDefault( m => m.X == x && m.Y == y );
}

Adding the Attack Command

Now for the fun part, the actual combat. Attacking will be another command just like moving so we will add the code to CommandSystem.cs. The first thing we need to do is update the MovePlayer() method to use our new DungeonMap.GetMonsterAt( x, y ) method to see if there is a monster in the Cell we are trying to move into. When there is then instead of moving we will attack it.

Add the following code to the MovePlayer() method in CommandSystem.cs after the if block where we try to SetActorPosition. This is also after we have established our x and y position based on the direction the player is trying to move.

Monster monster = Game.DungeonMap.GetMonsterAt( x, y );

if ( monster != null )
{
  Attack( Game.Player, monster );
  return true;
}

Now that is in place, lets make our Attack() method.

public void Attack( Actor attacker, Actor defender )
{
  StringBuilder attackMessage = new StringBuilder();
  StringBuilder defenseMessage = new StringBuilder();

  int hits = ResolveAttack( attacker, defender, attackMessage );

  int blocks = ResolveDefense( defender, hits, attackMessage, defenseMessage );

  Game.MessageLog.Add( attackMessage.ToString() );
  if ( !string.IsNullOrWhiteSpace( defenseMessage.ToString() ) )
  {
    Game.MessageLog.Add( defenseMessage.ToString() );
  }

  int damage = hits - blocks;

  ResolveDamage( defender, damage );
}

// The attacker rolls based on his stats to see if he gets any hits
private static int ResolveAttack( Actor attacker, Actor defender, StringBuilder attackMessage )
{
  int hits = 0;

  attackMessage.AppendFormat( "{0} attacks {1} and rolls: ", attacker.Name, defender.Name );

  // Roll a number of 100-sided dice equal to the Attack value of the attacking actor
  DiceExpression attackDice = new DiceExpression().Dice( attacker.Attack, 100 );
  DiceResult attackResult = attackDice.Roll();

  // Look at the face value of each single die that was rolled
  foreach ( TermResult termResult in attackResult.Results )
  {
    attackMessage.Append( termResult.Value + ", " );
    // Compare the value to 100 minus the attack chance and add a hit if it's greater
    if ( termResult.Value >= 100 - attacker.AttackChance )
    {
      hits++;
    }
  }

  return hits;
}

// The defender rolls based on his stats to see if he blocks any of the hits from the attacker
private static int ResolveDefense( Actor defender, int hits, StringBuilder attackMessage, StringBuilder defenseMessage )
{
  int blocks = 0;

  if ( hits > 0 )
  {
    attackMessage.AppendFormat( "scoring {0} hits.", hits );
    defenseMessage.AppendFormat( "  {0} defends and rolls: ", defender.Name );

    // Roll a number of 100-sided dice equal to the Defense value of the defendering actor
    DiceExpression defenseDice = new DiceExpression().Dice( defender.Defense, 100 );
    DiceResult defenseRoll = defenseDice.Roll();

    // Look at the face value of each single die that was rolled
    foreach ( TermResult termResult in defenseRoll.Results )
    {
      defenseMessage.Append( termResult.Value + ", " );
      // Compare the value to 100 minus the defense chance and add a block if it's greater
      if ( termResult.Value >= 100 - defender.DefenseChance )
      {
        blocks++;
      }
    }
    defenseMessage.AppendFormat( "resulting in {0} blocks.", blocks );
  }
  else
  {
    attackMessage.Append( "and misses completely." );
  }

  return blocks;
}

// Apply any damage that wasn't blocked to the defender
private static void ResolveDamage( Actor defender, int damage )
{
  if ( damage > 0 )
  {
    defender.Health = defender.Health - damage;

    Game.MessageLog.Add( $"  {defender.Name} was hit for {damage} damage" );

    if ( defender.Health <= 0 )
    {
      ResolveDeath( defender );
    }
  }
  else
  {
    Game.MessageLog.Add( $"  {defender.Name} blocked all damage" );
  }
}

// Remove the defender from the map and add some messages upon death.
private static void ResolveDeath( Actor defender )
{
  if ( defender is Player )
  {
    Game.MessageLog.Add( $"  {defender.Name} was killed, GAME OVER MAN!" );
  }
  else if ( defender is Monster )
  {
    Game.DungeonMap.RemoveMonster( (Monster) defender );

    Game.MessageLog.Add( $"  {defender.Name} died and dropped {defender.Gold} gold" );
  }
}

That’s a lot of code but I tried to add some comments to help clarify. It should read very similar to the Combat Overview section above. You’ll notice that instead of having one gigantic public method with 100 lines of code we extracted some private methods. I like doing this because you can quickly look at the public method and see what it is doing without having to know all of the details. Resolve the attack rolls, resolve the defense rolls, write the results to the message log, and resolve any damage that was sustained.

Giving the private methods clear names for their purpose can help with readability. Someone who doesn’t care about the internals can just skip over those, or if they do need to see the details of what it means to resolve an attack, they dive in and look at that private method.

You might notice that in the death message it mentions dropping gold. We haven’t actually implemented that yet but we’ll get to it soon. I don’t mind having the message in place and ready to go for when we do.

If the dice expressions seem a little tricky here you can read more about them here:

Run the program now and you should be able to move around the map and bump into monsters to damage them.

As always the code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/12SimpleCombat

Closing Thoughts

When you are going through the code examples here, please don’t feel like you have to type them in letter for letter. For example in this tutorial try changing the messages that get added to the log to make them more interesting. Create additional stats for actors and make up your own more exciting combat system. Rename and reorganize anything and everything you want so that it makes sense to you.

The code presented in this tutorial series is far from perfect. I am finding lots of issues that bug me every time I go back to write one of these posts. The point is that the examples I provide here are just one way of doing things. Get some ideas here and then visit other blogs and read other tutorials to collect more ideas. Eventually you’ll settle upon what works best for you and your own unique style.

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.

Advertisements

RogueSharp V3 Tutorial – Message Log

Next Tutorial Post – Player Stats
Previous Tutorial Post – Connecting Rooms with Hallways

Goal

During this tutorial we’ll create a new MessageLog class that will allow us to add messages and draw them to a console. We’ll then add a bunch of messages to the log to prove that it works

MessageLog

Message Log

Designing the Message Log Class

How do we want to handle messages? We want a class that allows us to add messages to some sort of list. We also want to display some number of previous messages and have new messages scroll into view when they are added. Old messages after a certain limit should scroll out of view.

A decent structure for handling this is a Queue. You can read more about them from the link but basically you can think of it as a line at the grocery store. The first person to get in line is the first person to check out. In terms of our message log the oldest message will be the first one removed from the history.

Creating the Message Log Class

Create a new class in the Systems folder called MessageLog.cs. Add the following code to it:

// Represents a queue of messages that can be added to
// Has a method for and drawing to an RLConsole
public class MessageLog
{
  // Define the maximum number of lines to store
  private static readonly int _maxLines = 9;

  // Use a Queue to keep track of the lines of text
  // The first line added to the log will also be the first removed
  private readonly Queue<string> _lines;

  public MessageLog()
  {
    _lines = new Queue<string>();
  }

  // Add a line to the MessageLog queue
  public void Add( string message )
  {
    _lines.Enqueue( message );
    
    // When exceeding the maximum number of lines remove the oldest one.
    if ( _lines.Count > _maxLines )
    {
      _lines.Dequeue();
    }
  }

  // Draw each line of the MessageLog queue to the console
  public void Draw( RLConsole console )
  {
    console.Clear();
    string[] lines = _lines.ToArray();
    for ( int i = 0; i < lines.Length; i++ )
    {
      console.Print( 1, i + 1, lines[i], RLColor.White );
    }
  }
}

Hooking up the Message Log

All of the remaining work will be handled in Game.cs.

First in the section at the top of the file where we define Player, DungeonMap and CommandSystem add the following:

public static MessageLog MessageLog { get; private set; }

Next in the Main() method of Game.cs add some code to instantiate a new MessageLog and add a couple of messages to it. Don’t forget to also remove the old code that was setting the _messageConsole to a blue color and printing “Messages” on it.

// Create a new MessageLog and print the random seed used to generate the level
MessageLog = new MessageLog();
MessageLog.Add( "The rogue arrives on level 1" );
MessageLog.Add( $"Level created with seed '{seed}'" );

// Remove these lines:
_messageConsole.SetBackColor( 0, 0, _messageWidth, _messageHeight, Swatch.DbDeepWater );
_messageConsole.Print( 1, 1, "Messages", Colors.TextHeading );

The last thing we need to do is call MessageLog.Draw() in our OnRootConsoleRender() method near where we call DungeonMap.Draw() and Player.Draw().

MessageLog.Draw( _messageConsole );

Temporary Code for Generating Lots of Messages

Just to prove that our messages work the way we expect and remove old messages once we reach the limit we set of “9” lets log a bunch of messages. We’ll do this by adding a private member variable _steps and increment it each time the player acts.

// Temporary member variable just to show our MessageLog is working
private static int _steps = 0;

// In OnRootConsoleUpdate() replace the if ( didPlayerAct ) block
if ( didPlayerAct )
{
  // Every time the player acts increment the steps and log it
  MessageLog.Add( $"Step # {++_steps}" );  
  _renderRequired = true;
}

The code for the tutorial series so far can be found on Bitbucket:
https://bitbucket.org/FaronBracy/roguesharpv3tutorial/commits/tag/08MessageLog

Closing Thoughts

I’ve been spending a bunch of time messing with RexPaint. It’s a fantastic ASCII art editor and can be used to design maps. I highly recommend you take a look at it.

Bored waiting for the next tutorial post? The completed project is already available on Bitbucket.