The Legend of the Tomato Quest Tutorial - Combat

From SwinGame

This part of the tutorial will focus entirely on the combat of the RPG Game. So our character is able to attack, and our enemies can also attack. But when their swords hit their enemies, nothing happens... yet.

This page contains a Tutorial. Tutorials are designed to walk you through the development of a small game.

Contents

Hit Sounds

Every sword needs a sound that plays when you hit an enemy, and for a cool effect, we'll have a special sound for when a combatant scores a critical hit.

Download the following sounds.zip file, and extract both Critical.wav and Hit.wav to the Resources\sounds folder in your project directory.

Sounds.zip

Now open the GameResources.cs sourcefile and add the following lines.

private static void LoadSounds()
{
    NewSound("Swing", "Swing.wav");
    NewSound("Heal", "Heal.wav");
 
    //ADD THESE LINES OF CODE
    NewSound("Critical", "Critical.wav");
    NewSound("Hit", "Hit.wav");
}

Now we have a Critical Hit sound, and a normal Hit sound.

Damaging a Character

The next step to getting combat working, is to create a method that damages an character. But before that, we need to make a change to our DamageType enumeration.

In the Character.cs source file, modify the DamageType enumeration to look like the following.

public enum DamageType
{
    Heal,
    Enemy,
    Player,
    Critical,
        
    //ADD THIS LINE
    Evade,
 
    None,
}

We also need to add some code to the UpdateCharacterStatus() method. Add the following lines of code to the UpdateCharacterStatus() in the Characters class within the Character.cs source file.

...
 
//Player Damage
case DamageType.Player:
    Text.DrawText(Convert.ToString(Damage),
        Color.White, Resources.GameFont("Arial"),
        (int)_Sprite.xPos, (int)_Sprite.yPos - 80 + StatusCooldown);
    break;
 
//ADD THIS CASE
//Evade an attack
case DamageType.Evade:
    Text.DrawText("Evaded",
        Color.White, Resources.GameFont("Arial"),
        (int)_Sprite.xPos, (int)_Sprite.yPos - 80 + StatusCooldown);
    break;
 
...

Now our characters can evade attacks if they are lucky enough.

Now let's move onto the actual method that will damage our characters.

Add the following method to the Characters class in the Character.cs source file

public void DamageCharacter(int healthamount, DamageType type)
{
    //Damage Character
    Health = Health - healthamount;
 
    //Set the damage change
    Damage = healthamount;
 
    //Set the damage type
    DamageType = type;
 
    //Set the status change cooldown
    StatusCooldown = 60;
}

This code is almost identical to the code for the HealCharacter() method, except that we have to pass in what DamageType has occured, since damage could be from the Player, Enemy, or a Critical Hit. The attack could also have been evaded.

First we damage the character health, by subtracting the healthamount pass in from the character's current health. We then store the amount of damage that has occured, and store DamageType that occured as well, so we can use both for our player's notification.

Lastly, we set the StatusCooldown to 60, so the notification doesn't spam.

Setting up our Characters

We need to make a quick change to the RefreshCharacterStats() method. Add the following line of code to the RefreshCharacterStats() method in the Characters class in the Character.cs source file:

...
 
//if the players experience is equal or greater then the next experience level, character gains a new level.
if (Experience >= ExperienceNextLevel)
{
    //Increase level by 1
    Level++;
 
    //Find the remaining experience to carry over to the next level, and set characters experience to it.
    Experience = Experience - ExperienceNextLevel;
 
    //Increase the amount of experience needed to get the next level
    ExperienceNextLevel = (int)(ExperienceNextLevel * 1.5);
 
    //Give the Character some stat Points
    StatPoints = StatPoints + 5;
 
    //Give the Character a skill point
    SkillPoints++;
 
    //ADD THIS LINE OF CODE
    //Give the Character his health back
    Health = MaxHealth;
 
    //ADD THIS LINE OF CODE
    //Give the Character his mana back
    Mana = MaxMana;
}
 
...

What this will do, is give the Player his full health and mana back when he grow's a level.

Random Number

For our Critical Hits, and Evasion, we need the use of random numbers. This is easy to implement.

Add the following field to the Game class in Game.cs

private Random _RandomNumber = new Random(System.DateTime.Now.Millisecond);

This will create an object that will give us random numbers. We pass in the System.DateTime.Now.Millisecond as the parameter, because if we do, if helps make sure that our numbers are always different.

Combat

We need to make a new source file now. Create a source file called Combat.cs and add the following code as a template.

using System;
using System.Drawing;
using System.Collections.Generic;
 
