D20 RPG – Damage

In the last lesson we implemented attack rolls and even saw a glimpse of damage rolls, though no damage has yet been applied. This aspect of the game, like nearly every aspect has rule after rule and exceptions to those rules. Let’s take a deeper dive into applying damage.

Overview

If you aren’t familiar with the rules around applying Damage, then I would recommend you begin by reading from the SRD here. There are quite a few rules, and since I am still relatively new to this game, I may end up missing something. Feel free to call it out if you see that I have misunderstood. With that said, I will begin to describe “my” understanding of the damage system, and what we will work toward implementing in this lesson.

After a successful attack roll, the attacker will next roll for damage. Consider damage dealt by a Longsword. The description for this weapon includes the following: “Damage 1d8 S”. The “1d8” portion of the description means that when you attack and hit with this sword, you would roll a single eight-sided dice. The dice result would be added to modifiers and proficiency of whomever was wielding the weapon (other bonuses or penalties could also apply). We will call that final value the “force” of the attack. The “S” in the description means that the type of damage is “Slashing” damage.

The “type” of damage only matters in special circumstances. Creatures or equipment may have immunities, resistances, or even weaknesses that need to be considered based on the type of damage. Consider for example plate armor. This kind of armor offers resistance to slashing damage – the same kind of damage our longsword can deal. So let’s suppose that the “force” of the attack is 5, but the target of our attack is wearing plate armor that resists 2 slashing damage. In such a case, the target will only lose 3 hit points.

Some weapons have traits that need to be considered. Our Longsword has one: “Versatile P” which means that in addition to slashing damage, you could choose instead to deal piercing damage. If you knew you were attacking a foe with plate armor, then a thrust would be a better choice than a slice. You must choose the type of damage to deal when making the attack – you don’t deal both kinds of damage simultaneously. So, if you chose piercing damage against the same foe from before, all of the force of the attack would be converted to the same amount of lost hit points.

It will be up to the design of our UI to figure out how a player should determine the type of damage to deal in this kind of scenario. Maybe after equipping a longsword, instead of having a singe “attack” option, they would have “Thrust” and “Slash” options.

But wait, there’s more! Consider the Smoking Sword. This is a magically enhanced Longsword. In addition to the slashing or piercing damage that the normal longsword can do, this sword will do an additional point of “fire” damage. Each type of damage is handled separately. A foe wearing super strong plate mail may resist all of the “slashing” part of the sword’s damage, but would still suffer the “fire” part of the damage unless they ALSO had fire resistance.

Weapons aren’t the only thing that can deal damage. The spell Frigid Flurry deals both cold and slashing damage. Like before, each part of the damage should be considered separately for immunities, resistances, and weaknesses.

One last thing – there can be exceptions to the rules. Consider the monster Barbazu whose description says: “Resistances physical 5 (except silver)”. That means that bludgeoning, slashing, and piercing damage types are all resisted by up to 5 points UNLESS the weapon happens to be made out of silver.

Getting Started

Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.

Damage System

Create a new folder at Scripts -> Combat named Damage. Then create a new C# script in the folder named DamageSystem and add the following:

using System.Collections.Generic;
using UnityEngine;

public struct DamageInfo
{
    public Entity target;
    public int damage;
    public int criticalDamage;
    public string type;
    public string material;
}

I started out by adding a struct that holds all of the relevant bits of information that are necessary for this system to do its job. It needs to know:

  • who the damage would be targeting
  • the amount of normal damage that should be dealt
  • the amount of critical damage – this will be zero unless the attack was a critical success. I am tracking this amount of damage separately because some creatures may be immune to critical hits.
  • the type of damage (slashing, piercing, cold, etc)
  • the optional type of material doing the damage (silver etc)

Add the following:

public interface IDamageTypeSystem
{
    bool GetImmunity(DamageInfo info);
    int GetWeakness(DamageInfo info);
    int GetResistance(DamageInfo info);
}

