D20 RPG – Skills

In the last lesson I created an Entry which mentions the possibility of jumping across a hole. In pathfinder, a “challenge” like this is determined with some sort of skill check. Of course we need to define what a Skill is first!

Overview

You can think of a skill as just a number – and that number is added to a d20 roll which is then compared against some sort of DC (difficulty check) to determine if the action was a success or not (and in some cases how good or how bad the result really was).

For handling skills, I am imagining a setup similar to how we handled ability scores. There was an enum for each type of ability score and a general system to get a specific score based on the enum, but there were also systems per type, such as a strength system.

There are many different skills defined in Pathfinder, which we can treat as an enum. We can have a general system to get a specific skill value based on that enum, and can also add specific systems per skill. The specific systems can handle “how” the final skill value is determined.

Each skill has a concept of how trained an Entity is within that type of skill. The training is called “Proficiency” and so there could also be a general system that can read or write proficiency for a given Entity and Skill, as well as specific proficiency systems per skill.

It may be worth pointing out that there are other aspects of a hero such as Perception, Saving Throws, Defenses and Weapons and Attacks, all of which also have proficiencies and similar methods of calculation, though they are not considered skills.

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.

Skill

Create a new folder at Assets -> Scripts -> Component named Skills. Then create a new C# script in this folder named SkillSystem and add the following:

public enum Skill
{
    Acrobatics,
    Arcana,
    Athletics,
    Crafting,
    Deception,
    Diplomacy,
    Intimidation,
    Lore,
    Medicine,
    Nature,
    Occultism,
    Performance,
    Religion,
    Society,
    Stealth,
    Survival,
    Thievery
}

I think that this list may not include every type of Skill in the Pathfinder universe, but it at least covers the list of skills that were included on the sample character sheets included in the beginner box.

Proficiency

The “calculation” of a skill’s value depends on something called Proficiency. I think of this as how much experience you have doing a particular task in the correct way. This means that you don’t gain proficiency bonuses until you are “trained” in a skill. As your character levels up, they may also increase their proficiency rank.

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

public enum Proficiency
{
    Untrained,
    Trained,
    Expert,
    Master,
    Legendary
}

Athletics Proficiency System

Let’s create a concrete system for an example skill, Athletics. Create a new C# script in the same folder named AthleticsProficiencySystem and add the following:

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

public interface IAthleticsProficiencySystem : IDependency<IAthleticsProficiencySystem>, IEntityTableSystem<Proficiency>
{

}

public class AthleticsProficiencySystem : EntityTableSystem<Proficiency>, IAthleticsProficiencySystem
{
    public override CoreDictionary<Entity, Proficiency> Table => IDataSystem.Resolve().Data.athleticsProficiency;
}

Nothing special here, it follows the same pattern as normal, except that I didn’t bother to add a partial definition for the Entity. The proficiency of a skill will only be used to calculate the skill value, so I will just use the system directly when needed.

Proficiency System

Re-open the ProficiencySystem script and add the following:

public interface IProficiencySystem : IDependency<IProficiencySystem>
{
    Proficiency Get(Entity entity, Skill skill);
    void Set(Entity entity, Skill skill, Proficiency value);
}

public class ProficiencySystem : IProficiencySystem
{
    public Proficiency Get(Entity entity, Skill skill)
    {
        return GetSystem(skill).Get(entity);
    }

    public void Set(Entity entity, Skill skill, Proficiency value)
    {
        GetSystem(skill).Set(entity, value);
    }

    IEntityTableSystem<Proficiency> GetSystem(Skill skill)
    {
        switch (skill)
        {
            case Skill.Athletics:
                return IAthleticsProficiencySystem.Resolve();
            default:
                return null;
        }
    }
}

This is the implementation for the general system where we can get or set the proficiency of any Entity’s skill. It should use a switch statement over each type of skill and map to the corresponding injected system. Since we have only implemented one example system, I am only showing one system. As an exercise later, you should implement the systems for each other type of skill.

Level System

As your character advances past the “Untrained” Proficiency rank, the character Level also plays a role in a skill’s calculation. Of course, there is a lot more to “leveling” (and therefore a proper level system) than just knowing your level’s value, but for now, we can keep things simple.

