3.1: Ability Score Enum
In most table top RPGs, the characters have different attributes that track how good (or bad) that character is at performing different actions. In Adventure Quest, these will be called Abilities and each Ability will have an Ability Score associated with it. In this chapter, you will create an AbilityScore
class to model these attributes.
Table of contents
00. Create a Feature Branch
You’re about to start a new feature! Before beginning, you should create a feature branch named {username}/character-creator
.
Creating a Branch with GitHub Desktop (Click to Expand)
- Select your project repository from the
Current repository
drop down. - Click the
Current branch
drop down. - Click
New branch
A dialog will open.
- Enter the name of the new branch (e.g.
username/character-creator
) - Click
Create branch
- Publish your branch to GitHub by clicking the
Publish branch
button.
What is an Ability
Much of what a character (or monster) is capable (or incapable) of doing in Adventure Quest will be determined by their abilities: Power, Speed, and Vitality.
In this chapter, you will write a program that supports Power, Speed, and Vitality. However, with a careful design, you should be capable of easily adding new abilities (which you will do in [3.4 Abilities Manifest Prefab]).
Ability Score
Each ability has a score which is an integer between 1 and 30 which can be used to determine if the character can use items, perform actions, or handle various situations. Here are a few examples:
- A suit of heavy plate mail armor requires a Power score
>= 16
. - A character with a Speed score
>= 15
may acquire the Dodge skill - When a character would be poisoned, if their Vitality score is
>= 14
, the damage is halved.
These types of checks can be made directly against an Ability Score.
Ability Score Modifier
Additionally, each Ability Score comes with a Modifier which determines a bonus or a penalty that a character receives when performing various actions using Dice Notation.
For example:
- A sword might deal
1d6 + Power
damage. - Fleeing from combat might require
1d20 + Speed >= 15
- When resting a character might gain
1d4 + Vitality
health.
The following table shows the relationship from a score to a modifier:
Score | Modifier | Score | Modifier |
---|---|---|---|
1 | -5 | 16-17 | +3 |
2-3 | -4 | 18-19 | +4 |
4-5 | -3 | 20-21 | +5 |
6-7 | -2 | 22-23 | +6 |
8-9 | -1 | 24-25 | +7 |
10-11 | +0 | 26-27 | +8 |
12-13 | +1 | 28-29 | +9 |
14-15 | +2 | 30 | +10 |
Defining an Ability Type
To begin, we will define an enumerated type to represent each of the different Ability
type.
- Create a new C# Script in your
Scripts/Model
folder namedAbility
- Replace the entire contents of the
Ability.cs
file with the following definition:
namespace AdventureQuest.Character
{
public enum Ability
{
Power,
Speed,
Vitality
}
}
That’s it! You’re done with the Ability
type!
So, what is an enum
? It is similar to the bool
type which can be true
or false
. However, in this case it is defined to have 3 possible options.
In short, an enumerated type is a type whose choices are one of a small list of possible options.
Organize Your Files
Currently, your Scripts/Model
folder contains three C# files, Die.cs
, DiceGroup.cs
, and Ability.cs
. As your project grows, the number of C# scripts will also grow and it will become important to keep them organized.
One common way to do this is to keep files with the same name spaces within the same folder.
- Create a
Dice
folder and moveDie.cs
andDiceGroup.cs
inside - Create a
Character
folder and moveAbility.cs
inside
Before continuing, complete this process for your Scripts/Controller
, and Tests/Model
folders.
Ability Score Class
With an Ability
type defined, think about how you might implement an AbilityScore
class.
- In your
Scripts/Model/Character
folder create a new C# Script calledAbilityScore
- It should be in the
AdventureQuest.Character
name space - It should not be a
MonoBehaviour
- It does not need to use any other name spaces
Think about the following questions, then expand the section below.
- What fields should an
AbilityScore
use internally to store state? - What properties should an
AbilityScore
expose publicly? - Which properties can be derived from the internal fields / other properties?
- What methods does an
AbilityScore
provide? - What parameters will the
AbilityScore
constructor accept? - How will the arguments passed to the constructor be validated?
AbilityScore Declaration (Click to Expand)
Just like DiceGroup
, there are many ways to implement an AbilityScore
class. If you chose a different set of properties, I’d love to hear about them.
I won’t try to claim that the properties I’ve chosen here are the best possible set of properties. But, I have attempted to choose properties that keep the AbilityScore
class as simple as possible.
namespace AdventureQuest.Character
{
public class AbilityScore
{
/// <summary>
/// The minimum possible score value
/// </summary>
public const int MIN = 1;
/// <summary>
/// The maximum possible score value
/// </summary>
public const int MAX = 30;
/// <summary>
/// Instantiates an <see cref="AbilityScore"/> specifying the <paramref name="ability"/> and <paramref name="score"/>.
/// </summary>
/// <exception cref="System.ArgumentException">If score < AbilityScore.Min or score > AbilityScore.Max</excpetion>
public AbilityScore(Ability ability, int score)
{
// TODO: Implement the constructor
}
/// <summary>
/// The <see cref="Ability"/> associated with this <see cref="AbilityScore"/>.
/// </summary>
public Ability Ability { get; }
/// <summary>
/// The value of this <see cref="AbilityScore"/>.
/// </summary>
public int Score { get; }
/// <summary>
/// The bonus / penalty that is applied when performing an <see cref="AbilityRoll"/> with this <see cref="AbilityScore"/>
/// </summary>
public int Modifier { get; }
}
}
This may be the first time you have seen a public const
. The const
keyword tells the compiler that the label (e.g. MIN
/MAX
) is a CONSTANT. That is, it is immutable and can never change. Defining MIN
and MAX
as const
values allows us to user them throughout the program rather than hard coding 1
and 30
.
Challenge: Implement the Ability Score class
Using the definition provided, can you finish the implementation of the AbilityScore
class?
To give you confidence that your implementation is correct, you should create a folder called Tests/Model/Character
and add the following AbilityScoreTest
to it.
Bonus: Can you implement the Modifier
property using an Expression body definition?
AbilityScoreTest (Click to Expand)
using NUnit.Framework;
namespace AdventureQuest.Character
{
public class AbilityScoreTest
{
[Test, Timeout(5000), Description("Tests the AbilityScore constructor")]
public void TestAbilityScoreConstructor()
{
AbilityScore power = new (Ability.Power, 15);
Assert.AreEqual(15, power.Score);
Assert.AreEqual(Ability.Power, power.Ability);
Assert.AreEqual(2, power.Modifier);
AbilityScore speed = new (Ability.Speed, AbilityScore.MIN);
Assert.AreEqual(1, speed.Score);
Assert.AreEqual(Ability.Speed, speed.Ability);
Assert.AreEqual(-5, speed.Modifier);
AbilityScore vitality = new (Ability.Vitality, AbilityScore.MAX);
Assert.AreEqual(30, vitality.Score);
Assert.AreEqual(Ability.Vitality, vitality.Ability);
Assert.AreEqual(10, vitality.Modifier);
}
[Test, Timeout(5000), Description("Tests that an AbilityScore will be between 1 and 30")]
public void TestConstructorArgumentException()
{
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Power, AbilityScore.MIN - 1));
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Speed, AbilityScore.MAX + 1));
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Vitality, AbilityScore.MIN - 10));
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Power, AbilityScore.MAX + 10));
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Speed, AbilityScore.MIN - 15));
Assert.Throws<System.ArgumentException>(() => new AbilityScore(Ability.Vitality, AbilityScore.MAX + 15));
}
}
}
Good Time to Commit
If you have not already done so, now would be a good time to make a commit. You just finished a feature. More specifically, you implemented an AbilityScore
class.
Committing with GitHub Desktop (Click to Expand)
- Ensure the files you would like to commit are checked in the
Changes
tab.
-
Enter a summary for your commit. Think of this as the subject line of an email. It should be SHORT and to the point. Aim to be less than 50 characters. It is good practice to prefix the commit with the type of work that was done. For example:
- A feature:
feat: Implemented Die class
- A chore:
chore: Added image assets to project
- A bug fix:
fix: Removed off by 1 error
- A work in progress:
wip: Partial implementation of DieGroup class
- A feature:
-
Add a description to your commit. This should provide additional details about what is included in the commit. For example:
This commit adds a Die class which models a multi-sided die providing an
interface with 2 properties: `Sides` and `LastRolled`. Additionally, it provides
a single method: `Roll()` which "rolls" the die and randomly selecting one of
the sides.
Additionally, added unit tests to test the Die class specification.
- When you’re ready, click the
Commit
button
- Lastly, push your commit to GitHub by clicking the
Push origin
button
What’s Next
With an AbilityScore
class in place you can start work on a Character Creation Scene
. In [3.2 Ability Score Label], you will make a new Prefab
defining how an AbilityScore
will be displayed in the Character Creation Scene
.
Join the Discussion
If you're stuck, have questions, or want to provide feedback, you can do so below. However, I ask that you please refrain from posting complete solutions to any of the challenges.
Before commenting, you will need to authorize giscus. Alternatively, you can add a comment directly on the GitHub Discussion Board.