There are a lot of different categories of damage including: physical, energy, alignment, mental, poison, bleed and precision. Categories of damage can then have types that it recognizes, such as a physical category having bludgeoning, piercing, and slashing damage types. Precious materials can also play a role in the amount of damage that is dealt. Given the variety of possible rules here, I decided it would be best if I could create unique systems for any special cases.

I want the main damage system to be able to hold all of these specialized systems in a more general fashion, so I will have them all conform to the same interface, and then the system can hold a collection of them.

The GetImmunity method will return true if the system determines that the target should be immune to a given type of damage. It will return false otherwise.

The GetWeakness method will let me know if the system determines that the target is weak to a given type of damage, and if so, by how much.

The GetResistance method will let me know if the system determine that the target can resist a given type of damage, and if so, by how much.

Add the following:

public interface IDamageSystem : IDependency<IDamageSystem>
{
    int Apply(DamageInfo info);
    void Add(IDamageTypeSystem damageTypeSystem);
}

This interface is for the main damage system. It will coordinate any logic between all the systems that handle specific types of damage.

You will call Apply to see how much damage can break through a target’s defenses. The returned value is the amount of hit points the target will lose.

Call Add to provide a system that knows how to handle a specific kind of damage. You can call this repeatedly, once for each new damage type.

Add the following:

public class DamageSystem : IDamageSystem
{
    List<IDamageTypeSystem> damageTypeSystems = new List<IDamageTypeSystem>();

    public int Apply(DamageInfo info)
    {
        // Step 1: Check for Immunity
        foreach (var system in damageTypeSystems)
            if (system.GetImmunity(info))
                return 0;

        int result = info.damage + info.criticalDamage;

        // Step 2: Check for Weaknesses
        foreach (var system in damageTypeSystems)
            result += system.GetWeakness(info);

        // Step 3: Check for Resistances
        foreach (var system in damageTypeSystems)
            result -= system.GetResistance(info);

        return Mathf.Max(result, 0);
    }

    public void Add(IDamageTypeSystem damageTypeSystem)
    {
        damageTypeSystems.Add(damageTypeSystem);
    }
}

Here is the concrete implementation of the main damage system. It holds a private list of systems, where each system handles a different kind of damage. The Add method appends the passed system to this list.

In the Apply method, we have three steps. Per “Step 3” of the SRD, we apply immunities first, then weaknesses and finally resistances. I am interpreting this to mean that if there is immunity to a damage type, that no damage can be done, and I can just exit the method early. If the target is not immune, then I will build up a new result that starts based on the sum of the normal and critical damage amounts, then add damage for any found weaknesses, and finally subtract damage for any resistances. The final returned result at this point will be the new result, or zero, whichever is higher.

Damage Immunity

Let’s create a new system to tag an entity as being immune to certain types of damage. Add a new C# script to the same folder named DamageImmunitySystem and add the following:

[System.Serializable]
public class DamageImmunity
{
    public CoreSet<string> types = new CoreSet<string>();
}

public partial class Data
{
    public CoreDictionary<Entity, DamageImmunity> damageImmunity = new CoreDictionary<Entity, DamageImmunity>();
}

public interface IDamageImmunitySystem : IDependency<IDamageImmunitySystem>, IEntityTableSystem<DamageImmunity>
{
    bool HasImmunity(Entity entity, string damageType);
    void AddImmunity(Entity entity, string damageType);
    void RemoveImmunity(Entity entity, string damageType);
}

public class DamageImmunitySystem : EntityTableSystem<DamageImmunity>, IDamageImmunitySystem
{
    public override CoreDictionary<Entity, DamageImmunity> Table => IDataSystem.Resolve().Data.damageImmunity;

    public bool HasImmunity(Entity entity, string damageType)
    {
        DamageImmunity damageImmunity;
        if (TryGetValue(entity, out damageImmunity))
            return damageImmunity.types.Contains(damageType);
        return false;
    }