Add a new C# script to Assets -> Scripts -> Component named LevelSystem and add the following:

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

public interface ILevelSystem : IDependency<ILevelSystem>, IEntityTableSystem<int>
{

}

public class LevelSystem : EntityTableSystem<int>, ILevelSystem
{
    public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.level;
}

public partial struct Entity
{
    public int Level
    {
        get { return ILevelSystem.Resolve().Get(this); }
        set { ILevelSystem.Resolve().Set(this, value); }
    }
}

Base Skill System

Most of the skills follow the same basic pattern in how the skill value is calculated. Therefore it makes sense to create a “base” interface / class that all the concrete skill classes can inherit from. Create a new C# script at Assets -> Scripts -> Component -> Skills named BaseSkillSystem and add the following:

public interface IBaseSkillSystem : IEntityTableSystem<int>
{
    void Setup(Entity entity);
}

public abstract class BaseSkillSystem : EntityTableSystem<int>, IBaseSkillSystem
{
    protected abstract Skill Skill { get; }
    protected abstract AbilityScore.Attribute Attribute { get; }

    public virtual void Setup(Entity entity)
    {
        Table[entity] = Calculate(entity);
    }

    protected virtual int Calculate(Entity entity)
    {
        int result = entity[Attribute].Modifier;
        var proficiency = IProficiencySystem.Resolve().Get(entity, Skill);
        if (proficiency != Proficiency.Untrained)
            result += (int)proficiency * 2 + entity.Level;
        return result;
    }
}

The interface inherits from IEntityTableSystem because the primary purpose of the system is just to map from an Entity to an int value representing its “Skill” level. It also adds a new method named “Setup” which can be called at key times such as when creating a new Entity or when an Entity experiences a level-up. The “Setup” method should then calculate the skill value of the Entity based on whatever rules apply.

The abstract class, BaseSkillSystem has two new abstract properties. The first property, “Skill”, is the enum type that the system deals with. The second property, “Attribute”, is the type of AbilityScore that is most directly responsible for talent at the given skill. Both of these properties are protected and are not included in the interface. This is because the subclasses may wish to work with those properties, but any other external class has no reason to know about that data.

BaseSkillSystem also provides a protected virtual method named “Calculate”. The “virtual” keyword means that I have provided a sample implementation of the method but that it can still be overwritten for special handling. The “protected” keyword indicates that this method, much like the earlier properties, may be used by the subclasses, but should not be used by anything else.

Athletics System

Let’s create our first concrete subclass of the BaseSkillSystem so you can see how it will work. Create a new C# script in the same “Skills” folder named AthleticsSystem and add the following:

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

public interface IAthleticsSystem : IDependency<IAthleticsSystem>, IBaseSkillSystem
{

}

public class AthleticsSystem : BaseSkillSystem, IAthleticsSystem
{
    public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.athletics;
    protected override Skill Skill => Skill.Athletics;
    protected override AbilityScore.Attribute Attribute => AbilityScore.Attribute.Strength;
}

public partial struct Entity
{
    public int Athletics
    {
        get { return IAthleticsSystem.Resolve().Get(this); }
        set { IAthleticsSystem.Resolve().Set(this, value); }
    }
}

This class looks pretty similar to the same patterns we have been following. It looks like pretty well every other table based system that we have made so far, except that it has a couple more properties to implement for the BaseSkillSystem.

Skill System

Re-open the SkillSystem script and add the following:

public interface ISkillSystem : IDependency<ISkillSystem>
{
    void Set(Entity entity, Skill skill, int value);
    int Get(Entity entity, Skill skill);
}

public class SkillSystem : ISkillSystem
{
    public void Set(Entity entity, Skill skill, int value)
    {
        GetSystem(skill).Set(entity, value);
    }

    public int Get(Entity entity, Skill skill)
    {
        return GetSystem(skill).Get(entity);
    }

    IEntityTableSystem<int> GetSystem(Skill skill)
    {
        switch (skill)
        {
            case Skill.Athletics:
                return IAthleticsSystem.Resolve();
            default:
                return null;
        }
    }
}

public partial struct Entity
{
    public int this[Skill skill]
    {
        get { return ISkillSystem.Resolve().Get(this, skill); }
        set { ISkillSystem.Resolve().Set(this, skill, value); }
    }
}