using SwinGame;
using Graphics = SwinGame.Graphics;
using Bitmap = SwinGame.Bitmap;
using Font = SwinGame.Font;
using FontStyle = SwinGame.FontStyle;
using Event = SwinGame.Event;
using CollisionSide = SwinGame.CollisionSide;
using Sprite = SwinGame.Sprite;
 
using GameResources;
 
namespace TomatoQuest
{
    public static class Combat
    {
    }
}

This will serve as a template for our combat

Critical Hits and Evasion

Now we need to create a function that will determine whether the Character has scored a critical hit.

Add the following method to the Combat class in Combat.cs source file:

private static bool CheckCritical(Character attacker, Random rnd)
{
    //Get a random number, representing 0 to 100%
    int roll = rnd.Next(100);
 
    //If the roll% is within the attacker critical rate %, 
    //the attacker scored a critical hit
    if (roll <= attacker.CriticalRate)
    {
        return true;
    }
 
    return false;
}

This method takes in a character, and a random object (the object we created in GameLogic.cs).

We use the random objects, next command to get a random number from 1 to 100. This will be our luck roll, to determine whether we have scored a critical hit.

We then determine if the roll is less then or equal to the Character's critical rate, and if so, he has score a critical hit, and the method returns true, else it returns false.

Next is the evasion method, just like the Critical hit method, Add the following method to the Combat class in the Combat.cs source file:

private static bool CheckEvasion(Character defender, Random rnd)
{
    //Get a random number, representing 0 to 100%
    int roll = rnd.Next(100);
 
    //If the roll% is within the defender's evasion %, the defender has evaded the attack
    if (roll <= defender.Evasion)
    {
        return true;
    }
 
    return false;
}

There really isn't that difference between the Evasion method and Critical Hit method, other then we check against the character's evasion instead of the critical rate.

With these methods we can now determine if a character, Evades an attack, or Scores and Critical Hit.

Dealing the Damage

Now we'll implement the method that will actually do the damage.

Add the following method to the Combat class in the Combat.cs source file:

public static void AIHitPlayer(Character thePlayer, List<Character> theAI, Random randomnumber)
{
    //Go through each AI
    for (int i = 0; i < theAI.Count; i++)
    {
        //Check that the AI is alive, On screen, and is attacking
        if (theAI[i].Alive && !Graphics.IsSpriteOffscreen(theAI[i].Sprite) && theAI[i].Attacking)
        {
            //Check that the AI's attack animation is at the final frame
            if (theAI[i].CurrentSlash.CurrentFrame == theAI[i].CurrentSlash.FramesPerCell.Length - 1)
            {
                //Check that the AI and Player is within striking distance
                if (AIController.CalculateDistance(thePlayer, theAI[i]) < 100)
                {
                    //Check that the AI sword sprite has collided with the player
                    if (Physics.HaveSpritesCollided(thePlayer.Sprite, theAI[i].CurrentSlash))
                    {
                        //Check if Player evaded
                        if (CheckEvasion(thePlayer, randomnumber))
                        {
                            thePlayer.DamageCharacter(0, DamageType.Evade);
                        }
                        //Check if AI score a Critical Hit
                        else if (CheckEvasion(theAI[i], randomnumber))
                        {
                            Audio.PlaySoundEffect(Resources.GameSound("Critical"));
                            thePlayer.DamageCharacter(theAI[i].Attack * 3, DamageType.Critical);
                        }
                        //Ordinary Hit
                        else
                        {
                            Audio.PlaySoundEffect(Resources.GameSound("Hit"));
                            thePlayer.DamageCharacter((theAI[i].Attack * (100 - thePlayer.Defense)) / 100, DamageType.Enemy);
                        }
 
                        //Set the attacking state to false
                        theAI[i].Attacking = false;
                    }
                }
            }
        }
    }
}

Also add this following method to the Combat class in Combat.cs