    public void AddImmunity(Entity entity, string damageType)
    {
        if (!Has(entity))
            Set(entity, new DamageImmunity());

        Get(entity).types.Add(damageType);
    }

    public void RemoveImmunity(Entity entity, string damageType)
    {
        DamageImmunity damageImmunity;
        if (TryGetValue(entity, out damageImmunity))
            damageImmunity.types.Remove(damageType);
    }
}

For the most part, this system matches the other variations of entity table systems we have added. Remember that Unity’s serializer struggles to handle nested generic types, so I chose to add an intermediate data class called DamageImmunity which is itself serializable and holds a set of the types of damage an entity is immune to.

The system itself maps from an entity to this new class, but I also wanted to be able to simply check for, add, or remove immunity by the damage type. I therefore added a few extra methods to handle this.

Damage Weakness

Let’s make a similar system to handle weakness. Add a new C# script in the same folder named DamageWeaknessSystem and add the following:

[System.Serializable]
public class DamageWeakness
{
    public CoreDictionary<string, int> types = new CoreDictionary<string, int>();
}

public partial class Data
{
    public CoreDictionary<Entity, DamageWeakness> damageWeakness = new CoreDictionary<Entity, DamageWeakness>();
}

public interface IDamageWeaknessSystem : IDependency<IDamageWeaknessSystem>, IEntityTableSystem<DamageWeakness>
{
    int GetWeakness(Entity entity, string damageType);
    void SetWeakness(Entity entity, string damageType, int amount);
    void RemoveWeakness(Entity entity, string damageType);
}

public class DamageWeaknessSystem : EntityTableSystem<DamageWeakness>, IDamageWeaknessSystem
{
    public override CoreDictionary<Entity, DamageWeakness> Table => IDataSystem.Resolve().Data.damageWeakness;

    public int GetWeakness(Entity entity, string damageType)
    {
        DamageWeakness damageWeakness;
        if (TryGetValue(entity, out damageWeakness))
        {
            if (damageWeakness.types.ContainsKey(damageType))
                return damageWeakness.types[damageType];
        }
        return 0;
    }

    public void SetWeakness(Entity entity, string damageType, int amount)
    {
        if (!Has(entity))
            Set(entity, new DamageWeakness());

        Get(entity).types[damageType] = amount;
    }

    public void RemoveWeakness(Entity entity, string damageType)
    {
        DamageWeakness damageWeakness;
        if (TryGetValue(entity, out damageWeakness))
            damageWeakness.types.Remove(damageType);
    }
}

This class is similar to the immunity system, except that instead of a set of damage types, we have a dictionary. For each type of damage, there is an “amount” of weakness. Accordingly I also added some methods to allow me to get, set or remove based on damage type.

Damage Resistance

Let’s make a similar system to handle resistance. Add a new C# script in the same folder named DamageResistanceSystem and add the following:

[System.Serializable]
public class DamageResistance
{
    public CoreDictionary<string, int> types = new CoreDictionary<string, int>();
}

public partial class Data
{
    public CoreDictionary<Entity, DamageResistance> damageResistance = new CoreDictionary<Entity, DamageResistance>();
}

public interface IDamageResistanceSystem : IDependency<IDamageResistanceSystem>, IEntityTableSystem<DamageResistance>
{
    int GetResistance(Entity entity, string damageType);
    void SetResistance(Entity entity, string damageType, int amount);
    void RemoveResistance(Entity entity, string damageType);
}

public class DamageResistanceSystem : EntityTableSystem<DamageResistance>, IDamageResistanceSystem
{
    public override CoreDictionary<Entity, DamageResistance> Table => IDataSystem.Resolve().Data.damageResistance;

    public int GetResistance(Entity entity, string damageType)
    {
        DamageResistance damageResistance;
        if (TryGetValue(entity, out damageResistance))
        {
            if (damageResistance.types.ContainsKey(damageType))
                return damageResistance.types[damageType];
        }
        return 0;
    }

