diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py
index afe28ca..3b14de3 100644
--- a/dungeonsheets/character.py
+++ b/dungeonsheets/character.py
@@ -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,8 +850,16 @@ 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):
"""What type and how many dice to use for re-gaining hit points.
diff --git a/dungeonsheets/classes/fighter.py b/dungeonsheets/classes/fighter.py
index 354dc10..17c4237 100644
--- a/dungeonsheets/classes/fighter.py
+++ b/dungeonsheets/classes/fighter.py
@@ -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]
diff --git a/dungeonsheets/entity.py b/dungeonsheets/entity.py
index bcf7a12..a33b6da 100644
--- a/dungeonsheets/entity.py
+++ b/dungeonsheets/entity.py
@@ -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,]
+
diff --git a/dungeonsheets/epub.py b/dungeonsheets/epub.py
index 27442c4..be3c80f 100644
--- a/dungeonsheets/epub.py
+++ b/dungeonsheets/epub.py
@@ -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
diff --git a/dungeonsheets/features/fighter.py b/dungeonsheets/features/fighter.py
index a0fa3e2..2626dad 100644
--- a/dungeonsheets/features/fighter.py
+++ b/dungeonsheets/features/fighter.py
@@ -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)"
diff --git a/dungeonsheets/fill_pdf_template.py b/dungeonsheets/fill_pdf_template.py
index 838955c..1111d0e 100644
--- a/dungeonsheets/fill_pdf_template.py
+++ b/dungeonsheets/fill_pdf_template.py
@@ -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
diff --git a/dungeonsheets/forms.py b/dungeonsheets/forms.py
index 8cef656..0cae01a 100644
--- a/dungeonsheets/forms.py
+++ b/dungeonsheets/forms.py
@@ -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
diff --git a/dungeonsheets/forms/background_template.html b/dungeonsheets/forms/background_template.html
new file mode 100644
index 0000000..911ce62
--- /dev/null
+++ b/dungeonsheets/forms/background_template.html
@@ -0,0 +1 @@
+
Background
diff --git a/dungeonsheets/forms/character_sheet_template.html b/dungeonsheets/forms/character_sheet_template.html
new file mode 100644
index 0000000..6b3f9ae
--- /dev/null
+++ b/dungeonsheets/forms/character_sheet_template.html
@@ -0,0 +1,119 @@
+Character Sheet
+
+
+
+ - Character Name
+ - [[ character.name ]]
+ - Class & Level
+ - [[ character.classes_and_levels ]]
+ - Background
+ - [[ character.background ]]
+ - Player Name
+ - [[ character.player_name ]]
+ - Race
+ - [[ character.race ]]
+ - Alignment
+ - [[ character.alignment ]]
+ - Experience Points
+ - [[ character.xp ]]
+ - Inspiration
+ - [% if character.inspiration %]✓[% else %]–[% endif %]
+
+
+
+ - Armor Class
+ - [[ character.armor_class ]]
+ - Initiative
+ - [[ character.initiative ]]
+ - Speed
+ - [[ character.speed ]]
+ - Passive Wisdom (Perception)
+ - [[ character.passive_wisdom ]]
+
+
+
+ - Hit Point Maximum
+ - [[ character.hp_max ]]
+ - Current Hit Points
+ - [[ character.hp_current ]]
+ - Temporary Hit Points
+ - [% if character.hp_temp > 0 %][[ character.hp_temp ]][% endif %]
+ - Hit Dice Total
+ - [[ character.hit_dice ]]
+
+
+
+
+
+ | Ability |
+ Mod |
+ Saving Throw |
+
+ [% for ability in character.abilities %]
+
+ | [[ ability.name | capitalize ]] |
+ [[ ability.modifier | mod_str ]] ([[ ability.value ]]) |
+ [% if ability.name in character.saving_throw_proficiencies %]✓[% endif %] |
+ [[ character.strength.saving_throw | mod_str ]] |
+
+ [% endfor %]
+
+
+
+
+ | Skill |
+ Mod |
+
+ [% for skill in character.skills %]
+
+ | [[ skill ]] |
+ [[ skill.modifier | mod_str ]] |
+
+ [% if skill.is_expertise == 1 %]✓✓
+ [% elif skill.is_proficient %]✓
+ [% elif skill.is_remarkable_athlete %]◓
+ [% elif skill.is_jack_of_all_trades %]◒
+ [% endif %]
+ |
+
+ [% endfor %]
+
+
+
+
+ | Name |
+ Atk Bonus |
+ Damage/Type |
+
+ [% for weapon in character.weapons %]
+
+ | [[ weapon.name ]] |
+ [[ weapon.attack_modifier | mod_str ]] |
+ [[ weapon.damage ]] / [[ weapon.damage_type ]] |
+
+ [% endfor %]
+
+
+
+ - Proficiences
+ - [[ character.proficiencies_text ]]
+ - Languages
+ - [[ character.languages ]]
+
+
+Inventory
+
+ - [[ character.cp ]] CP
+ - [[ character.sp ]] SP
+ - [[ character.ep ]] EP
+ - [[ character.gp ]] GP
+ - [[ character.pp ]] PP
+ [% set inventory_items = character.magic_items_text.split(',') %]
+ [% for item in inventory_items %]
+ - [[ item ]]
+ [% endfor %]
+ [% set inventory_items = character.equipment.split(',') %]
+ [% for item in inventory_items %]
+ - [[ item ]]
+ [% endfor %]
+
diff --git a/dungeonsheets/forms/dungeonsheets_epub.css b/dungeonsheets/forms/dungeonsheets_epub.css
index 2038e7f..cf6d59d 100644
--- a/dungeonsheets/forms/dungeonsheets_epub.css
+++ b/dungeonsheets/forms/dungeonsheets_epub.css
@@ -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;
}
diff --git a/dungeonsheets/forms/party_summary_template.html b/dungeonsheets/forms/party_summary_template.html
index 511d3c8..3f8ff24 100644
--- a/dungeonsheets/forms/party_summary_template.html
+++ b/dungeonsheets/forms/party_summary_template.html
@@ -44,7 +44,7 @@
| [[ 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 %]
diff --git a/dungeonsheets/forms/party_summary_template.tex b/dungeonsheets/forms/party_summary_template.tex
index fbebc2b..fa25c81 100644
--- a/dungeonsheets/forms/party_summary_template.tex
+++ b/dungeonsheets/forms/party_summary_template.tex
@@ -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 %]
diff --git a/dungeonsheets/forms/spell_list_template.html b/dungeonsheets/forms/spell_list_template.html
new file mode 100644
index 0000000..1c9765f
--- /dev/null
+++ b/dungeonsheets/forms/spell_list_template.html
@@ -0,0 +1 @@
+Spell List
diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py
index 040a0ec..f5d1308 100755
--- a/dungeonsheets/make_sheets.py
+++ b/dungeonsheets/make_sheets.py
@@ -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
+ ```` 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:
diff --git a/dungeonsheets/stats.py b/dungeonsheets/stats.py
index 465e9b5..442f410 100644
--- a/dungeonsheets/stats.py
+++ b/dungeonsheets/stats.py
@@ -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
diff --git a/tests/test_make_sheets.py b/tests/test_make_sheets.py
index 800f727..73ff1ca 100644
--- a/tests/test_make_sheets.py
+++ b/tests/test_make_sheets.py
@@ -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", html)
+ self.assertIn("Features", html)
+ self.assertIn("Magic Items", html)
+ self.assertIn("Spells", html)
+ self.assertIn("Infusions", html)
+ self.assertIn("Known Beasts", html)
+ # Check the character sheet
+ self.assertIn("Character Sheet", 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")
diff --git a/tests/test_stats.py b/tests/test_stats.py
index a99d632..7ecfe61 100644
--- a/tests/test_stats.py
+++ b/tests/test_stats.py
@@ -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)
|