mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 12:13:27 +02:00
Added ability to make epub files for character sheets (missing stats and spell list).
This commit is contained in:
@@ -3,6 +3,9 @@ examples/*.pdf
|
||||
examples/*.aux
|
||||
examples/*.tex
|
||||
|
||||
# Generated epub files
|
||||
examples/*.epub
|
||||
|
||||
# Emacs temp files
|
||||
*~
|
||||
|
||||
|
||||
@@ -106,7 +106,6 @@ class SlipperyMind(Feature):
|
||||
proficiency in Wisdom saving throws.
|
||||
|
||||
"""
|
||||
|
||||
name = "Slippery Mind"
|
||||
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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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 %]
|
||||
@@ -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,50 +362,47 @@ 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
|
||||
if output_format == "pdf":
|
||||
try:
|
||||
if len(tex) > 2:
|
||||
if len(content) > 2:
|
||||
latex.create_latex_pdf(
|
||||
tex="".join(tex),
|
||||
tex="".join(content),
|
||||
basename=features_base,
|
||||
keep_temp_files=debug,
|
||||
use_dnd_decorations=fancy_decorations,
|
||||
@@ -442,6 +414,18 @@ def make_character_sheet(
|
||||
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'"
|
||||
)
|
||||
|
||||
|
||||
def merge_pdfs(src_filenames, dest_filename, clean_up=False):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user