    public void SetResistance(Entity entity, string damageType, int amount)
    {
        if (!Has(entity))
            Set(entity, new DamageResistance());

        Get(entity).types[damageType] = amount;
    }

    public void RemoveResistance(Entity entity, string damageType)
    {
        DamageResistance damageResistance;
        if (TryGetValue(entity, out damageResistance))
            damageResistance.types.Remove(damageType);
    }
}

This script follows the same pattern used for the damage weakness system. We added a new game data that is managed by a subclass of an EntityTableSystem. We added additional methods to let us set and remove resistances directly.

Damage Resistance Exception

Damage resistance may have exceptions, so let’s add another class to handle this. Create a new C# script in the same folder named DamageResistanceExceptionSystem and add the following:

[System.Serializable]
public class DamageResistanceException
{
    public CoreDictionary<string, string> types = new CoreDictionary<string, string>();
}

public partial class Data
{
    public CoreDictionary<Entity, DamageResistanceException> damageResistanceException = new CoreDictionary<Entity, DamageResistanceException>();
}

public interface IDamageResistanceExceptionSystem : IDependency<IDamageResistanceExceptionSystem>, IEntityTableSystem<DamageResistanceException>
{
    string GetException(Entity entity, string damageType);
    void SetException(Entity entity, string damageType, string exception);
    void RemoveException(Entity entity, string damageType);
}

public class DamageResistanceExceptionSystem : EntityTableSystem<DamageResistanceException>, IDamageResistanceExceptionSystem
{
    public override CoreDictionary<Entity, DamageResistanceException> Table => IDataSystem.Resolve().Data.damageResistanceException;

    public string GetException(Entity entity, string damageType)
    {
        DamageResistanceException value;
        if (TryGetValue(entity, out value))
        {
            if (value.types.ContainsKey(damageType))
                return value.types[damageType];
        }
        return null;
    }

    public void SetException(Entity entity, string damageType, string exception)
    {
        if (!Has(entity))
            Set(entity, new DamageResistanceException());

        Get(entity).types[damageType] = exception;
    }

    public void RemoveException(Entity entity, string damageType)
    {
        DamageResistanceException value;
        if (TryGetValue(entity, out value))
            value.types.Remove(damageType);
    }
}

This system looks quite similar to the damage resistance system, except that it maps from a damage type to another string representing a material that could make a resistance exception.

Damage Type System

For each of the main categories of damage types, I can make a reusable system that merely accepts the category and type names in its constructor. Create a new C# script in the same folder named DamageTypeSystem and add the following:

using System.Collections.Generic;

public class DamageTypeSystem : IDamageTypeSystem
{
    string category;
    HashSet<string> types;

    public DamageTypeSystem(string category, string[] types)
    {
        this.category = category;
        this.types = new HashSet<string>(types);
    }

    public bool GetImmunity(DamageInfo info)
    {
        if (string.Equals(info.type, category) || types.Contains(info.type))
        {
            var system = IDamageImmunitySystem.Resolve();
            return system.HasImmunity(info.target, info.type);
        }
        return false;
    }

    public int GetResistance(DamageInfo info)
    {
        if (string.Equals(info.type, category) || types.Contains(info.type))
        {
            var system = IDamageResistanceSystem.Resolve();
            var excSystem = IDamageResistanceExceptionSystem.Resolve();
            var result = system.GetResistance(info.target, info.type);
            var exception = excSystem.GetException(info.target, info.type);
            return string.Equals(info.material, exception) ? 0 : result;
        }
        return 0;
    }

    public int GetWeakness(DamageInfo info)
    {
        if (string.Equals(info.type, category) || types.Contains(info.type))
        {
            var system = IDamageWeaknessSystem.Resolve();
            return system.GetWeakness(info.target, info.type);
        }
        return 0;
    }
}

