I had planned for the next step in creating our hero party to be implementing the hero’s class. However, the class holds several bits of information around mechanics we still need to implement, so we will take a bit of a detour, starting with Saving Throws.
Overview
You may read about the Saving Throws game mechanic on the system reference doc. For a quick recap, there are three new proficiency stats: fortitude, reflex and will. These stats play a special role in determining your resistance to negative effects such as damage or disease.
To perform a saving throw, you would roll a d20, add the relevant boost based on your proficiency and attribute score modifier (and any other bonuses or penalties) and then compare the result against some difficulty check.
If you are curious, here are some spells that would trigger a saving throw. Each spell has its own rules around how the outcome of a saving throw applies to the effected target:
- Disintegrate – Fortitude save on target
- Glitterdust – Reflex save on entities within its target area
- Brain Drain – Will save on target
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.
Refactoring
Saving Throws are not technically skills (in a game mechanic definition sort of way), although the implementation of both is very similar. For example, you have a proficiency in skills like Acrobatics just like you have a proficiency in a saving throw like Fortitude. Each skill has a particular ability score modifier that is used to determine its value, and likewise each saving throw also has a particular ability score modifier used in the same way.
I don’t plan to extend the various skill systems to include saving throws though, because the game mechanics are still considered separate. Therefore I will need new systems that are similar to the skill systems but with their own name. At the moment, there is a ProficiencySystem used with Skills, which I would like to rename to be SkillProficiencySystem – I want to be more explicit with the name so it is less confusing about what it applies to.
Go ahead and open the ProficiencySystem.cs script. Many IDE’s have the ability to do things like renaming automatically. For example, in Visual Studio Community, I can right-click the name of my interface, IProficiencySystem, choose “Rename”, type the name I want (ISkillProficiencySystem), then confirm and save the changes. It will have already updated all of the occurrences of that interface name both in this file and in any other files that it is referenced. That includes changes in these files:
- SkillsProficiencyProvider
- BaseSkillSystem
- ProficiencySystem
- SkillsInjector
- BaseSkillSystemTests
- MockProficiencySystem
Next, we will do something similar by right-clicking the name of our class ProficiencySystem and do a refactor/rename to SkillProficiencySystem. Once again, it will change the name in this file as well as any other references, such as in the injector. Some IDE’s will be smart enough to also change the file name to match. If yours does not, then make sure to do so manually. While we are at it, rename the MockProficienySystem to be MockSkillProficienySystem.
The last change I want to make here is to move the Proficiency enum to its own file (by the same name), since it will be used in many files, and otherwise I would not know which file to find it within. Make sure that there are no compiler issues and then we can move on.
Saving Throw
Our first “new” code will be an enum for the SavingThrow. Add a new folder at Scripts -> Component named SavingThrows. Then Create a new script within that folder named SavingThrow and add the following:
public enum SavingThrow { Fortitude, Reflex, Will }
This will allow us to switch over the “types” of saving throws easily, and will be used very similarly to how the Skill enum was used.
Saving Throw Proficiency System
Now we can create our next proficiency system, this time for saving throws. Create a new script within the same folder named SavingThrowProficiencySystem and add the following:
public interface ISavingThrowProficiencySystem : IDependency<ISavingThrowProficiencySystem> { Proficiency Get(Entity entity, SavingThrow savingThrow); void Set(Entity entity, SavingThrow savingThrow, Proficiency value); } public class SavingThrowProficiencySystem : ISavingThrowProficiencySystem { public Proficiency Get(Entity entity, SavingThrow savingThrow) { return GetSystem(savingThrow).Get(entity); } public void Set(Entity entity, SavingThrow savingThrow, Proficiency value) { GetSystem(savingThrow).Set(entity, value); } IEntityTableSystem<Proficiency> GetSystem(SavingThrow savingThrow) { switch (savingThrow) { case SavingThrow.Fortitude: return IFortitudeProficiencySystem.Resolve(); case SavingThrow.Reflex: return IReflexProficiencySystem.Resolve(); case SavingThrow.Will: return IWillProficiencySystem.Resolve(); default: return null; } } }
I actually intend to make a separate proficiency system per type of saving throw, as you can see in the GetSystem method above. The purpose of this system therefore is to allow us to use the more specific systems in a general sense – by way of the SavingThrow enum. To help explain why I do this, imagine that I am putting together some kind of Spell as a game object asset, and I want to indicate that one of its effects triggers a saving throw. I can easily serialize a reference to the SavingThrow enum in that component, and then use this script to distribute any special handling to the individual system, each of which may know about special rules for additional bonuses or penalties.
Now let’s add the individual proficiency systems:
Fortitude Proficiency System
Add another script named FortitudeProficiencySystem to the same folder and add the following:
public partial class Data { public CoreDictionary<Entity, Proficiency> fortitudeProficiency = new CoreDictionary<Entity, Proficiency>(); } public interface IFortitudeProficiencySystem : IDependency<IFortitudeProficiencySystem>, IEntityTableSystem<Proficiency> { } public class FortitudeProficiencySystem : EntityTableSystem<Proficiency>, IFortitudeProficiencySystem { public override CoreDictionary<Entity, Proficiency> Table => IDataSystem.Resolve().Data.fortitudeProficiency; }
Reflex Proficiency System
Add another script named ReflexProficiencySystem to the same folder and add the following:
public partial class Data { public CoreDictionary<Entity, Proficiency> reflexProficiency = new CoreDictionary<Entity, Proficiency>(); } public interface IReflexProficiencySystem : IDependency<IReflexProficiencySystem>, IEntityTableSystem<Proficiency> { } public class ReflexProficiencySystem : EntityTableSystem<Proficiency>, IReflexProficiencySystem { public override CoreDictionary<Entity, Proficiency> Table => IDataSystem.Resolve().Data.reflexProficiency; }
Will Proficiency System
Add another script named WillProficiencySystem to the same folder and add the following:
public partial class Data { public CoreDictionary<Entity, Proficiency> willProficiency = new CoreDictionary<Entity, Proficiency>(); } public interface IWillProficiencySystem : IDependency<IWillProficiencySystem>, IEntityTableSystem<Proficiency> { } public class WillProficiencySystem : EntityTableSystem<Proficiency>, IWillProficiencySystem { public override CoreDictionary<Entity, Proficiency> Table => IDataSystem.Resolve().Data.willProficiency; }
Saving Throw System
We will also have a sort of wrapper system for all of the specific systems that handle combining proficiency with an ability score modifier, etc. Create a new script in the same folder named SavingThrowSystem and add the following:
using System; public interface ISavingThrowSystem : IDependency<ISavingThrowSystem> { void Set(Entity entity, SavingThrow savingThrow, int value); int Get(Entity entity, SavingThrow savingThrow); void SetupAllSavingThrows(Entity entity); void Setup(Entity entity, SavingThrow savingThrow); } public class SavingThrowSystem : ISavingThrowSystem { public void Set(Entity entity, SavingThrow savingThrow, int value) { GetSystem(savingThrow).Set(entity, value); } public int Get(Entity entity, SavingThrow savingThrow) { return GetSystem(savingThrow).Get(entity); } public void SetupAllSavingThrows(Entity entity) { foreach (SavingThrow savingThrow in Enum.GetValues(typeof(SavingThrow))) GetSystem(savingThrow).Setup(entity); } public void Setup(Entity entity, SavingThrow savingThrow) { GetSystem(savingThrow).Setup(entity); } IBaseSavingThrowSystem GetSystem(SavingThrow savingThrow) { switch (savingThrow) { case SavingThrow.Fortitude: return IFortitudeSystem.Resolve(); case SavingThrow.Reflex: return IReflexSystem.Resolve(); case SavingThrow.Will: return IWillSystem.Resolve(); default: return null; } } }
With this system I can either manually assign a value to saving throw scores for an entity (via Get or Set), or I can allow the various subsystems to calculate the score automatically using proficiency, ability score modifier, etc (via SetupAllSavingThrows). For our heroes I will let the scores be calculated, but it is possible that for some monsters or special cases I could just assign it directly. I could also see assigning it directly for unit tests.
We will also need to add each of the subsystems:
Base Saving Throw System
There is room to reuse some logic for these subsystems, so I will begin by creating a shared base class and interface. Create a new script in the same folder named BaseSavingThrowSystem and add the following:
public interface IBaseSavingThrowSystem : IEntityTableSystem<int> { void Setup(Entity entity); } public abstract class BaseSavingThrowSystem : EntityTableSystem<int>, IBaseSavingThrowSystem { protected abstract SavingThrow SavingThrow { 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 = ISavingThrowProficiencySystem.Resolve().Get(entity, SavingThrow); if (proficiency != Proficiency.Untrained) result += (int)proficiency * 2 + entity.Level; UnityEngine.Debug.Log(string.Format("SavingThrow: {0}, Value: {1}", SavingThrow.ToString(), result)); return result; } }
The base class is abstract, meaning I can’t create instances of it. It must be subclassed in order to be used, and each subclass will have to provide some of the needed information. In particular, each subclass will let the base class know which type of SavingThrow it is for, and what type of AbilityScore it uses in its calculations.
We won’t be doing a whole lot with Saving Throws yet, so the closest I am getting to a “Demo” is to put in a Debug Log after calculating the score. You can remove that after seeing that it all works correctly.
Fortitude System
Create another script in the same folder named FortitudeSystem and add the following:
public partial class Data { public CoreDictionary<Entity, int> fortitude = new CoreDictionary<Entity, int>(); } public interface IFortitudeSystem : IDependency<IFortitudeSystem>, IBaseSavingThrowSystem { } public class FortitudeSystem : BaseSavingThrowSystem, IFortitudeSystem { public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.fortitude; protected override SavingThrow SavingThrow => SavingThrow.Fortitude; protected override AbilityScore.Attribute Attribute => AbilityScore.Attribute.Constitution; } public partial struct Entity { public int Fortitude { get { return IFortitudeSystem.Resolve().Get(this); } set { IFortitudeSystem.Resolve().Set(this, value); } } }
This class inherits from the BaseSavingThrowSystem and is the explicit handler for calculating the Fortitude case of the saving throw. It works with the Consitution modifier. It adds the necessary overrides, and in the same file we have added the necessary data and entity extensions to make things easy to grab.
Reflex System
Create another script in the same folder named ReflexSystem and add the following:
public partial class Data { public CoreDictionary<Entity, int> reflex = new CoreDictionary<Entity, int>(); } public interface IReflexSystem : IDependency<IReflexSystem>, IBaseSavingThrowSystem { } public class ReflexSystem : BaseSavingThrowSystem, IReflexSystem { public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.reflex; protected override SavingThrow SavingThrow => SavingThrow.Reflex; protected override AbilityScore.Attribute Attribute => AbilityScore.Attribute.Dexterity; } public partial struct Entity { public int Reflex { get { return IReflexSystem.Resolve().Get(this); } set { IReflexSystem.Resolve().Set(this, value); } } }
This is our second saving throw system, for Reflex, and it works with the Dexterity score.
Will System
Add another script in the same folder named WillSystem and add the following:
public partial class Data { public CoreDictionary<Entity, int> will = new CoreDictionary<Entity, int>(); } public interface IWillSystem : IDependency<IWillSystem>, IBaseSavingThrowSystem { } public class WillSystem : BaseSavingThrowSystem, IWillSystem { public override CoreDictionary<Entity, int> Table => IDataSystem.Resolve().Data.will; protected override SavingThrow SavingThrow => SavingThrow.Will; protected override AbilityScore.Attribute Attribute => AbilityScore.Attribute.Wisdom; } public partial struct Entity { public int Will { get { return IWillSystem.Resolve().Get(this); } set { IWillSystem.Resolve().Set(this, value); } } }
This system handles the final saving throw, Will. It works with the Wisdom modifier.
Saving Throws Injector
We will need to inject all of our new systems. Create another script in the same folder named SavingThrowsInjector and add the following:
public class SavingThrowsInjector { public static void Inject() { IFortitudeProficiencySystem.Register(new FortitudeProficiencySystem()); IFortitudeSystem.Register(new FortitudeSystem()); IReflexProficiencySystem.Register(new ReflexProficiencySystem()); IReflexSystem.Register(new ReflexSystem()); ISavingThrowProficiencySystem.Register(new SavingThrowProficiencySystem()); ISavingThrowSystem.Register(new SavingThrowSystem()); IWillProficiencySystem.Register(new WillProficiencySystem()); IWillSystem.Register(new WillSystem()); } }
Then open up the ComponentInjector and add the following to its Inject method:
SavingThrowsInjector.Inject();
Saving Throws Proficiency Provider
In the near future we will be assigning the initial saving throw proficiency values based on the hero’s class. Just to see that things are working, we can go ahead and add another provider which we can attach to the basic Hero prefab. Create a new script at Scripts -> AttributeProvider named SavingThrowsProficiencyProvider and add the following:
using System.Collections.Generic; using UnityEngine; public class SavingThrowsProficiencyProvider : MonoBehaviour, IAttributeProvider { [System.Serializable] public struct ValuePair { public SavingThrow savingThrow; public Proficiency proficiency; } public List<ValuePair> valuePairs; public void Setup(Entity entity) { var system = ISavingThrowProficiencySystem.Resolve(); foreach (var pair in valuePairs) system.Set(entity, pair.savingThrow, pair.proficiency); } }
This single component will allow us to specify proficiency values for one or more of the various kinds of saving throws (all of them if we want).
Hero Prefab
To make use of the script we just made, open the asset at Objects -> EntityRecipe -> Hero. Attach a new SavingThrowsProficiencyProvider and then configure it with the following:
- Fortitude -> Trained
- Reflex -> Trained
- Will -> Expert
Create Hero Party Flow
Open the CreateHeroPartyFlow script and add the following to the end of the Play method’s for loop body
ISkillSystem.Resolve().SetupAllSkills(entity); ISavingThrowSystem.Resolve().SetupAllSavingThrows(entity);
This will cause the saving throw scores to be calculated, and also makes sure to calculate the skill scores (should have been added a while ago and was overlooked). If you run the game now, you should see the calculated saving throw values printed to the console. Example: “SavingThrow: Fortitude, Value: 4”. Note that the numbers are still a bit low because the ability scores contribute to the overall calculation and we are still missing the bonus that will be given based on applying a hero class.
Summary
In this lesson we implemented everything necessary to represent saving throw proficiency and score values. We provided some sample training to the hero recipe so we could see the values get calculated. In the future we will be able to use these scores to help roll for the success of evading danger or negative effects!
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!