public static void PlayerHitAI(Character thePlayer, List<Character> theAI, Random randomnumber)
{
    //If the player's attack animation is at the last frame, and is attacking
    if (thePlayer.CurrentSlash.CurrentFrame == thePlayer.CurrentSlash.FramesPerCell.Length - 1 && thePlayer.Attacking)
    {
        //for each AI
        for (int i = 0; i < theAI.Count; i++)
        {
            //if AI, is alive, onscreen
            if (theAI[i].Alive && !Graphics.IsSpriteOffscreen(theAI[i].Sprite))
            {
                //if the AI is within attacking distance
                if (AIController.CalculateDistance(thePlayer, theAI[i]) < 100)
                {
                    //If the player's sword has hit the AI
                    if (Physics.HaveSpritesCollided(theAI[i].Sprite, thePlayer.CurrentSlash))
                    {
                        //Check if AI evaded
                        if (CheckEvasion(theAI[i], randomnumber))
                        {
                            theAI[i].DamageCharacter(0, DamageType.Evade);
                        }
                        //Check if Player score a Critical Hit
                        else if (CheckCritical(thePlayer, randomnumber))
                        {
                            Audio.PlaySoundEffect(Resources.GameSound("Critical"));
                            theAI[i].DamageCharacter(thePlayer.Attack * 3, DamageType.Critical);
                        }
                        //If its a normal Hit
                        else
                        {
                            Audio.PlaySoundEffect(Resources.GameSound("Hit"));
                            theAI[i].DamageCharacter((thePlayer.Attack * (100 - theAI[i].Defense)) / 100, DamageType.Player);
                        }
 
                        //If the AI's health is less then 0, the AI has died
                        //and so, the Player gains experience
                        if (theAI[i].Health <= 0)
                        {
                            thePlayer.Experience = thePlayer.Experience + theAI[i].Experience;
                        }
 
                        //Set Players Attacking to false
                        thePlayer.Attacking = false;
                    }
                }
            }
        }
    }
}

Now our Player and AI can attack each other.

Finishing Touches

Add the following Lines of code to the Run() method in the Game class in Game.cs source file, within the loop:

...
 
//Interact with Healers and Update Player Status
Interaction.InteractWithHealers(_Player, _Healers.Characters);
_Player.UpdateCharacterStatus();
 
//ADD THESE 2 LINE OF CODE
//Combat
Combat.AIHitPlayer(_Player, _AI, _RandomNumber);
Combat.PlayerHitAI(_Player, _AI, _RandomNumber);
 
//Run User Interface
_Interface.RunUI(_Player);
 
...

This will run the checks for the combat.

One final thing needs to be done.

Add the following line of code to the UpdateAI() method in the AIController class in the AIController.cs source file:

...
 
            //If the AI can attack, and is within attacking distance, attack
            if (theAI[i].CanAttack && CalculateDistance(theAI[i], thePlayer) < 45)
            {
                theAI[i].InitiateAttack();
            }
 
            //Updates the AI's Walking Animation
            theAI[i].UpdateCharacterAnimation();
 
            //Draws the AI
            Graphics.DrawSprite(theAI[i].Sprite);
        }
 
        theAI[i].UpdateCharacterStatus();
    }
}

This will add the notifications to the AI.

I've also made a new CharacterInformation() method that shows you your level and how many unused skill points you have, while your in the game.

Add the following method to the UserInterface class of the UserInterface.cs source file:

private void DrawCharacterInformation(Character theCharacter)
{
    Text.DrawTextOnScreen("Level: " + theCharacter.Level, Color.White, Resources.GameFont("Arial"), 20, 60);
    Text.DrawTextOnScreen("Unused Stat Points: " + theCharacter.StatPoints, Color.Red, Resources.GameFont("Arial"), 20, 85);
}

Also add the following line of code to the RunUI() method in the UserInterface class within the UserInterface.cs source file:

//Execute the User Interface
public void RunUI(Character theCharacter)
{
    //Toggles the stat page if the Space Bar is hit
    if (Input.WasKeyTyped(SwinGame.Keys.VK_S))
    {
        ToggleStatPage();
    }
 
    //Draws the Health Bar and the Stat Page
    DrawHealthBar(theCharacter);
 
    //ADD THIS LINE OF CODE
    DrawCharacterInformation(theCharacter);
 
    DrawStatsPage(theCharacter);
}


And that's that, Compile and run the game, now you'll be able to fight your enemies, gain levels, and add stats.

Image:RPGss10.PNG

Next tutorial will be looking at making the Legendary Tomato Item, and adding in the functionality to win and lose the game.

The Project so far

The Source code for the tutorial up to this point can be found Here.

It is strongly recommended that you read the source code as you go through the tutorial to get a better understanding of how it works.

Tutorial Table of Contents

  1. The Legend of the Tomato Quest Tutorial
  2. Loading the level
  3. The Player
    1. Loading and Drawing the Player
    2. Moving the Player
  4. Player Stats
  5. User Interface
  6. Attacking Animation
  7. AI
    1. The Healer
    2. The Healer II
    3. Enemies
    4. Enemies II
  8. Interaction
  9. Combat
  10. Items and End Game Conditions
  11. Additions and Improvements