The GetImmunity method will check to see if the type of damage matches either its own category, or one of its own damage types. If so, then it will check the damage immunity system to see if the target of the attack has that damage type as an immunity.

The GetResistance method will check to see if the type of damage matches either its own category, or one of its own damage types. If so, then it will grab the amount of resistance to that type of damage that the target has. Next it will check to see if the info’s material is an exception to that particular type of damage. If there is no exception found, then we return the amount of resistance. Otherwise, we treat it as if there is no resistance.

The GetWeakness method will check to see if the type of damage matches either its own category, or one of its own damage types. If so, then it will return the amount of weakness to that type of damage that the entity has.

Material Damage

Some creatures, like a werewolf, may have a weakness to a material, like silver. I suppose it could also be true for immunities or resistances based on a material (though I haven’t examined every creature to see if I could find any). Therefore, I can make another type of IDamageTypeSystem based on the material used in a damage check.

Create a new C# script in the same folder named MaterialDamageTypeSystem and add the following:

public class MaterialDamageTypeSystem : IDamageTypeSystem
{
    public bool GetImmunity(DamageInfo info)
    {
        if (string.IsNullOrEmpty(info.material))
            return false;
        var system = IDamageImmunitySystem.Resolve();
        return system.HasImmunity(info.target, info.material);
    }

    public int GetResistance(DamageInfo info)
    {
        if (string.IsNullOrEmpty(info.material))
            return 0;
        var system = IDamageResistanceSystem.Resolve();
        return system.GetResistance(info.target, info.material);
    }

    public int GetWeakness(DamageInfo info)
    {
        if (string.IsNullOrEmpty(info.material))
            return 0;
        var system = IDamageWeaknessSystem.Resolve();
        return system.GetWeakness(info.target, info.material);
    }
}

This looks pretty similar to our DamageTypeSystem but with one quick note to point out. I will expect every damage check to have provided a damage “type”, but do not expect them all to provide a “material”. Therefore, I check for an empty string and return early if that is the case.

Injection

Now we need to create an injector to handle the creation of all of our damage systems. Create a new C# script in the same folder named DamageInjector and add the following:

public static class DamageInjector
{
    public static void Inject()
    {
        var damageSystem = new DamageSystem();
        damageSystem.Add(new DamageTypeSystem("physical", new string[] { "bludgeoning", "piercing", "slashing" }));
        damageSystem.Add(new DamageTypeSystem("energy", new string[] { "acid", "cold", "electricity", "fire", "sonic" }));
        damageSystem.Add(new DamageTypeSystem("alignment", new string[] { "chaotic", "evil", "good", "lawful" }));
        damageSystem.Add(new DamageTypeSystem("mental", new string[0]));
        damageSystem.Add(new DamageTypeSystem("poison", new string[0]));
        damageSystem.Add(new DamageTypeSystem("bleed", new string[0]));
        damageSystem.Add(new DamageTypeSystem("precision", new string[0]));
        damageSystem.Add(new MaterialDamageTypeSystem());

        IDamageSystem.Register(damageSystem);
        IDamageImmunitySystem.Register(new DamageImmunitySystem());
        IDamageWeaknessSystem.Register(new DamageWeaknessSystem());
        IDamageResistanceSystem.Register(new DamageResistanceSystem());
        IDamageResistanceExceptionSystem.Register(new DamageResistanceExceptionSystem());
    }
}

Nothing too special here, except to note that I create and configure the damage system with all of its added damage type subsystems before registering it to its interface.

Open the CombatInjector and add the following to its Inject method:

DamageInjector.Inject();

Unit Tests

As a quick way to verify that the various bits of logic are all working, I created some Unit Tests. I added folders as necessary to create the file at Assets -> Tests -> Combat -> Damage -> DamageSystemTests.cs. I added the following:

using NUnit.Framework;