This is the implementation for the general system where we can get or set the value of any Skill for any Entity. Like before, it should use a switch statement over each type of skill and map to the corresponding injected system. Since we have only implemented one example system, I am only showing one system. As an exercise later, you should implement the systems for each other type of skill.

Injection

Create another C# script in the “Skills” folder named SkillsInjector and add the following:

public static class SkillsInjector
{
    public static void Inject()
    {
        IAthleticsProficiencySystem.Register(new AthleticsProficiencySystem());
        IAthleticsSystem.Register(new AthleticsSystem());
        IProficiencySystem.Register(new ProficiencySystem());
        ISkillSystem.Register(new SkillSystem());
    }
}

Next open ComponentInjector.cs and add the following statements to its “Inject” method:

ILevelSystem.Register(new LevelSystem());
SkillsInjector.Inject();

On Your Own

I only documented adding specific systems for the “Athletics” Skill, but there are many other skills left to add. As an exercise for the reader, try adding the skill and proficiency systems for each skill type, and then implement them within the general systems as well. Don’t forget they will also need to be injected.

You can use the following list to determine which ability score modifier should be used with each type of skill:

  • Acrobatics – Dex
  • Arcana – Int
  • Crafting – Int
  • Deception – Cha
  • Diplomacy – Cha
  • Intimidation – Cha
  • Lore – Int
  • Medicine – Wis
  • Nature – Wis
  • Occultism – Int
  • Performance – Cha
  • Religion – Wis
  • Society – Int
  • Stealth – Dex
  • Survival – Wis
  • Thievery – Dex

Unit Tests

We’ve added a lot of stuff in this lesson, and yet there is nothing new to “see” in the game. Not to worry, we can add some new Unit Tests and our “Demo” can involve running the tests and seeing all the great little green check marks. Pretty exciting, right?

Let’s start by adding some mock classes…

Mock Ability Score System

Create a new C# script at Tests -> Component -> AbilityScore named MockAbilityScoreSystem and add the following:

using System.Collections.Generic;
using NUnit.Framework;

public class MockAbilityScoreSystem : IAbilityScoreSystem
{
    Dictionary<Entity, Dictionary<AbilityScore.Attribute, AbilityScore>> fakeTable = new Dictionary<Entity, Dictionary<AbilityScore.Attribute, AbilityScore>>();

    public AbilityScore Get(Entity entity, AbilityScore.Attribute attribute)
    {
        if (fakeTable.ContainsKey(entity))
        {
            var map = fakeTable[entity];
            if (map.ContainsKey(attribute))
                return map[attribute];
        }
        return new AbilityScore(0);
    }

    public void Set(Entity entity, AbilityScore.Attribute attribute, AbilityScore value)
    {
        if (!fakeTable.ContainsKey(entity))
            fakeTable[entity] = new Dictionary<AbilityScore.Attribute, AbilityScore>();

        fakeTable[entity][attribute] = value;
    }

    public void Set(Entity entity, IEnumerable<int> scores)
    {
        Assert.Fail("Using un-implemented mock feature");
    }
}

This will let us set any ability score without needing to inject the generic ability score system and the various systems per attribute. It simply holds an in-memory collection of the mapping from Entity to Attribute type to Ability Score value. Note that I did not bother to implement one of the Set methods – feel free to implement that variation on your own if desired.

Mock Proficiency System

Create a new folder at Assets -> Tests -> Component named Skills. Then create a new C# Test Script in the same folder named MockProficiencySystem and add the following:

using System.Collections.Generic;

public class MockProficiencySystem : IProficiencySystem
{
    Dictionary<Entity, Dictionary<Skill, Proficiency>> fakeTable = new Dictionary<Entity, Dictionary<Skill, Proficiency>>();

    public Proficiency Get(Entity entity, Skill skill)
    {
        if (fakeTable.ContainsKey(entity))
        {
            var map = fakeTable[entity];
            if (map.ContainsKey(skill))
                return map[skill];
        }
        return 0;
    }

    public void Set(Entity entity, Skill skill, Proficiency value)
    {
        if (!fakeTable.ContainsKey(entity))
            fakeTable[entity] = new Dictionary<Skill, Proficiency>();

        fakeTable[entity][skill] = value;
    }
}

