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): def clear(self):
# reset class-defined items # reset class-defined items
self.class_list = list() self.class_list = list()
self.weapons = list() self._weapons = list()
self.magic_items = list() self.magic_items = list()
self._saving_throw_proficiencies = tuple() self._saving_throw_proficiencies = tuple()
self.other_weapon_proficiencies = tuple() self.other_weapon_proficiencies = tuple()
@@ -850,8 +850,16 @@ class Character(Entity):
warning_message=msg, warning_message=msg,
) )
# Save it to the array # 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 @property
def hit_dice(self): def hit_dice(self):
"""What type and how many dice to use for re-gaining hit points. """What type and how many dice to use for re-gaining hit points.
+1 -1
View File
@@ -16,7 +16,7 @@ class Champion(SubClass):
name = "Champion" name = "Champion"
features_by_level = defaultdict(list) features_by_level = defaultdict(list)
features_by_level[3] = [features.ImprovedCritical] 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[10] = [features.AdditionalFightingStyle]
features_by_level[15] = [features.SuperiorCritical] features_by_level[15] = [features.SuperiorCritical]
features_by_level[18] = [features.Survivor] features_by_level[18] = [features.Survivor]
+24 -1
View File
@@ -70,7 +70,7 @@ class Entity(ABC):
gp = 0 gp = 0
pp = 0 pp = 0
equipment = "" equipment = ""
weapons = list() _weapons = list()
magic_items = list() magic_items = list()
armor = None armor = None
shield = None shield = None
@@ -87,3 +87,26 @@ class Entity(ABC):
def __init__(self): def __init__(self):
pass 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(" ", "-") return inpt.replace(" ", "-")
# Prepare the jinja environment # Prepare the jinja environment
jinja_env = jinja_environment() jinja_env = jinja_environment()
jinja_env.filters["rst_to_html"] = rst_to_html jinja_env.filters["rst_to_html"] = rst_to_html
+2 -2
View File
@@ -234,7 +234,7 @@ class ImprovedCritical(Feature):
source = "Fighter (Champion)" source = "Fighter (Champion)"
class RemarkableAthelete(Feature): class RemarkableAthlete(Feature):
"""Starting at 7th level, you can add half your proficiency bonus (round up) """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 to any Strength, Dexterity, or Constitution check you make that doesn't
already use your proficiency bonus. already use your proficiency bonus.
@@ -244,7 +244,7 @@ class RemarkableAthelete(Feature):
""" """
name = "Remarkable Athelete" name = "Remarkable Athlete"
source = "Fighter (Champion)" 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), "AC": str(character.armor_class),
"Initiative": str(character.initiative), "Initiative": str(character.initiative),
"Speed": str(character.speed), "Speed": str(character.speed),
"Passive": 10 + character.perception, "Passive": character.passive_wisdom,
# Saving throws (proficiencies handled later) # Saving throws (proficiencies handled later)
"ST Strength": mod_str(character.strength.saving_throw), "ST Strength": mod_str(character.strength.saving_throw),
"ST Dexterity": mod_str(character.dexterity.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 Wisdom": mod_str(character.wisdom.saving_throw),
"ST Charisma": mod_str(character.charisma.saving_throw), "ST Charisma": mod_str(character.charisma.saving_throw),
# Skills (proficiencies handled below) # Skills (proficiencies handled below)
"Acrobatics": mod_str(character.acrobatics), "Acrobatics": mod_str(character.acrobatics.modifier),
"Animal": mod_str(character.animal_handling), "Animal": mod_str(character.animal_handling.modifier),
"Arcana": mod_str(character.arcana), "Arcana": mod_str(character.arcana.modifier),
"Athletics": mod_str(character.athletics), "Athletics": mod_str(character.athletics.modifier),
"Deception ": mod_str(character.deception), "Deception ": mod_str(character.deception.modifier),
"History ": mod_str(character.history), "History ": mod_str(character.history.modifier),
"Insight": mod_str(character.insight), "Insight": mod_str(character.insight.modifier),
"Intimidation": mod_str(character.intimidation), "Intimidation": mod_str(character.intimidation.modifier),
"Investigation ": mod_str(character.investigation), "Investigation ": mod_str(character.investigation.modifier),
"Medicine": mod_str(character.medicine), "Medicine": mod_str(character.medicine.modifier),
"Nature": mod_str(character.nature), "Nature": mod_str(character.nature.modifier),
"Perception ": mod_str(character.perception), "Perception ": mod_str(character.perception.modifier),
"Performance": mod_str(character.performance), "Performance": mod_str(character.performance.modifier),
"Persuasion": mod_str(character.persuasion), "Persuasion": mod_str(character.persuasion.modifier),
"Religion": mod_str(character.religion), "Religion": mod_str(character.religion.modifier),
"SleightofHand": mod_str(character.sleight_of_hand), "SleightofHand": mod_str(character.sleight_of_hand.modifier),
"Stealth ": mod_str(character.stealth), "Stealth ": mod_str(character.stealth.modifier),
"Survival": mod_str(character.survival), "Survival": mod_str(character.survival.modifier),
# Hit points # Hit points
"HDTotal": character.hit_dice, "HDTotal": character.hit_dice,
"HPMax": str(character.hp_max), "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 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 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): for _fields, weapon in zip(weapon_fields, character.weapons):
name_field, atk_field, dmg_field = _fields name_field, atk_field, dmg_field = _fields
fields[name_field] = weapon.name fields[name_field] = weapon.name
+2 -1
View File
@@ -3,7 +3,7 @@ import re
from jinja2 import Environment, PackageLoader 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`` # A dice string, with optional backticks: ``1d6 + 3``
@@ -31,4 +31,5 @@ def jinja_environment():
variable_end_string="]]", variable_end_string="]]",
) )
jinja_env.filters["mod_str"] = mod_str jinja_env.filters["mod_str"] = mod_str
jinja_env.filters["ability_mod_str"] = ability_mod_str
return jinja_env 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 */ /* 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 { .known-beast-disabled {
color: lightgrey; color: lightgrey;
} }
@@ -44,7 +44,7 @@
<tr> <tr>
<td>[[ member.name[:28] ]]</td> <td>[[ member.name[:28] ]]</td>
<td>[[ member.armor_class ]]</td> <td>[[ member.armor_class ]]</td>
<td>[[ member.perception + 10 ]]</td> <td>[[ member.perception.modifier + 10 ]]</td>
<td>[% for class in member.class_list %] <td>[% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %] [% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %] [% endfor %]
@@ -29,7 +29,7 @@
[% for member in party %] [% for member in party %]
[[ member.name[:28] ]] [[ member.name[:28] ]]
& [[ member.armor_class ]] & [[ member.armor_class ]]
& [[ member.perception + 10 ]] & [[ member.perception.modifier + 10 ]]
& [% for class in member.class_list %] & [% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %] [% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %] [% endfor %]
@@ -57,7 +57,7 @@
[% for member in party %] [% for member in party %]
[[ member.name[:28] ]] [[ member.name[:28] ]]
& [[ member.armor_class ]] & [[ member.armor_class ]]
& [[ member.perception + 10 ]] & [[ member.perception.modifier + 10 ]]
& [% for class in member.class_list %] & [% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %] [% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %] [% 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 pathlib import Path
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from itertools import product from itertools import product
from typing import Union, Sequence, Optional from typing import Union, Sequence, Optional, Literal, List
from dungeonsheets import ( from dungeonsheets import (
character as _char, character as _char,
@@ -70,6 +70,9 @@ class CharacterRenderer():
return template.render(character=character, return template.render(character=character,
use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS) 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_subclasses_content = CharacterRenderer("subclasses_template.{suffix}")
create_features_content = CharacterRenderer("features_template.{suffix}") create_features_content = CharacterRenderer("features_template.{suffix}")
@@ -192,7 +195,7 @@ def make_gm_sheet(
gm_file = Path(gm_file) gm_file = Path(gm_file)
basename = gm_file.stem basename = gm_file.stem
gm_props = readers.read_sheet_file(gm_file) 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 # Create the intro tex
content_suffix = format_suffixes[output_format] content_suffix = format_suffixes[output_format]
content = [ 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( def make_character_sheet(
char_file: Union[str, Path], char_file: Union[str, Path],
character: Optional[Character] = None, character: Optional[Character] = None,
@@ -336,12 +429,6 @@ def make_character_sheet(
pages = [] pages = []
# Prepare the tex/html content # Prepare the tex/html content
content_suffix = format_suffixes[output_format] 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 # Start of PDF gen
char_pdf = create_character_pdf_template( char_pdf = create_character_pdf_template(
character=character, basename=char_base, flatten=flatten character=character, basename=char_base, flatten=flatten
@@ -360,43 +447,10 @@ def make_character_sheet(
sheets.append(spell_base + ".pdf") sheets.append(spell_base + ".pdf")
# end of PDF gen # end of PDF gen
features_base = "{:s}_features".format(basename) 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 # Create a list of features and magic items
if character.features: content = make_character_content(character=character,
content.append( content_format=content_suffix,
create_features_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations) fancy_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
)
)
# Typeset combined LaTeX file # Typeset combined LaTeX file
if output_format == "pdf": if output_format == "pdf":
try: try:
+90 -26
View File
@@ -17,7 +17,7 @@ from dungeonsheets.features import (
NaturalExplorerRevised, NaturalExplorerRevised,
QuickDraw, QuickDraw,
RakishAudacity, RakishAudacity,
RemarkableAthelete, RemarkableAthlete,
SeaSoul, SeaSoul,
SoulOfTheForge, SoulOfTheForge,
SuperiorMobility, SuperiorMobility,
@@ -35,7 +35,11 @@ def mod_str(modifier):
return "{:+d}".format(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: class Ability:
@@ -68,7 +72,7 @@ class Ability:
if is_proficient: if is_proficient:
saving_throw += entity.proficiency_bonus saving_throw += entity.proficiency_bonus
# Create the named tuple # 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 return value
def __set__(self, entity, val): def __set__(self, entity, val):
@@ -78,38 +82,98 @@ class Ability:
class Skill: 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): def __init__(self, ability):
self.ability_name = ability self.ability_name = ability
def __set_name__(self, entity, name): def __set_name__(self, owner, name):
self.skill_name = name.lower().replace("_", " ") self.skill_name = name.lower().replace("_", " ")
self.character = entity
def __get__(self, entity, owner): def __get__(self, entity, owner):
log.debug("Getting skill '%s' for '%s'", self.skill_name, entity.name) self.entity = entity
ability = getattr(entity, self.ability_name) return self
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)
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 # Check for expertise
is_expert = self.skill_name in entity.skill_expertise if self.is_expertise:
if is_expert: modifier += self.entity.proficiency_bonus
modifier += entity.proficiency_bonus return modifier
log.info("'%s' modifier for '%s': %d", self.skill_name, entity.name, 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 return modifier
+29
View File
@@ -56,11 +56,40 @@ class EpubOutputTestCase(unittest.TestCase):
gm_epub = Path(f"{GMFILE.stem}.epub").resolve() gm_epub = Path(f"{GMFILE.stem}.epub").resolve()
char_epub = Path(f"{CHARFILE.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): def tearDown(self):
for f in [self.gm_epub, self.char_epub]: for f in [self.gm_epub, self.char_epub]:
if f.exists(): if f.exists():
f.unlink() 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): def test_gm_file_created(self):
# Check that a file is created once the function is run # Check that a file is created once the function is run
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub") 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 proficiency_bonus = 2
my_class = MyClass() 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 # Check for a proficiency
my_class.skill_proficiencies = ["acrobatics"] 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 # Check for a proficiency with spaces in the name
my_class.skill_proficiencies = ["sleight_of_hand"] my_class.skill_proficiencies = ["sleight_of_hand"]
self.assertEqual(my_class.sleight_of_hand, 4) self.assertEqual(my_class.sleight_of_hand.modifier, 4)