public class DamageSystemTests
{
    Entity entity;
    string damageType;
    string material;
    DamageInfo damageInfo;

    [SetUp]
    public void SetUp()
    {
        DamageInjector.Inject();
        entity = new Entity(123);
        damageType = "slashing";
        material = "silver";
        damageInfo = new DamageInfo
        {
            target = entity,
            damage = 10,
            criticalDamage = 5,
            type = damageType,
            material = material
        };

        IDataSystem.Register(new MockDataSystem());
        IDataSystem.Resolve().Create();
    }
}

Here I have some simple fields that will be useful throughout my tests. Then in the SetUp method I inject all the systems that are managed by the DamageInjector along with a MockDataSystem that has created its game data in memory. I create a simple DamageInfo struct that can be used repeatedly for testing out how the same Damage “attempt” is modified based on the unique defenses of the target.

Following are a bunch of tests to add within the DamageSystemTests class:

[Test]
public void NoImmunityWeaknessResistance_ReturnsInitialForce()
{
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(15, result);
}

For our first test, I have not taken any additional steps on the target. This is the baseline test, and it verifies that when the target has no immunity, weakness, or resistance, that the damage result is unmodified.

[Test]
public void DamageTypeImmunity_ReturnsNoDamage()
{
    IDamageImmunitySystem.Resolve().AddImmunity(entity, damageType);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(0, result);
}

For the next test, I give the target immunity to the type of damage used in the attack. This time, I expect to see that no damage is actually dealt.

[Test]
public void MaterialImmunity_ReturnsNoDamage()
{
    IDamageImmunitySystem.Resolve().AddImmunity(entity, material);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(0, result);
}

For the next test, I wanted to check that immunity to material types could also work. Like before, I expect no damage to be dealt.

[Test]
public void DamageTypeWeakness_ReturnsExtraDamage()
{
    IDamageWeaknessSystem.Resolve().SetWeakness(entity, damageType, 5);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(20, result);
}

In this test, I gave the target a weakness to the type of damage used in the attack. My expectation is that more damage will be dealt.

[Test]
public void MaterialWeakness_ReturnsExtraDamage()
{
    IDamageWeaknessSystem.Resolve().SetWeakness(entity, material, 5);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(20, result);
}

Here I have added a material weakness of the same type used in the attack. Once again, I expect more damage than normal to occur.

[Test]
public void DamageTypeResistance_ReturnsLessDamage()
{
    IDamageResistanceSystem.Resolve().SetResistance(entity, damageType, 5);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(10, result);
}

In this test I give the target a resistance to the damage type used in the attack. I expect less damage than normal to be dealt.

[Test]
public void MaterialResistance_ReturnsLessDamage()
{
    IDamageResistanceSystem.Resolve().SetResistance(entity, material, 5);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(10, result);
}

Here I am testing resistance to damage based on the material type. Less damage should be dealt.

[Test]
public void DamageTypeResistanceException_ReturnsInitialForce()
{
    IDamageResistanceExceptionSystem.Resolve().SetException(entity, damageType, material);
    IDamageResistanceSystem.Resolve().SetResistance(entity, damageType, 5);
    var sut = IDamageSystem.Resolve();
    var result = sut.Apply(damageInfo);
    Assert.AreEqual(15, result);
}

For my final test, I give the target resistance to the attack’s damage type, but also make an exception to the type of material used in the attack. Because of this, I expect the amount of damage to be unmodified.

Go ahead and run your tests. All should pass.

Demo

The unit tests were helpful to verify that the various features of our damage system are working, but it still might be fun to “see” it in the game in some way. Let’s add a few more classes to help with that. First, let’s add a few attribute providers so that we can customize the hero and or monster for immunities, weaknesses or resistances. Then we can modify their attacks to specify damage and material types, and to actually call into the damage system.

Damage Immunity Provider

Create a new script at Scripts -> AttributeProvider named DamageImmunityProvider and add the following:

using UnityEngine;

