mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Epub now contains character sheets information.
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
@@ -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,]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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 %]–[% 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>
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user