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 Class |
+ Hit Points |
+ Speed |
+
+
+ | [[ 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 %] |
+
+
+
+
+
+ | STR |
+ DEX |
+ CON |
+
+
+ | [[ 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)