public class DamageImmunityProvider : MonoBehaviour, IAttributeProvider
{
    [SerializeField] string damageType;

    public void Setup(Entity entity)
    {
        IDamageImmunitySystem.Resolve().AddImmunity(entity, damageType);
    }
}

Damage Resistance Provider

Create a new script in the same folder named DamageResistanceProvider and add the following:

using UnityEngine;

public class DamageResistanceProvider : MonoBehaviour, IAttributeProvider
{
    [SerializeField] string damageType;
    [SerializeField] int amount;
    [SerializeField] string exception;

    public void Setup(Entity entity)
    {
        IDamageResistanceSystem.Resolve().SetResistance(entity, damageType, amount);
        if (!string.IsNullOrEmpty(exception))
            IDamageResistanceExceptionSystem.Resolve().SetException(entity, damageType, exception);
    }
}

Damage Weakness Provider

Create a new script in the same folder named DamageWeaknessProvider and add the following:

using UnityEngine;

public class DamageWeaknessProvider : MonoBehaviour, IAttributeProvider
{
    [SerializeField] string damageType;
    [SerializeField] int amount;

    public void Setup(Entity entity)
    {
        IDamageWeaknessSystem.Resolve().SetWeakness(entity, damageType, amount);
    }
}

Solo Adventure Attack

Open the solo adventure attack script and add the following fields:

[SerializeField] string damageType;
[SerializeField] string material;

Next replace the placeholder code around the damage roll with the following:

// Determine Damage
DamageInfo damageInfo = new DamageInfo
{
    target = target,
    damage = 0,
    criticalDamage = 0,
    type = damageType,
    material = material
};
switch (attackRoll)
{
    case Check.CriticalSuccess:
        var critRoll = damage.Roll();
        damageInfo.damage = critRoll;
        damageInfo.criticalDamage = critRoll;
        Debug.Log(string.Format("Critical Hit for {0} Damage!", critRoll * 2));
        break;
    case Check.Success:
        var roll = damage.Roll();
        damageInfo.damage = roll;
        Debug.Log(string.Format("Hit for {0} Damage!", roll));
        break;
    default:
        Debug.Log("Miss");
        break;
}

// TODO: Apply Damage if applicable
var damageAmount = IDamageSystem.Resolve().Apply(damageInfo);
Debug.Log("Final Damage: " + damageAmount);

When you play the game, after each attack you will see the original damage roll printed to the console, followed by another message that displays the amount of damage to actually apply based on the targets defenses. We still aren’t applying the damage to anything because we haven’t yet defined hit points, but that will come soon.

Entity Recipe

Feel free to modify either the Hero or Rat EntityRecipe assets by adding one or more of our attribute providers. In the final project for this lesson you can see that I have attached a DamageResistanceProvider to the hero so that he can resist up to 5 points of “piercing” damage. I did not add an exception to his resistance. This change just makes it extra sure that the player will survive their combat encounter.

Combat Action

Both the “Bite” and “Strike (Shortsword)” assets need to be updated to define the damage type that they deal. The material is not required. For the hero’s attack, I specified “slashing” damage. For the monster’s attack, I specified “piercing” damage.

Give it a try

Go ahead and play the game like normal, and allow the rat to attack you. You should see that some, if not all, of its damage will be blocked by the hero’s new resistance to piercing damage.

Feel free to experiment with other combinations including immunities, weaknesses, and resistance exceptions. Add additional attacks if desired. Have fun!

Summary

This lesson was all about damage. More specifically, how to handle the way damage is modified based on traits that an Entity could have such as immunity, weakness or resistance. These traits could be based on a damage category, a damage type, or a material that is attacked with. In addition, damage resistances can have exceptions based on materials. After adding all of this functionality, we verified it was working with some unit tests, then we modified the game so we could see some damage resistance in action!

If you got stuck along the way, feel free to download the finished project for this lesson here.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

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