This will let us set any skill proficiency without needing to inject the generic proficiency system along with the various systems per skill type. It simply holds an in-memory collection of the mapping from Entity to Skill to Proficiency in that skill.

Mock Base Skill System

Create another new C# script in the same folder named MockBaseSkillSystem and add the following:

public class MockBaseSkillSystem : BaseSkillSystem
{
    CoreDictionary<Entity, int> fakeTable = new CoreDictionary<Entity, int>();
    Skill fakeSkill;
    AbilityScore.Attribute fakeAttribute;

    public MockBaseSkillSystem(Skill fakeSkill, AbilityScore.Attribute fakeAttribute)
    {
        this.fakeSkill = fakeSkill;
        this.fakeAttribute = fakeAttribute;
    }

    public override CoreDictionary<Entity, int> Table => fakeTable;
    protected override Skill Skill => fakeSkill;
    protected override AbilityScore.Attribute Attribute => fakeAttribute;
}

This “mock” is able to simulate a concrete subclass such as the “AthleticsSkillSystem”. It lets you specify the type of skill and attribute that are relevant, but does not override any of the other base class functionality such as the “Calculate” method. This guarantees that I will be able to test the “base” class functionality without any potential overriding that other subclasses may require.

Base Skill System Tests

Create a new C# Test Script in the same folder named BaseSkillSystemTests and add the following:

using NUnit.Framework;

public class BaseSkillSystemTests
{
    [SetUp]
    public void SetUp()
    {
        IAbilityScoreSystem.Register(new MockAbilityScoreSystem());
        IDataSystem.Register(new MockDataSystem());
        ILevelSystem.Register(new LevelSystem());
        IProficiencySystem.Register(new MockProficiencySystem());

        IDataSystem.Resolve().Create();
    }

    [TearDown]
    public void TearDown()
    {
        IAbilityScoreSystem.Reset();
        IDataSystem.Reset();
        ILevelSystem.Reset();
        IProficiencySystem.Reset();
    }

    [Test]
    public void Setup_TrainedEntity_AssignsCorrectSkillValue()
    {
        // Arrange
        var sut = new MockBaseSkillSystem(Skill.Athletics, AbilityScore.Attribute.Strength);
        var hero = new Entity(1);
        hero[AbilityScore.Attribute.Strength] = 18;
        hero.Level = 1;
        IProficiencySystem.Resolve().Set(hero, Skill.Athletics, Proficiency.Trained);

        // Act
        sut.Setup(hero);

        // Assert
        Assert.AreEqual(7, sut.Table[hero]); // 4 (Str) + 3 (Prof)
    }

    [Test]
    public void Setup_UntrainedEntity_AssignsCorrectSkillValue()
    {
        // Arrange
        var sut = new MockBaseSkillSystem(Skill.Athletics, AbilityScore.Attribute.Strength);
        var hero = new Entity(1);
        hero[AbilityScore.Attribute.Strength] = 12;
        hero.Level = 1;
        IProficiencySystem.Resolve().Set(hero, Skill.Athletics, Proficiency.Untrained);

        // Act
        sut.Setup(hero);

        // Assert
        Assert.AreEqual(1, sut.Table[hero]); // 1 (Str) + 0 (Prof)
    }
}

For the Test “Setup” I have injected my new mocks for the ability score system and proficiency system. I decided I would go ahead and just use the real level system though, which meant I needed to also inject a mock data system. All four dependencies are then cleared in the “TearDown” method.

I created two different tests, one for a hero that is trained in the skill, and one for a hero that is untrained in the skill. This allows me to test that the ability score modifier, and proficiency (including level when trained) are being included in the calculation correctly.

Head over to Unity and open up the Test Runner, then Run our new tests. They should pass successfully!

Summary

In this lesson we implemented a complex skill system. We can get and set skill values by an enum case, or we can let skill values be calculated based on things like an Entity’s ability scores and their proficiency. To be thorough we added some unit tests.

The lesson only shows one concrete skill system implementation – for the Athletics skill. Adding systems for the other skill types was left as an exercise for the reader. If you got stuck or just don’t feel like finishing, 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 *