Epub now contains character sheets information.

This commit is contained in:
Mark Wolfman
2021-07-11 09:13:55 -05:00
parent 50754f7d5d
commit 8be06d2ffe
17 changed files with 430 additions and 104 deletions
+10 -2
View File
@@ -248,7 +248,7 @@ class Character(Entity):
def clear(self):
# reset class-defined items
self.class_list = list()
self.weapons = list()
self._weapons = list()
self.magic_items = list()
self._saving_throw_proficiencies = tuple()
self.other_weapon_proficiencies = tuple()
@@ -850,7 +850,15 @@ class Character(Entity):
warning_message=msg,
)
# Save it to the array
self.weapons.append(ThisWeapon(wielder=self))
self._weapons.append(ThisWeapon(wielder=self))
@property
def weapons(self):
my_weapons = self._weapons.copy()
# Account for unarmed strike
if len(my_weapons) == 0 or hasattr(self, "Monk"):
my_weapons.append(weapons.Unarmed(wielder=self))
return my_weapons
@property
def hit_dice(self):
+1 -1
View File
@@ -16,7 +16,7 @@ class Champion(SubClass):
name = "Champion"
features_by_level = defaultdict(list)
features_by_level[3] = [features.ImprovedCritical]
features_by_level[7] = [features.RemarkableAthelete]
features_by_level[7] = [features.RemarkableAthlete]
features_by_level[10] = [features.AdditionalFightingStyle]
features_by_level[15] = [features.SuperiorCritical]
features_by_level[18] = [features.Survivor]
+24 -1
View File
@@ -70,7 +70,7 @@ class Entity(ABC):
gp = 0
pp = 0
equipment = ""
weapons = list()
_weapons = list()
magic_items = list()
armor = None
shield = None
@@ -87,3 +87,26 @@ class Entity(ABC):
def __init__(self):
pass
@property
def weapons(self):
return self._weapons.copy()
@property
def passive_wisdom(self):
return self.perception.modifier + 10
@property
def abilities(self):
return [self.strength, self.dexterity, self.constitution,
self.intelligence, self.wisdom, self.charisma]
@property
def skills(self):
return [self.acrobatics, self.animal_handling, self.arcana,
self.athletics, self.deception, self.history,
self.insight, self.intimidation, self.investigation,
self.medicine, self.nature, self.perception,
self.performance, self.persuasion, self.religion,
self.sleight_of_hand, self.stealth, self.survival,]
+1
View File
@@ -267,6 +267,7 @@ def to_heading_id(inpt: str) -> str:
return inpt.replace(" ", "-")
# Prepare the jinja environment
jinja_env = jinja_environment()
jinja_env.filters["rst_to_html"] = rst_to_html
+2 -2
View File
@@ -234,7 +234,7 @@ class ImprovedCritical(Feature):
source = "Fighter (Champion)"
class RemarkableAthelete(Feature):
class RemarkableAthlete(Feature):
"""Starting at 7th level, you can add half your proficiency bonus (round up)
to any Strength, Dexterity, or Constitution check you make that doesn't
already use your proficiency bonus.
@@ -244,7 +244,7 @@ class RemarkableAthelete(Feature):
"""
name = "Remarkable Athelete"
name = "Remarkable Athlete"
source = "Fighter (Champion)"
+19 -21
View File
@@ -57,7 +57,7 @@ def create_character_pdf_template(character, basename, flatten=False):
"AC": str(character.armor_class),
"Initiative": str(character.initiative),
"Speed": str(character.speed),
"Passive": 10 + character.perception,
"Passive": character.passive_wisdom,
# Saving throws (proficiencies handled later)
"ST Strength": mod_str(character.strength.saving_throw),
"ST Dexterity": mod_str(character.dexterity.saving_throw),
@@ -66,24 +66,24 @@ def create_character_pdf_template(character, basename, flatten=False):
"ST Wisdom": mod_str(character.wisdom.saving_throw),
"ST Charisma": mod_str(character.charisma.saving_throw),
# Skills (proficiencies handled below)
"Acrobatics": mod_str(character.acrobatics),
"Animal": mod_str(character.animal_handling),
"Arcana": mod_str(character.arcana),
"Athletics": mod_str(character.athletics),
"Deception ": mod_str(character.deception),
"History ": mod_str(character.history),
"Insight": mod_str(character.insight),
"Intimidation": mod_str(character.intimidation),
"Investigation ": mod_str(character.investigation),
"Medicine": mod_str(character.medicine),
"Nature": mod_str(character.nature),
"Perception ": mod_str(character.perception),
"Performance": mod_str(character.performance),
"Persuasion": mod_str(character.persuasion),
"Religion": mod_str(character.religion),
"SleightofHand": mod_str(character.sleight_of_hand),
"Stealth ": mod_str(character.stealth),
"Survival": mod_str(character.survival),
"Acrobatics": mod_str(character.acrobatics.modifier),
"Animal": mod_str(character.animal_handling.modifier),
"Arcana": mod_str(character.arcana.modifier),
"Athletics": mod_str(character.athletics.modifier),
"Deception ": mod_str(character.deception.modifier),
"History ": mod_str(character.history.modifier),
"Insight": mod_str(character.insight.modifier),
"Intimidation": mod_str(character.intimidation.modifier),
"Investigation ": mod_str(character.investigation.modifier),
"Medicine": mod_str(character.medicine.modifier),
"Nature": mod_str(character.nature.modifier),
"Perception ": mod_str(character.perception.modifier),
"Performance": mod_str(character.performance.modifier),
"Persuasion": mod_str(character.persuasion.modifier),
"Religion": mod_str(character.religion.modifier),
"SleightofHand": mod_str(character.sleight_of_hand.modifier),
"Stealth ": mod_str(character.stealth.modifier),
"Survival": mod_str(character.survival.modifier),
# Hit points
"HDTotal": character.hit_dice,
"HPMax": str(character.hp_max),
@@ -150,8 +150,6 @@ def create_character_pdf_template(character, basename, flatten=False):
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
]
if len(character.weapons) == 0 or hasattr(character, "Monk"):
character.wield_weapon("unarmed")
for _fields, weapon in zip(weapon_fields, character.weapons):
name_field, atk_field, dmg_field = _fields
fields[name_field] = weapon.name
+2 -1
View File
@@ -3,7 +3,7 @@ import re
from jinja2 import Environment, PackageLoader
from dungeonsheets.stats import mod_str
from dungeonsheets.stats import mod_str, ability_mod_str
# A dice string, with optional backticks: ``1d6 + 3``
@@ -31,4 +31,5 @@ def jinja_environment():
variable_end_string="]]",
)
jinja_env.filters["mod_str"] = mod_str
jinja_env.filters["ability_mod_str"] = ability_mod_str
return jinja_env
@@ -0,0 +1 @@
<h1 class="background">Background</h1>
@@ -0,0 +1,119 @@
<h1 class="character-sheet">Character Sheet</h1>
<!-- Identity -->
<dl class="character-details">
<dt>Character Name</dt>
<dd>[[ character.name ]]</dd>
<dt>Class &amp; Level</dt>
<dd>[[ character.classes_and_levels ]]</dd>
<dt>Background</dt>
<dd>[[ character.background ]]</dd>
<dt>Player Name</dt>
<dd>[[ character.player_name ]]</dd>
<dt>Race</dt>
<dd>[[ character.race ]]</dd>
<dt>Alignment</dt>
<dd>[[ character.alignment ]]</dd>
<dt>Experience Points</dt>
<dd>[[ character.xp ]]</dd>
<dt>Inspiration</dt>
<dd>[% if character.inspiration %]✓[% else %]&ndash;[% endif %]</dd>
</dl>
<dl class="combat-stats">
<dt>Armor Class</dt>
<dd>[[ character.armor_class ]]</dd>
<dt>Initiative</dt>
<dd>[[ character.initiative ]]</dd>
<dt>Speed</dt>
<dd>[[ character.speed ]]</dd>
<dt>Passive Wisdom (Perception)</dt>
<dd>[[ character.passive_wisdom ]]</dd>
</dl>
<dl class="hit-points">
<dt>Hit Point Maximum</dt>
<dd>[[ character.hp_max ]]</dd>
<dt>Current Hit Points</dt>
<dd>[[ character.hp_current ]]</dd>
<dt>Temporary Hit Points</dt>
<dd>[% if character.hp_temp > 0 %][[ character.hp_temp ]][% endif %]</dd>
<dt>Hit Dice Total</dt>
<dd>[[ character.hit_dice ]]</dd>
</dl>
<!-- Character abilities, saving throws and skill modifiers -->
<table class="character-abilities">
<tr>
<th>Ability</th>
<th>Mod</th>
<th colspan="2">Saving<br />Throw</th>
</tr>
[% for ability in character.abilities %]
<tr>
<td>[[ ability.name | capitalize ]]</td>
<td>[[ ability.modifier | mod_str ]] ([[ ability.value ]])</td>
<td>[% if ability.name in character.saving_throw_proficiencies %]✓[% endif %]</td>
<td>[[ character.strength.saving_throw | mod_str ]]</td>
</tr>
[% endfor %]
</table>
<table class"character-skills">
<tr>
<th>Skill</th>
<th>Mod</th>
</tr>
[% for skill in character.skills %]
<tr>
<td>[[ skill ]]</td>
<td>[[ skill.modifier | mod_str ]]</td>
<td>
[% if skill.is_expertise == 1 %]✓✓
[% elif skill.is_proficient %]✓
[% elif skill.is_remarkable_athlete %]◓
[% elif skill.is_jack_of_all_trades %]◒
[% endif %]
</td>
</tr>
[% endfor %]
</table>
<table class="attacks-and-spellcasting">
<tr>
<th>Name</th>
<th>Atk Bonus</th>
<th>Damage/Type</th>
</tr>
[% for weapon in character.weapons %]
<tr>
<td>[[ weapon.name ]]</td>
<td>[[ weapon.attack_modifier | mod_str ]]</td>
<td>[[ weapon.damage ]] / [[ weapon.damage_type ]]</td>
</tr>
[% endfor %]
</table>
<dl class="proficiences">
<dt>Proficiences</dt>
<dd>[[ character.proficiencies_text ]]</dd>
<dt>Languages</dt>
<dd>[[ character.languages ]]</dd>
</dl>
<h2 id="inventory">Inventory</h2>
<ul class="inventory">
<li>[[ character.cp ]] CP</li>
<li>[[ character.sp ]] SP</li>
<li>[[ character.ep ]] EP</li>
<li>[[ character.gp ]] GP</li>
<li>[[ character.pp ]] PP</li>
[% set inventory_items = character.magic_items_text.split(',') %]
[% for item in inventory_items %]
<li>[[ item ]]</li>
[% endfor %]
[% set inventory_items = character.equipment.split(',') %]
[% for item in inventory_items %]
<li>[[ item ]]</li>
[% endfor %]
</ul>
@@ -3,6 +3,29 @@ h1, h2, h3, h4, h5, h6 {
}
/* End fancy decorations */
/* Dictionary lists for showing stats, etc */
dt {
float: left;
clear: left;
text-align: right;
font-weight: bold;
}
dt::after {
content: ":";
}
dd {
padding: 0 0 0.5em 0;
}
dl.character-details dt {
width: 200px;
}
dl.character-details dd {
width: 200px;
margin: 0 0 0 210px;
}
.known-beast-disabled {
color: lightgrey;
}
@@ -44,7 +44,7 @@
<tr>
<td>[[ member.name[:28] ]]</td>
<td>[[ member.armor_class ]]</td>
<td>[[ member.perception + 10 ]]</td>
<td>[[ member.perception.modifier + 10 ]]</td>
<td>[% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %]
@@ -29,7 +29,7 @@
[% for member in party %]
[[ member.name[:28] ]]
& [[ member.armor_class ]]
& [[ member.perception + 10 ]]
& [[ member.perception.modifier + 10 ]]
& [% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %]
@@ -57,7 +57,7 @@
[% for member in party %]
[[ member.name[:28] ]]
& [[ member.armor_class ]]
& [[ member.perception + 10 ]]
& [[ member.perception.modifier + 10 ]]
& [% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %]
@@ -0,0 +1 @@
<h1 class="spell-list">Spell List</h1>
+98 -44
View File
@@ -9,7 +9,7 @@ import re
from pathlib import Path
from multiprocessing import Pool, cpu_count
from itertools import product
from typing import Union, Sequence, Optional
from typing import Union, Sequence, Optional, Literal, List
from dungeonsheets import (
character as _char,
@@ -70,6 +70,9 @@ class CharacterRenderer():
return template.render(character=character,
use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS)
create_character_sheet_content = CharacterRenderer("character_sheet_template.{suffix}")
create_spell_list_content = CharacterRenderer("spell_list_template.{suffix}")
create_background_content = CharacterRenderer("background_template.{suffix}")
create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}")
create_features_content = CharacterRenderer("features_template.{suffix}")
@@ -192,7 +195,7 @@ def make_gm_sheet(
gm_file = Path(gm_file)
basename = gm_file.stem
gm_props = readers.read_sheet_file(gm_file)
session_title = gm_props.get("session_title", f"GM Notes: {basename}")
session_title = gm_props.pop("session_title", f"GM Notes: {basename}")
# Create the intro tex
content_suffix = format_suffixes[output_format]
content = [
@@ -295,6 +298,96 @@ def make_gm_sheet(
)
def make_character_content(
character: Character,
content_format: Literal["tex", "html"],
fancy_decorations: bool = False,) -> List[str]:
"""Prepare the inner content for a character sheet.
This will produce a fully renderable document, suitable for
passing to routines in either the ``epub`` or ``latex``
modules. If *content_format* is ``"html"``, the returned content
is just the portion that would be found inside the
``<body></body>`` tag.
If *content_format* is ``"tex"``, the content returned will not
include the character, spell list, or biography sheets, since
these are currently processed through fillable PDFs.
Parameters
----------
character
The character to render content for.
content_format
Which markup syntax to use.
fancy_decorations
Use fancy page layout and decorations for extra sheets, namely
the dnd style file for *tex*, or extended CSS for *html*.
Returns
-------
content
The list of rendered character sheet contents for *character* in
markup format *content_format*.
"""
# Preamble, empty for HTML
content = [
jinja_env.get_template(f"preamble.{content_format}").render(
use_dnd_decorations=fancy_decorations,
title="Features, Magical Items and Spells",
)
]
# Make the character sheet, and background pages if producing HTML
if content_format != "tex":
content.append(create_character_sheet_content(character,
content_suffix=content_format,
use_dnd_decorations=fancy_decorations))
content.append(create_spell_list_content(character,
content_suffix=content_format,
use_dnd_decorations=fancy_decorations)
)
content.append(create_background_content(character,
content_suffix=content_format,
use_dnd_decorations=fancy_decorations)
)
# Create a list of subcasses, features, spells, etc
if character.subclasses:
content.append(create_subclasses_content(character,
content_suffix=content_format,
use_dnd_decorations=fancy_decorations)
)
if character.features:
content.append(
create_features_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
if character.magic_items:
content.append(
create_magic_items_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
if character.is_spellcaster:
content.append(
create_spellbook_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
if len(getattr(character, "infusions", [])) > 0:
content.append(
create_infusions_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
# Create a list of Druid wild_shapes
if len(getattr(character, "all_wild_shapes", [])) > 0:
content.append(
create_druid_shapes_content(character, content_suffix=content_format, use_dnd_decorations=fancy_decorations)
)
# Postamble, empty for HTML
content.append(
jinja_env.get_template(f"postamble.{content_format}").render(
use_dnd_decorations=fancy_decorations
)
)
return content
def make_character_sheet(
char_file: Union[str, Path],
character: Optional[Character] = None,
@@ -336,12 +429,6 @@ def make_character_sheet(
pages = []
# Prepare the tex/html content
content_suffix = format_suffixes[output_format]
content = [
jinja_env.get_template(f"preamble.{content_suffix}").render(
use_dnd_decorations=fancy_decorations,
title="Features, Magical Items and Spells",
)
]
# Start of PDF gen
char_pdf = create_character_pdf_template(
character=character, basename=char_base, flatten=flatten
@@ -360,43 +447,10 @@ def make_character_sheet(
sheets.append(spell_base + ".pdf")
# end of PDF gen
features_base = "{:s}_features".format(basename)
# Create a list of subcasses
if character.subclasses:
content.append( create_subclasses_content(character,
content_suffix=content_suffix,
use_dnd_decorations=fancy_decorations) )
# Create a list of features and magic items
if character.features:
content.append(
create_features_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
)
if character.magic_items:
content.append(
create_magic_items_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
)
# Create a list of spells
if character.is_spellcaster:
content.append(
create_spellbook_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
)
# Create a list of Artificer infusions
if getattr(character, "infusions", []):
content.append(
create_infusions_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
)
# Create a list of Druid wild_shapes
if getattr(character, "wild_shapes", []):
content.append(
create_druid_shapes_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
)
content.append(
jinja_env.get_template(f"postamble.{content_suffix}").render(
use_dnd_decorations=fancy_decorations
)
)
content = make_character_content(character=character,
content_format=content_suffix,
fancy_decorations=fancy_decorations)
# Typeset combined LaTeX file
if output_format == "pdf":
try:
+90 -26
View File
@@ -17,7 +17,7 @@ from dungeonsheets.features import (
NaturalExplorerRevised,
QuickDraw,
RakishAudacity,
RemarkableAthelete,
RemarkableAthlete,
SeaSoul,
SoulOfTheForge,
SuperiorMobility,
@@ -35,7 +35,11 @@ def mod_str(modifier):
return "{:+d}".format(modifier)
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))
def ability_mod_str(character, ability):
return mod_str(getattr(character, ability).modifier)
AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw", "name"))
class Ability:
@@ -68,7 +72,7 @@ class Ability:
if is_proficient:
saving_throw += entity.proficiency_bonus
# Create the named tuple
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw, name=self.ability_name)
return value
def __set__(self, entity, val):
@@ -78,38 +82,98 @@ class Ability:
class Skill:
"""An ability-based skill, such as athletics."""
"""An ability-based skill, such as athletics.
Attributes
----------
ability_name:
The name of the ability, as a python-compatible string.
skill_name:
The name of the base ability that determines the skill
modifier.
is_proficient
Bool that describes if the owner is proficient in this skill.
is_expertise
Bool that describes if the owner has expertise in this skill.
is_jack_of_all_trades
Bool that describes if this skill benefits from Jack of All
Trades feature (False if already proficient).
is_remarkable_athlete
Bool that describes if this skill benefits from Remarkable
Athlete feature (False if already proficient).
modifier
The base ability modifier, after relevant proficiency bonuses
have been applied.
proficiency_modifier
The bonus that is applied to the base ability. Usually the same
as the owner's proficiency bonus if ``is_proficient`` is True,
but can be different based on class features.,
"""
ability_name = ""
skill_name = ""
entity = None
def __init__(self, ability):
self.ability_name = ability
def __set_name__(self, entity, name):
def __set_name__(self, owner, name):
self.skill_name = name.lower().replace("_", " ")
self.character = entity
def __get__(self, entity, owner):
log.debug("Getting skill '%s' for '%s'", self.skill_name, entity.name)
ability = getattr(entity, self.ability_name)
modifier = ability.modifier
# Check for proficiency
proficiencies = [p.replace("_", " ") for p in entity.skill_proficiencies]
is_proficient = self.skill_name in proficiencies
log.debug(
"%s is proficient in %s: %s", entity.name, self.skill_name, is_proficient
)
if is_proficient:
modifier += entity.proficiency_bonus
elif entity.has_feature(JackOfAllTrades):
modifier += entity.proficiency_bonus // 2
elif entity.has_feature(RemarkableAthelete):
if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
modifier += ceil(entity.proficiency_bonus / 2.0)
self.entity = entity
return self
def __str__(self):
return self.skill_name.title()
@property
def is_remarkable_athlete(self):
already_proficient = (self.is_proficient or self.is_expertise)
if self.entity.has_feature(RemarkableAthlete) and not already_proficient:
return True
else:
return False
@property
def is_jack_of_all_trades(self):
already_proficient = (self.is_proficient or self.is_expertise or self.is_remarkable_athlete)
if self.entity.has_feature(JackOfAllTrades) and not already_proficient:
return True
else:
return False
@property
def is_proficient(self):
# Check for proficiency
proficiencies = [p.replace("_", " ") for p in self.entity.skill_proficiencies]
is_proficient = self.skill_name in proficiencies
return is_proficient
@property
def is_expertise(self):
return self.skill_name in self.entity.skill_expertise
@property
def proficiency_modifier(self):
modifier = 0
if self.is_proficient:
modifier += self.entity.proficiency_bonus
if self.is_remarkable_athlete:
modifier += ceil(self.entity.proficiency_bonus / 2.0)
if self.is_jack_of_all_trades:
modifier += self.entity.proficiency_bonus // 2
# Check for expertise
is_expert = self.skill_name in entity.skill_expertise
if is_expert:
modifier += entity.proficiency_bonus
log.info("'%s' modifier for '%s': %d", self.skill_name, entity.name, modifier)
if self.is_expertise:
modifier += self.entity.proficiency_bonus
return modifier
@property
def modifier(self):
ability = getattr(self.entity, self.ability_name)
modifier = ability.modifier + self.proficiency_modifier
log.info("%s modifier for '%s': %d", self, self.entity.name, modifier)
return modifier
+29
View File
@@ -56,11 +56,40 @@ class EpubOutputTestCase(unittest.TestCase):
gm_epub = Path(f"{GMFILE.stem}.epub").resolve()
char_epub = Path(f"{CHARFILE.stem}.epub").resolve()
def new_character(self):
char = character.Character(
name="Dr. Who",
classes=["Monk", "Druid", "Artificer"],
levels=[1, 1, 1],
subclasses=["way of the open hand", None, None],
magic_items=["cloak of protection"],
spells=["invisibility"],
wild_shapes=["crocodile"],
infusions=["boots of the winding path"]
)
return char
def tearDown(self):
for f in [self.gm_epub, self.char_epub]:
if f.exists():
f.unlink()
def test_character_html_content(self):
my_char = self.new_character()
html = make_sheets.make_character_content(character=my_char,
content_format="html")
html = "".join(html)
# Make sure the various sections get rendered
self.assertIn("Subclasses</h1>", html)
self.assertIn("Features</h1>", html)
self.assertIn("Magic Items</h1>", html)
self.assertIn("Spells</h1>", html)
self.assertIn("Infusions</h1>", html)
self.assertIn("Known Beasts</h1>", html)
# Check the character sheet
self.assertIn("Character Sheet</h1>", html)
self.assertIn("Dr. Who", html)
def test_gm_file_created(self):
# Check that a file is created once the function is run
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub")
+7 -3
View File
@@ -77,10 +77,14 @@ class TestStats(TestCase):
proficiency_bonus = 2
my_class = MyClass()
self.assertEqual(my_class.acrobatics, 2)
self.assertEqual(str(my_class.acrobatics), "Acrobatics")
self.assertEqual(my_class.acrobatics.modifier, 2)
self.assertEqual(str(my_class.sleight_of_hand), "Sleight Of Hand")
# Check for a proficiency
my_class.skill_proficiencies = ["acrobatics"]
self.assertEqual(my_class.acrobatics, 4)
self.assertTrue(my_class.acrobatics.is_proficient)
self.assertEqual(my_class.acrobatics.proficiency_modifier, 2)
self.assertEqual(my_class.acrobatics.modifier, 4)
# Check for a proficiency with spaces in the name
my_class.skill_proficiencies = ["sleight_of_hand"]
self.assertEqual(my_class.sleight_of_hand, 4)
self.assertEqual(my_class.sleight_of_hand.modifier, 4)