Added ability to make epub files for character sheets (missing stats and spell list).

This commit is contained in:
Mark Wolfman
2021-07-07 22:32:20 -05:00
parent a31b9eb42c
commit 50754f7d5d
11 changed files with 248 additions and 93 deletions
+3
View File
@@ -3,6 +3,9 @@ examples/*.pdf
examples/*.aux examples/*.aux
examples/*.tex examples/*.tex
# Generated epub files
examples/*.epub
# Emacs temp files # Emacs temp files
*~ *~
+1 -2
View File
@@ -103,10 +103,9 @@ class BlindSense(Feature):
class SlipperyMind(Feature): class SlipperyMind(Feature):
"""By 15th level, you have acquired greater mental strength. You gain """By 15th level, you have acquired greater mental strength. You gain
proficiency in W isdom saving throws. proficiency in Wisdom saving throws.
""" """
name = "Slippery Mind" name = "Slippery Mind"
source = "Rogue" source = "Rogue"
@@ -0,0 +1,52 @@
<h1 id="known-beasts">Known Beasts</h1>
[% for shape in character.all_wild_shapes|sort(attribute='challenge_rating') %]
<block class="[% if not character.can_assume_shape(shape) %]known-beast-disabled[% endif %]">
<h2 id="known-beasts-[[ shape.name | to_heading_id ]]">[[ shape.name ]]</h2>
[% if shape.description %]
<p>[[ shape.description ]]</p>
[% endif %]
<table>
<tr>
<th>Armor Class</th>
<th>Hit Points</th>
<th>Speed</th>
</tr>
<tr>
<td>[[ shape.armor_class ]]</td>
<td>[[ shape.hp_max ]] ([[ shape.hit_dice ]])</td>
<td>[[ shape.speed ]][% if shape.swim_speed %], [[ shape.swim_speed ]] swim[% endif %][% if shape.fly_speed %], [[ shape.fly_speed ]] fly[% endif %]</td>
</tr>
</table>
<table>
<tr>
<th>STR</th>
<th>DEX</th>
<th>CON</th>
</tr>
<tr>
<td>[[ shape.strength.value ]] ([[ shape.strength.modifier|mod_str ]])</td>
<td>[[ shape.dexterity.value ]] ([[ shape.dexterity.modifier|mod_str ]])</td>
<td>[[ shape.constitution.value ]] ([[ shape.constitution.modifier|mod_str ]])</td>
</tr>
</table>
<dl>
<dt>Skills:</dt><dd>[[ shape.skills ]]</dd>
<dt>Senses:</dt><dd>[[ shape.senses ]]</dd>
<dt>Languages:</dt><dd>[[ shape.languages ]]</dd>
<dt>Resistance:</dt><dd>[[ shape.damage_resistance ]]</dd>
<dt>Immunities:</dt><dd>[[ shape.condition_immunities ]]</dd>
</dl>
<p>[[ shape.__doc__ | rst_to_html(top_heading_level=2) ]]</p>
[% endfor %]
</block>
+15 -2
View File
@@ -1,7 +1,20 @@
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
color: #58180d; color: #58180d;
} }
/* End fancy decorations */ /* End fancy decorations */
.known-beast-disabled {
color: lightgrey;
}
.not-implemented {
font-weight: bold;
color: darkred;
background: pink;
}
.spell-school {
font-style: italic;
}
table { table {
margin-bottom: 10px; margin-bottom: 10px;
@@ -32,10 +45,10 @@ dd > p {
div.system-message { div.system-message {
background: pink; background: pink;
border-color: red; border-color: darkred;
border-style: solid; border-style: solid;
border-width: 2px; border-width: 2px;
color: red; color: darkred;
} }
.literal { .literal {
font-family: monospace; font-family: monospace;
@@ -0,0 +1,13 @@
<h1 id="features">Features</h1>
[% for feat in character.features %]
<h2 id="features-[[ feat.name | to_heading_id ]]">[[ feat.name ]]</h2>
<dl>
<dt>Source:</dt>
<dd>[[ feat.source ]]</dd>
</dl>
[[ feat.__doc__|rst_to_html ]]
[% endfor %]
@@ -0,0 +1,19 @@
<h1 id="infusions">Infusions</h1>
[% for inf in character.infusions %]
<h2 id="infusions-[[ inf.name | to_heading_id ]]">[[ inf.name ]]</h2>
<dl>
[% if inf.prerequisite %]
<dt>Prerequisite:</dt>
<dd>[[ inf.prerequisite ]]</dd>
[% endif %]
[% if inf.item %]
<dt>Item:</dt>
<dd>[[ inf.item ]]</dd>
[% endif %]
</dl>
[[ inf.__doc__ | rst_to_html(top_heading_level=2) ]]
[% endfor %]
@@ -0,0 +1,19 @@
<h1 id="magic-items">Magic Items</h1>
[% for mitem in character.magic_items %]
<h2 id="magic-items-[[ mitem.name | to_heading_id ]]">[[ mitem.name ]]</h2>
<dl>
<dt>Requires Attunement:</dt>
<dd>[[ mitem.requires_attunement ]]</dd>
<dt>Rarity:</dt>
<dd>[[ mitem.rarity ]]</dd>
</dl>
[% if mitem.needs_implementation %]
<p class="not-implemented">**Not included in stats on Character Sheet</p>
[% endif %]
[[ mitem.__doc__|rst_to_html ]]
[% endfor %]
@@ -0,0 +1,40 @@
<h1 id="spells">Spells</h1>
[% for spl in character.spells %]
<h2 id="spells-[[ spl.name | to_heading_id ]]">[[ spl.name ]]</h2>
<p class="spell-school">
<!-- Spell school and level -->
[% if spl.level > 0 %]
[[ spl.magic_school ]] Level [[ spl.level ]]
[% else %]
[[ spl.magic_school ]] Cantrip
[% endif %]
<!-- Ritual and/or concentration -->
[% if spl.ritual and spl.concentration %]
(ritual, concentration)
[% elif spl.ritual %]
(ritual)
[% elif spl.concentration %]
(concentration)
[% endif %]
</p>
<dl class="spell-details">
<dt>Casting Time:</dt>
<dd>[[ spl.casting_time ]]</dd>
<dt>Duration:</dt>
<dd>[[ spl.duration ]]</dd>
<dt>Range:</dt>
<dd>[[ spl.casting_range ]]</dd>
<dt>Components:</dt>
<dd>[[ spl.component_string ]]</dd>
</dl>
<block class="spell-description">
[[ spl.__doc__ | rst_to_html(top_heading_level=1) ]]
</block>
[% endfor %]
@@ -0,0 +1,8 @@
<h1 id="subclasses">Subclasses</h1>
[% for sc in character.subclasses if sc not in ['', None, 'None', 'none']%]
<h2 id="subclasses-[[ sc.name | to_heading_id ]]">[[ sc.name ]]</h2><!-- Would like to add source here -->
[[ sc.__doc__ | rst_to_html(top_heading_level=2) ]]
[% endfor %]
+53 -69
View File
@@ -61,28 +61,22 @@ jinja_env.filters["to_heading_id"] = epub.to_heading_id
File = Union[Path, str] File = Union[Path, str]
def create_subclasses_tex( class CharacterRenderer():
character: Character, def __init__(self, template_name: str):
use_dnd_decorations: bool = False, self.template_name = template_name
) -> str:
template = jinja_env.get_template("subclasses_template.tex") def __call__(self, character: Character, content_suffix: str = "tex", use_dnd_decorations: bool = False):
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) template = jinja_env.get_template(self.template_name.format(suffix=content_suffix))
return template.render(character=character,
use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS)
def create_features_tex( create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}")
character: Character, create_features_content = CharacterRenderer("features_template.{suffix}")
use_dnd_decorations: bool = False, create_magic_items_content = CharacterRenderer("magic_items_template.{suffix}")
) -> str: create_spellbook_content = CharacterRenderer("spellbook_template.{suffix}")
template = jinja_env.get_template("features_template.tex") create_infusions_content = CharacterRenderer("infusions_template.{suffix}")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) create_druid_shapes_content = CharacterRenderer("druid_shapes_template.{suffix}")
def create_magic_items_tex(
character: Character,
use_dnd_decorations: bool = False,
) -> str:
template = jinja_env.get_template("magic_items_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_monsters_content( def create_monsters_content(
@@ -108,30 +102,6 @@ def create_party_summary_content(
) )
def create_spellbook_tex(
character: Character,
use_dnd_decorations: bool = False,
) -> str:
template = jinja_env.get_template("spellbook_template.tex")
return template.render(
character=character, ordinals=ORDINALS, use_dnd_decorations=use_dnd_decorations
)
def create_infusions_tex(
character: Character,
use_dnd_decorations: bool = False,
) -> str:
template = jinja_env.get_template("infusions_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_druid_shapes_tex(
character: Character,
use_dnd_decorations: bool = False,
) -> str:
template = jinja_env.get_template("druid_shapes_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_random_tables_content( def create_random_tables_content(
@@ -184,6 +154,7 @@ def make_sheet(
ret = make_character_sheet( ret = make_character_sheet(
char_file=sheet_file, char_file=sheet_file,
flatten=flatten, flatten=flatten,
output_format=output_format,
fancy_decorations=fancy_decorations, fancy_decorations=fancy_decorations,
debug=debug, debug=debug,
) )
@@ -328,6 +299,7 @@ def make_character_sheet(
char_file: Union[str, Path], char_file: Union[str, Path],
character: Optional[Character] = None, character: Optional[Character] = None,
flatten: bool = False, flatten: bool = False,
output_format: str = "pdf",
fancy_decorations: bool = False, fancy_decorations: bool = False,
debug: bool = False, debug: bool = False,
): ):
@@ -343,6 +315,8 @@ def make_character_sheet(
flatten flatten
If true, the resulting PDF will look better and won't be If true, the resulting PDF will look better and won't be
fillable form. fillable form.
output_format
Either "pdf" or "epub" to generate a PDF file or an EPUB file.
fancy_decorations fancy_decorations
Use fancy page layout and decorations for extra sheets, namely Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
@@ -360,13 +334,14 @@ def make_character_sheet(
person_base = basename + "_person" person_base = basename + "_person"
sheets = [char_base + ".pdf", person_base + ".pdf"] sheets = [char_base + ".pdf", person_base + ".pdf"]
pages = [] pages = []
tex = [ # Prepare the tex/html content
jinja_env.get_template("preamble.tex").render( content_suffix = format_suffixes[output_format]
content = [
jinja_env.get_template(f"preamble.{content_suffix}").render(
use_dnd_decorations=fancy_decorations, use_dnd_decorations=fancy_decorations,
title="Features, Magical Items and Spells", 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
@@ -387,50 +362,47 @@ def make_character_sheet(
features_base = "{:s}_features".format(basename) features_base = "{:s}_features".format(basename)
# Create a list of subcasses # Create a list of subcasses
if character.subclasses: if character.subclasses:
tex.append( content.append( create_subclasses_content(character,
create_subclasses_tex(character, use_dnd_decorations=fancy_decorations) content_suffix=content_suffix,
) use_dnd_decorations=fancy_decorations) )
# Create a list of features and magic items
# Create a list of features
if character.features: if character.features:
tex.append( content.append(
create_features_tex(character, use_dnd_decorations=fancy_decorations) create_features_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
if character.magic_items: if character.magic_items:
tex.append( content.append(
create_magic_items_tex(character, use_dnd_decorations=fancy_decorations) create_magic_items_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
# Create a list of spells # Create a list of spells
if character.is_spellcaster: if character.is_spellcaster:
tex.append( content.append(
create_spellbook_tex(character, use_dnd_decorations=fancy_decorations) create_spellbook_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
# Create a list of Artificer infusions # Create a list of Artificer infusions
if getattr(character, "infusions", []): if getattr(character, "infusions", []):
tex.append( content.append(
create_infusions_tex(character, use_dnd_decorations=fancy_decorations) create_infusions_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
# Create a list of Druid wild_shapes # Create a list of Druid wild_shapes
if getattr(character, "wild_shapes", []): if getattr(character, "wild_shapes", []):
tex.append( content.append(
create_druid_shapes_tex(character, use_dnd_decorations=fancy_decorations) create_druid_shapes_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
tex.append( content.append(
jinja_env.get_template("postamble.tex").render( jinja_env.get_template(f"postamble.{content_suffix}").render(
use_dnd_decorations=fancy_decorations use_dnd_decorations=fancy_decorations
) )
) )
# Typeset combined LaTeX file # Typeset combined LaTeX file
if output_format == "pdf":
try: try:
if len(tex) > 2: if len(content) > 2:
latex.create_latex_pdf( latex.create_latex_pdf(
tex="".join(tex), tex="".join(content),
basename=features_base, basename=features_base,
keep_temp_files=debug, keep_temp_files=debug,
use_dnd_decorations=fancy_decorations, use_dnd_decorations=fancy_decorations,
@@ -442,6 +414,18 @@ def make_character_sheet(
log.warning( log.warning(
f"``pdflatex`` not available. Skipping features for {character.name}" f"``pdflatex`` not available. Skipping features for {character.name}"
) )
elif output_format == "epub":
epub.create_epub(
chapters={character.name: "".join(content)},
basename=basename,
title=character.name,
use_dnd_decorations=fancy_decorations,
)
else:
raise exceptions.UnknownOutputFormat(
f"Unknown output format requested: {output_format}. Valid options are:"
" 'pdf', 'epub'"
)
def merge_pdfs(src_filenames, dest_filename, clean_up=False): def merge_pdfs(src_filenames, dest_filename, clean_up=False):
+14 -9
View File
@@ -54,18 +54,23 @@ class MakeSheetsTestCase(unittest.TestCase):
class EpubOutputTestCase(unittest.TestCase): 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()
def tearDown(self): def tearDown(self):
for f in [self.gm_epub]: for f in [self.gm_epub, self.char_epub]:
if f.exists(): if f.exists():
f.unlink() f.unlink()
def test_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
# self.assertFalse(os.path.exists(pdf_name), f'{pdf_name} already exists.')
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub") make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub")
self.assertTrue(self.gm_epub.exists(), f"{self.gm_epub} not created.") self.assertTrue(self.gm_epub.exists(), f"{self.gm_epub} not created.")
def test_character_file_created(self):
# Check that a file is created once the function is run
make_sheets.make_character_sheet(char_file=CHARFILE, output_format="epub")
self.assertTrue(self.char_epub.exists(), f"{self.char_epub} not created.")
class PdfOutputTestCase(unittest.TestCase): class PdfOutputTestCase(unittest.TestCase):
basename = "clara" basename = "clara"
@@ -121,37 +126,37 @@ class TexCreatorTestCase(unittest.TestCase):
def test_create_subclasses_tex(self): def test_create_subclasses_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_subclasses_tex(character=char) tex = make_sheets.create_subclasses_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Subclasses}", tex) self.assertIn(r"\section*{Subclasses}", tex)
self.assertIn(r"\subsection*{Way of the Open Hand}", tex) self.assertIn(r"\subsection*{Way of the Open Hand}", tex)
def test_create_features_tex(self): def test_create_features_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_features_tex(character=char) tex = make_sheets.create_features_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Features}", tex) self.assertIn(r"\section*{Features}", tex)
self.assertIn(r"\subsection*{Martial Arts}", tex) self.assertIn(r"\subsection*{Martial Arts}", tex)
def test_create_magic_items_tex(self): def test_create_magic_items_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_magic_items_tex(character=char) tex = make_sheets.create_magic_items_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Magic Items}", tex) self.assertIn(r"\section*{Magic Items}", tex)
self.assertIn(r"\subsection*{Cloak of Protection}", tex) self.assertIn(r"\subsection*{Cloak of Protection}", tex)
def test_create_spellbook_tex(self): def test_create_spellbook_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_spellbook_tex(character=char) tex = make_sheets.create_spellbook_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Spells}", tex) self.assertIn(r"\section*{Spells}", tex)
self.assertIn(r"\section*{Invisibility}", tex) self.assertIn(r"\section*{Invisibility}", tex)
def test_create_infusions_tex(self): def test_create_infusions_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_infusions_tex(character=char) tex = make_sheets.create_infusions_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Infusions}", tex) self.assertIn(r"\section*{Infusions}", tex)
self.assertIn(r"\subsection*{Boots of the Winding Path}", tex) self.assertIn(r"\subsection*{Boots of the Winding Path}", tex)
def test_create_druid_shapes_tex(self): def test_create_druid_shapes_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_druid_shapes_tex(character=char) tex = make_sheets.create_druid_shapes_content(character=char, content_suffix="tex")
self.assertIn(r"\section*{Known Beasts}", tex) self.assertIn(r"\section*{Known Beasts}", tex)
self.assertIn(r"\section*{Crocodile}", tex) self.assertIn(r"\section*{Crocodile}", tex)