From 50754f7d5d0bcc3bcb81d45fd5953ca278622926 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 7 Jul 2021 22:32:20 -0500 Subject: [PATCH] Added ability to make epub files for character sheets (missing stats and spell list). --- .gitignore | 3 + dungeonsheets/features/rogue.py | 3 +- .../forms/druid_shapes_template.html | 52 +++++++ dungeonsheets/forms/dungeonsheets_epub.css | 17 ++- dungeonsheets/forms/features_template.html | 13 ++ dungeonsheets/forms/infusions_template.html | 19 +++ dungeonsheets/forms/magic_items_template.html | 19 +++ dungeonsheets/forms/spellbook_template.html | 40 +++++ dungeonsheets/forms/subclasses_template.html | 8 + dungeonsheets/make_sheets.py | 144 ++++++++---------- tests/test_make_sheets.py | 23 +-- 11 files changed, 248 insertions(+), 93 deletions(-) create mode 100644 dungeonsheets/forms/druid_shapes_template.html create mode 100644 dungeonsheets/forms/features_template.html create mode 100644 dungeonsheets/forms/infusions_template.html create mode 100644 dungeonsheets/forms/magic_items_template.html create mode 100644 dungeonsheets/forms/spellbook_template.html create mode 100644 dungeonsheets/forms/subclasses_template.html diff --git a/.gitignore b/.gitignore index 60bc7a3..6fddae7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ examples/*.pdf examples/*.aux examples/*.tex +# Generated epub files +examples/*.epub + # Emacs temp files *~ diff --git a/dungeonsheets/features/rogue.py b/dungeonsheets/features/rogue.py index 8a79432..968b469 100644 --- a/dungeonsheets/features/rogue.py +++ b/dungeonsheets/features/rogue.py @@ -103,10 +103,9 @@ class BlindSense(Feature): class SlipperyMind(Feature): """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" source = "Rogue" diff --git a/dungeonsheets/forms/druid_shapes_template.html b/dungeonsheets/forms/druid_shapes_template.html new file mode 100644 index 0000000..f88deb3 --- /dev/null +++ b/dungeonsheets/forms/druid_shapes_template.html @@ -0,0 +1,52 @@ +

Known Beasts

+ +[% for shape in character.all_wild_shapes|sort(attribute='challenge_rating') %] + + + + +

[[ shape.name ]]

+ + [% if shape.description %] +

[[ shape.description ]]

+ [% endif %] + + + + + + + + + + + + +
Armor ClassHit PointsSpeed
[[ shape.armor_class ]][[ shape.hp_max ]] ([[ shape.hit_dice ]])[[ shape.speed ]][% if shape.swim_speed %], [[ shape.swim_speed ]] swim[% endif %][% if shape.fly_speed %], [[ shape.fly_speed ]] fly[% endif %]
+ + + + + + + + + + + + +
STRDEXCON
[[ shape.strength.value ]] ([[ shape.strength.modifier|mod_str ]])[[ shape.dexterity.value ]] ([[ shape.dexterity.modifier|mod_str ]])[[ shape.constitution.value ]] ([[ shape.constitution.modifier|mod_str ]])
+ +
+
Skills:
[[ shape.skills ]]
+
Senses:
[[ shape.senses ]]
+
Languages:
[[ shape.languages ]]
+
Resistance:
[[ shape.damage_resistance ]]
+
Immunities:
[[ shape.condition_immunities ]]
+
+ +

[[ shape.__doc__ | rst_to_html(top_heading_level=2) ]]

+ + [% endfor %] + +
diff --git a/dungeonsheets/forms/dungeonsheets_epub.css b/dungeonsheets/forms/dungeonsheets_epub.css index 2827fcc..2038e7f 100644 --- a/dungeonsheets/forms/dungeonsheets_epub.css +++ b/dungeonsheets/forms/dungeonsheets_epub.css @@ -1,7 +1,20 @@ h1, h2, h3, h4, h5, h6 { color: #58180d; } + /* End fancy decorations */ +.known-beast-disabled { + color: lightgrey; +} +.not-implemented { + font-weight: bold; + color: darkred; + background: pink; +} + +.spell-school { + font-style: italic; +} table { margin-bottom: 10px; @@ -32,10 +45,10 @@ dd > p { div.system-message { background: pink; - border-color: red; + border-color: darkred; border-style: solid; border-width: 2px; - color: red; + color: darkred; } .literal { font-family: monospace; diff --git a/dungeonsheets/forms/features_template.html b/dungeonsheets/forms/features_template.html new file mode 100644 index 0000000..e1425d3 --- /dev/null +++ b/dungeonsheets/forms/features_template.html @@ -0,0 +1,13 @@ +

Features

+ +[% for feat in character.features %] +

[[ feat.name ]]

+ +
+
Source:
+
[[ feat.source ]]
+
+ +[[ feat.__doc__|rst_to_html ]] + +[% endfor %] diff --git a/dungeonsheets/forms/infusions_template.html b/dungeonsheets/forms/infusions_template.html new file mode 100644 index 0000000..9770d92 --- /dev/null +++ b/dungeonsheets/forms/infusions_template.html @@ -0,0 +1,19 @@ +

Infusions

+ +[% for inf in character.infusions %] +

[[ inf.name ]]

+ +
+ [% if inf.prerequisite %] +
Prerequisite:
+
[[ inf.prerequisite ]]
+ [% endif %] + [% if inf.item %] +
Item:
+
[[ inf.item ]]
+ [% endif %] +
+ +[[ inf.__doc__ | rst_to_html(top_heading_level=2) ]] + +[% endfor %] diff --git a/dungeonsheets/forms/magic_items_template.html b/dungeonsheets/forms/magic_items_template.html new file mode 100644 index 0000000..44fb087 --- /dev/null +++ b/dungeonsheets/forms/magic_items_template.html @@ -0,0 +1,19 @@ +

Magic Items

+ +[% for mitem in character.magic_items %] +

[[ mitem.name ]]

+ +
+
Requires Attunement:
+
[[ mitem.requires_attunement ]]
+
Rarity:
+
[[ mitem.rarity ]]
+
+ +[% if mitem.needs_implementation %] +

**Not included in stats on Character Sheet

+[% endif %] + +[[ mitem.__doc__|rst_to_html ]] + +[% endfor %] diff --git a/dungeonsheets/forms/spellbook_template.html b/dungeonsheets/forms/spellbook_template.html new file mode 100644 index 0000000..89b221f --- /dev/null +++ b/dungeonsheets/forms/spellbook_template.html @@ -0,0 +1,40 @@ +

Spells

+ +[% for spl in character.spells %] + +

[[ spl.name ]]

+ +

+ + [% if spl.level > 0 %] + [[ spl.magic_school ]] Level [[ spl.level ]] + [% else %] + [[ spl.magic_school ]] Cantrip + [% endif %] + + + [% if spl.ritual and spl.concentration %] + (ritual, concentration) + [% elif spl.ritual %] + (ritual) + [% elif spl.concentration %] + (concentration) + [% endif %] +

+ +
+
Casting Time:
+
[[ spl.casting_time ]]
+
Duration:
+
[[ spl.duration ]]
+
Range:
+
[[ spl.casting_range ]]
+
Components:
+
[[ spl.component_string ]]
+
+ + + [[ spl.__doc__ | rst_to_html(top_heading_level=1) ]] + + +[% endfor %] diff --git a/dungeonsheets/forms/subclasses_template.html b/dungeonsheets/forms/subclasses_template.html new file mode 100644 index 0000000..128dd5b --- /dev/null +++ b/dungeonsheets/forms/subclasses_template.html @@ -0,0 +1,8 @@ +

Subclasses

+ +[% for sc in character.subclasses if sc not in ['', None, 'None', 'none']%] +

[[ sc.name ]]

+ + [[ sc.__doc__ | rst_to_html(top_heading_level=2) ]] + +[% endfor %] diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index ffa8c02..040a0ec 100755 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -61,28 +61,22 @@ jinja_env.filters["to_heading_id"] = epub.to_heading_id File = Union[Path, str] -def create_subclasses_tex( - character: Character, - use_dnd_decorations: bool = False, -) -> str: - template = jinja_env.get_template("subclasses_template.tex") - return template.render(character=character, use_dnd_decorations=use_dnd_decorations) +class CharacterRenderer(): + def __init__(self, template_name: str): + self.template_name = template_name + + def __call__(self, character: Character, content_suffix: str = "tex", use_dnd_decorations: bool = False): + 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( - character: Character, - use_dnd_decorations: bool = False, -) -> str: - template = jinja_env.get_template("features_template.tex") - return template.render(character=character, use_dnd_decorations=use_dnd_decorations) - - -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) +create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}") +create_features_content = CharacterRenderer("features_template.{suffix}") +create_magic_items_content = CharacterRenderer("magic_items_template.{suffix}") +create_spellbook_content = CharacterRenderer("spellbook_template.{suffix}") +create_infusions_content = CharacterRenderer("infusions_template.{suffix}") +create_druid_shapes_content = CharacterRenderer("druid_shapes_template.{suffix}") 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( @@ -184,6 +154,7 @@ def make_sheet( ret = make_character_sheet( char_file=sheet_file, flatten=flatten, + output_format=output_format, fancy_decorations=fancy_decorations, debug=debug, ) @@ -328,6 +299,7 @@ def make_character_sheet( char_file: Union[str, Path], character: Optional[Character] = None, flatten: bool = False, + output_format: str = "pdf", fancy_decorations: bool = False, debug: bool = False, ): @@ -343,6 +315,8 @@ def make_character_sheet( flatten If true, the resulting PDF will look better and won't be fillable form. + output_format + Either "pdf" or "epub" to generate a PDF file or an EPUB file. fancy_decorations Use fancy page layout and decorations for extra sheets, namely the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. @@ -360,13 +334,14 @@ def make_character_sheet( person_base = basename + "_person" sheets = [char_base + ".pdf", person_base + ".pdf"] pages = [] - tex = [ - jinja_env.get_template("preamble.tex").render( + # 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 @@ -387,60 +362,69 @@ def make_character_sheet( features_base = "{:s}_features".format(basename) # Create a list of subcasses if character.subclasses: - tex.append( - create_subclasses_tex(character, use_dnd_decorations=fancy_decorations) - ) - - # Create a list of features + 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: - tex.append( - create_features_tex(character, use_dnd_decorations=fancy_decorations) + content.append( + create_features_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations) ) - if character.magic_items: - tex.append( - create_magic_items_tex(character, use_dnd_decorations=fancy_decorations) + 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: - tex.append( - create_spellbook_tex(character, use_dnd_decorations=fancy_decorations) + 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", []): - tex.append( - create_infusions_tex(character, use_dnd_decorations=fancy_decorations) + 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", []): - tex.append( - create_druid_shapes_tex(character, use_dnd_decorations=fancy_decorations) + content.append( + create_druid_shapes_content(character, content_suffix=content_suffix, use_dnd_decorations=fancy_decorations) ) - tex.append( - jinja_env.get_template("postamble.tex").render( + content.append( + jinja_env.get_template(f"postamble.{content_suffix}").render( use_dnd_decorations=fancy_decorations ) ) - # Typeset combined LaTeX file - try: - if len(tex) > 2: - latex.create_latex_pdf( - tex="".join(tex), - basename=features_base, - keep_temp_files=debug, - use_dnd_decorations=fancy_decorations, + if output_format == "pdf": + try: + if len(content) > 2: + latex.create_latex_pdf( + tex="".join(content), + basename=features_base, + keep_temp_files=debug, + use_dnd_decorations=fancy_decorations, + ) + sheets.append(features_base + ".pdf") + final_pdf = f"{basename}.pdf" + merge_pdfs(sheets, final_pdf, clean_up=True) + except exceptions.LatexNotFoundError: + log.warning( + f"``pdflatex`` not available. Skipping features for {character.name}" ) - sheets.append(features_base + ".pdf") - final_pdf = f"{basename}.pdf" - merge_pdfs(sheets, final_pdf, clean_up=True) - except exceptions.LatexNotFoundError: - log.warning( - 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'" ) diff --git a/tests/test_make_sheets.py b/tests/test_make_sheets.py index a3d8227..800f727 100644 --- a/tests/test_make_sheets.py +++ b/tests/test_make_sheets.py @@ -54,18 +54,23 @@ class MakeSheetsTestCase(unittest.TestCase): class EpubOutputTestCase(unittest.TestCase): gm_epub = Path(f"{GMFILE.stem}.epub").resolve() + char_epub = Path(f"{CHARFILE.stem}.epub").resolve() def tearDown(self): - for f in [self.gm_epub]: + for f in [self.gm_epub, self.char_epub]: if f.exists(): f.unlink() - def test_file_created(self): + def test_gm_file_created(self): # 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") 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): basename = "clara" @@ -121,37 +126,37 @@ class TexCreatorTestCase(unittest.TestCase): def test_create_subclasses_tex(self): 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"\subsection*{Way of the Open Hand}", tex) def test_create_features_tex(self): 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"\subsection*{Martial Arts}", tex) def test_create_magic_items_tex(self): 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"\subsection*{Cloak of Protection}", tex) def test_create_spellbook_tex(self): 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*{Invisibility}", tex) def test_create_infusions_tex(self): 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"\subsection*{Boots of the Winding Path}", tex) def test_create_druid_shapes_tex(self): 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*{Crocodile}", tex)