Now creates GM sheet PDF, but with monsters only.

This commit is contained in:
Mark Wolfman
2021-06-03 23:35:42 -05:00
parent 3b8dbc0566
commit 905ecee4c9
8 changed files with 385 additions and 59 deletions
+21 -19
View File
@@ -238,19 +238,20 @@ class BardBattleMagic(Feature):
source = "Bard (College of Valor)" source = "Bard (College of Valor)"
# College of Glamour # XgtE - College of Glamour:
class MantleOfInspiration(Feature): class MantleOfInspiration(Feature):
"""When you join the College of Glamour at 3rd level, you gain the ability to """When you join the College of Glamour at 3rd level, you gain the
weave a song of fey magic that imbues your allies with vigor and speed. As ability to weave a song of fey magic that imbues your allies with
a bonus action, you can expend one use of your Bardic Inspiration to grant vigor and speed. As a bonus action, you can expend one use of your
yourself a wondrous appearance. When you do so, choose a number of Bardic Inspiration to grant yourself a wondrous appearance. When
creatures you can see and that can see you within 60 feet of you, up to a you do so, choose a number of creatures you can see and that can
number equal to your Charisma modifier (mini mum of one). Each of them see you within 60 feet of you, up to a number equal to your
gains 5 temporary hit points. When a creature gains these temporary hit Charisma modifier (minimum of one). Each of them gains 5 temporary
points, it can immediately use its reaction to move up to its speed, hit points. When a creature gains these temporary hit points, it
without provoking opportunity attacks. The number of temporary hit points can immediately use its reaction to move up to its speed, without
inoreases when you reach certain levels in this class, increasing to 8 at provoking opportunity attacks. The number of temporary hit points
5th level, 11 at 10th level, and 14 at 15th level. increases when you reach certain levels in this class, increasing
to 8 at 5th level, 11 at 10th level, and 14 at 15th level.
""" """
@@ -402,13 +403,14 @@ class MastersFlourish(Feature):
# College of Whispers # College of Whispers
class PsychicBlades(Feature): class PsychicBlades(Feature):
"""When you join the College of Whispers at 3rd level, you gain the ability to """When you join the College of Whispers at 3rd level, you gain the
make your weapon attacks magically toxic to a creature's mind. When you hit ability to make your weapon attacks magically toxic to a
a creature with a weapon attack, you can expend one use ofyour Bardic creature's mind. When you hit a creature with a weapon attack, you
Inspiration to deal an extra 2d6 psychic damage to that target. You can do can expend one use ofyour Bardic Inspiration to deal an extra 2d6
so only once per round on your turn. The psychic damage increases when you psychic damage to that target. You can do so only once per round
reach certain levels in this class, increasing to 3d6 at 5th level, 5d6 on your turn. The psychic damage increases when you reach certain
at 10th level, and 8d6 at 15th level. levels in this class, increasing to 3d6 at 5th level, 5d6 at 10th
level, and 8d6 at 15th level.
""" """
+89
View File
@@ -0,0 +1,89 @@
\section*{Monsters}
[% if use_dnd_decorations %]
[% for monster in monsters|sort(attribute='name') %]
\begin{DndMonster}{[[ monster.name ]]}
\DndMonsterType{[[ monster.description ]]}
% If you want to use commas in the key values, enclose the values in braces.
\DndMonsterBasics[
armor-class = {[[ monster.armor_class ]]},
hit-points = {[[ monster.hp_max ]] ([[ monster.hit_dice ]])},
speed = {[[ monster.speed ]] ft.[% if monster.swim_speed %], [[ monster.swim_speed ]]ft. swim[% endif %][% if monster.fly_speed %], [[ monster.fly_speed ]] ft. fly[% endif %]},
]
\DndMonsterAbilityScores[
str = [[ monster.strength.value ]],
dex = [[ monster.dexterity.value ]],
con = [[ monster.constitution.value ]],
int = [[ monster.intelligence.value ]],
wis = [[ monster.wisdom.value ]],
cha = [[ monster.charisma.value ]],
]
\DndMonsterDetails[
%saving-throws = {Str +0, Dex +0, Con +0, Int +0, Wis +0, Cha +0},
skills = {[[ monster.skills ]]},
%damage-vulnerabilities = {cold},
damage-resistances = {[[ monster.damage_resistance ]]},
%damage-immunities = {poison},
condition-immunities = {[[ monster.condition_immunities ]]},
senses = {[[ monster.senses ]]},
languages = {[% if monster.languages %][[ monster.languages ]][% else %] --- [% endif %]},
challenge = {[[ monster.challenge_rating ]]},
]
%\DndMonsterSection{Actions}
[[ monster.__doc__ | rst_to_latex(top_heading_level=2) ]]
\end{DndMonster}
[% endfor %]
[% else %]
[% for monster in monsters|sort(attribute='name') %]
{
\section*{[[ monster.name ]]}
[% if monster.description %]
\subsection*{[[ monster.description ]]}
[% endif %]
\begin{tabular}{c | c | c}
Armor Class & Hit Points & Speed \\
\hline
[[ monster.armor_class ]] &
[[ monster.hp_max ]] ([[ monster.hit_dice ]]) &
[[ monster.speed ]] \\
[% if monster.swim_speed %]
& & [[ monster.swim_speed ]] swim \\
[% endif %]
[% if monster.fly_speed %]
& & [[ monster.fly_speed ]] fly \\
[% endif %]
\end{tabular}
\vspace{0.2cm}
\begin{tabular}{c | c | c}
STR & DEX & CON \\
\hline
[[ monster.strength.value ]] ([[ monster.strength.modifier|mod_str ]]) &
[[ monster.dexterity.value ]] ([[ monster.dexterity.modifier|mod_str ]]) &
[[ monster.constitution.value ]] ([[ monster.constitution.modifier|mod_str ]]) \\
\end{tabular}
\vspace{0.2cm}
\begin{tabular}{p{0.1\textwidth} p{0.32\textwidth}}
\textbf{Skills:} & [[ monster.skills ]] \\
\textbf{Senses:} & [[ monster.senses ]] \\
\textbf{Languages:} & [[ monster.languages ]] \\
\textbf{Resistance:} & [[ monster.damage_resistance ]] \\
\textbf{Immunities:} & [[ monster.condition_immunities ]] \\
\end{tabular}
\vspace{0.2cm}
[[ monster.__doc__ | rst_to_latex(top_heading_level=2) ]]
} %\color
[% endfor %]
[% endif %]
+2 -1
View File
@@ -51,4 +51,5 @@
[% endraw %] [% endraw %]
\begin{document} \begin{document}
\chapter*{Features, Magical Items and Spells}
\chapter*{[[ title ]]}
+139 -31
View File
@@ -9,11 +9,12 @@ import re
from pathlib import Path from pathlib import Path
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from itertools import product from itertools import product
from typing import Union, Mapping, Sequence
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from dungeonsheets import character as _char, exceptions, readers, latex from dungeonsheets import character as _char, exceptions, readers, latex, monsters
from dungeonsheets.stats import mod_str from dungeonsheets.stats import mod_str, findattr
from dungeonsheets.fill_pdf_template import ( from dungeonsheets.fill_pdf_template import (
create_character_pdf_template, create_character_pdf_template,
create_spells_pdf_template, create_spells_pdf_template,
@@ -23,8 +24,6 @@ from dungeonsheets.character import Character
"""Program to take character definitions and build a PDF of the """Program to take character definitions and build a PDF of the
character sheet.""" character sheet."""
PDFTK_CMD = "pdftk"
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
ORDINALS = { ORDINALS = {
@@ -53,6 +52,10 @@ jinja_env.filters["mod_str"] = mod_str
PDFTK_CMD = "pdftk" PDFTK_CMD = "pdftk"
# Custom types
File = Union[Path, str]
def create_subclasses_tex( def create_subclasses_tex(
character: Character, character: Character,
use_dnd_decorations: bool = False, use_dnd_decorations: bool = False,
@@ -77,6 +80,15 @@ def create_magic_items_tex(
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_monsters_tex(
monsters: Sequence[Union[monsters.Monster, str]],
use_dnd_decorations: bool = False,
) -> str:
# Convert strings to Monster objects
template = jinja_env.get_template("monsters_template.tex")
return template.render(monsters=monsters, use_dnd_decorations=use_dnd_decorations)
def create_spellbook_tex( def create_spellbook_tex(
character: Character, character: Character,
use_dnd_decorations: bool = False, use_dnd_decorations: bool = False,
@@ -103,23 +115,15 @@ def create_druid_shapes_tex(
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def make_sheet( def make_sheet(sheet_file: File,
character_file, flatten: bool = False,
character=None, fancy_decorations: bool = False,
flatten=False, debug: bool = False):
latex_template=True, """Make a character or GM sheet into a PDF.
fancy_decorations=False,
debug=False,
):
"""Prepare a PDF character sheet from the given character file.
Parameters Parameters
---------- ----------
character_file : str sheet_file
File (.py) to load character from. Will save PDF using same name File (.py) to load character from. Will save PDF using same name
character : Character, optional
If provided, will not load from the character file, just use
file for PDF name
flatten : bool, optional flatten : bool, optional
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.
@@ -129,18 +133,120 @@ def make_sheet(
debug : bool, optional debug : bool, optional
Provide extra info and preserve temporary files. Provide extra info and preserve temporary files.
"""
# Parse the file
sheet_file = Path(sheet_file)
base_name = sheet_file.stem
sheet_props = readers.read_sheet_file(sheet_file)
# Create the sheet
if sheet_props.get("sheet_type", "") == "gm":
ret = make_gm_sheet(basename=base_name, gm_props=sheet_props,
fancy_decorations=fancy_decorations,
debug=debug)
else:
ret = make_character_sheet(
basename=base_name,
character_props=sheet_props,
flatten=flatten,
fancy_decorations=fancy_decorations,
debug=debug)
return ret
def make_gm_sheet(
basename: str,
gm_props: Mapping,
fancy_decorations: bool = False,
debug: bool = False,
):
"""Prepare a PDF character sheet from the given character file.
Parameters
----------
basename
The basename for saving files.
gm_props
Properties for creating the GM notes.
fancy_decorations
Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
debug
Provide extra info and preserve temporary files.
"""
tex = [
jinja_env.get_template("preamble.tex").render(
use_dnd_decorations=fancy_decorations,
title=gm_props['session_title'],
)
]
# Add the monsters
monsters_ = [findattr(monsters, m)() for m in gm_props.get("monsters", [])]
if len(monsters_) > 0:
tex.append(
create_monsters_tex(monsters_, use_dnd_decorations=fancy_decorations)
)
# Add the closing TeX
tex.append(
jinja_env.get_template("postamble.tex").render(
use_dnd_decorations=fancy_decorations
)
)
# Typeset combined LaTeX file
try:
if len(tex) > 2:
latex.create_latex_pdf(
tex="".join(tex),
basename=basename,
keep_temp_files=debug,
use_dnd_decorations=fancy_decorations,
)
except exceptions.LatexNotFoundError:
log.warning(
f"``pdflatex`` not available. Skipping {basename}"
)
def make_character_sheet(
basename: str,
character_props: Mapping,
character: Character = None,
flatten: bool = False,
fancy_decorations: bool = False,
debug: bool = False,
):
"""Prepare a PDF character sheet from the given character file.
Parameters
----------
basename
The basename for saving files (PDFs, etc).
character_props
Properties to load character from.
character
If provided, will not load from the character file, just use
file for PDF name
flatten
If true, the resulting PDF will look better and won't be
fillable form.
fancy_decorations
Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
debug
Provide extra info and preserve temporary files.
""" """
if character is None: if character is None:
char_props = readers.read_sheet_file(character_file) character = _char.Character.load(character_props)
character = _char.Character.load(char_props)
# Set the fields in the FDF # Set the fields in the FDF
char_base = os.path.splitext(character_file)[0] + "_char" char_base = basename + "_char"
sheets = [char_base + ".pdf"] sheets = [char_base + ".pdf"]
pages = [] pages = []
tex = [ tex = [
jinja_env.get_template("preamble.tex").render( jinja_env.get_template("preamble.tex").render(
use_dnd_decorations=fancy_decorations use_dnd_decorations=fancy_decorations,
title="Features, Magical Items and Spells",
) )
] ]
@@ -151,14 +257,13 @@ def make_sheet(
pages.append(char_pdf) pages.append(char_pdf)
if character.is_spellcaster: if character.is_spellcaster:
# Create spell sheet # Create spell sheet
spell_base = "{:s}_spells".format(os.path.splitext(character_file)[0]) spell_base = "{:s}_spells".format(basename)
create_spells_pdf_template( create_spells_pdf_template(
character=character, basename=spell_base, flatten=flatten character=character, basename=spell_base, flatten=flatten
) )
sheets.append(spell_base + ".pdf") sheets.append(spell_base + ".pdf")
# end of PDF gen # end of PDF gen
features_base = "{:s}_features".format(basename)
features_base = "{:s}_features".format(os.path.splitext(character_file)[0])
# Create a list of subcasses # Create a list of subcasses
if character.subclasses: if character.subclasses:
tex.append( tex.append(
@@ -204,13 +309,13 @@ def make_sheet(
try: try:
if len(tex) > 2: if len(tex) > 2:
latex.create_latex_pdf( latex.create_latex_pdf(
"".join(tex), tex="".join(tex),
features_base, basename=features_base,
keep_temp_files=debug, keep_temp_files=debug,
use_dnd_decorations=fancy_decorations, use_dnd_decorations=fancy_decorations,
) )
sheets.append(features_base + ".pdf") sheets.append(features_base + ".pdf")
final_pdf = os.path.splitext(character_file)[0] + ".pdf" final_pdf = f"{basename}.pdf"
merge_pdfs(sheets, final_pdf, clean_up=True) merge_pdfs(sheets, final_pdf, clean_up=True)
except exceptions.LatexNotFoundError: except exceptions.LatexNotFoundError:
log.warning( log.warning(
@@ -250,7 +355,7 @@ def _build(filename, args) -> int:
print(f"Processing {basename}...") print(f"Processing {basename}...")
try: try:
make_sheet( make_sheet(
character_file=filename, sheet_file=filename,
flatten=(not args.editable), flatten=(not args.editable),
debug=args.debug, debug=args.debug,
fancy_decorations=args.fancy_decorations, fancy_decorations=args.fancy_decorations,
@@ -268,7 +373,7 @@ def _build(filename, args) -> int:
return 1 return 1
def main(): def main(args=None):
# Prepare an argument parser # Prepare an argument parser
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Prepare Dungeons and Dragons character sheets as PDFs" description="Prepare Dungeons and Dragons character sheets as PDFs"
@@ -307,7 +412,7 @@ def main():
action="store_true", action="store_true",
help="Provide verbose logging for debugging purposes.", help="Provide verbose logging for debugging purposes.",
) )
args = parser.parse_args() args = parser.parse_args(args)
# Prepare logging if necessary # Prepare logging if necessary
if args.debug: if args.debug:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@@ -326,6 +431,8 @@ def main():
valid_files.extend(get_char_files(f, parse_dirs=args.recursive)) valid_files.extend(get_char_files(f, parse_dirs=args.recursive))
elif fpath.suffix in known_extensions: elif fpath.suffix in known_extensions:
valid_files.append(fpath) valid_files.append(fpath)
else:
log.info(f"Unhandled file: {str(fpath)}")
return valid_files return valid_files
temp_filenames = [] temp_filenames = []
@@ -344,6 +451,7 @@ def main():
# Process the requested files # Process the requested files
if args.debug: if args.debug:
for filename in filenames: for filename in filenames:
print("building")
_build(filename, args) _build(filename, args)
else: else:
with Pool(cpu_count()) as p: with Pool(cpu_count()) as p:
+4
View File
@@ -8,3 +8,7 @@ monsters, etc.
dungeonsheets_version = "0.14.0" dungeonsheets_version = "0.14.0"
sheet_type = "gm" sheet_type = "gm"
session_title = "Objects in Space"
monsters = ["wolf", "giant eagle"]
+76 -2
View File
@@ -2,8 +2,8 @@
from unittest import TestCase from unittest import TestCase
from dungeonsheets import features from dungeonsheets import features, character
from dungeonsheets.features import create_feature, Feature, all_features from dungeonsheets.features import create_feature, Feature, all_features, bard
class TestFeatures(TestCase): class TestFeatures(TestCase):
@@ -31,3 +31,77 @@ class TestFeatures(TestCase):
self.assertEqual(NewFeature.name, "Hello world") self.assertEqual(NewFeature.name, "Hello world")
feature = NewFeature() feature = NewFeature()
print(feature, feature.__class__, type(feature)) print(feature, feature.__class__, type(feature))
class BardTests(TestCase):
def test_bardic_inspiration(self):
# Level 1-4 Bard
char = character.Character(classes=["bard"], levels=[2])
bi = bard.BardicInspiration(owner=char)
self.assertEqual(bi.name, "Bardic Inspiration (1d6/LR)")
# Level 5-9 Bard
char = character.Character(classes=["bard"], levels=[5])
bi = bard.BardicInspiration(owner=char)
self.assertEqual(bi.name, "Bardic Inspiration (1d8/SR)")
# Level 10-14 Bard
char = character.Character(classes=["bard"], levels=[10])
bi = bard.BardicInspiration(owner=char)
self.assertEqual(bi.name, "Bardic Inspiration (1d10/SR)")
# Level 15+ Bard
char = character.Character(classes=["bard"], levels=[15])
bi = bard.BardicInspiration(owner=char)
self.assertEqual(bi.name, "Bardic Inspiration (1d12/SR)")
def test_song_of_rest(self):
# Level 1-8 Bard
char = character.Character(classes=["bard"], levels=[2])
sor = bard.SongOfRest(owner=char)
self.assertEqual(sor.name, "Song of Rest (1d6)")
# Level 9-12 Bard
char = character.Character(classes=["bard"], levels=[9])
sor = bard.SongOfRest(owner=char)
self.assertEqual(sor.name, "Song of Rest (1d8)")
# Level 13-16 Bard
char = character.Character(classes=["bard"], levels=[13])
sor = bard.SongOfRest(owner=char)
self.assertEqual(sor.name, "Song of Rest (1d10)")
# Level 17+ Bard
char = character.Character(classes=["bard"], levels=[17])
sor = bard.SongOfRest(owner=char)
self.assertEqual(sor.name, "Song of Rest (1d12)")
def test_mantle_of_inspiration(self):
for lvl in range(1, 5):
char = character.Character(classes=["bard"], levels=[lvl])
moi = bard.MantleOfInspiration(owner=char)
self.assertEqual(moi.name, "Mantle of Inspiration (5HP)")
for lvl in range(5, 10):
char = character.Character(classes=["bard"], levels=[lvl])
moi = bard.MantleOfInspiration(owner=char)
self.assertEqual(moi.name, "Mantle of Inspiration (8HP)")
for lvl in range(10, 15):
char = character.Character(classes=["bard"], levels=[lvl])
moi = bard.MantleOfInspiration(owner=char)
self.assertEqual(moi.name, "Mantle of Inspiration (11HP)")
for lvl in range(15, 20):
char = character.Character(classes=["bard"], levels=[lvl])
moi = bard.MantleOfInspiration(owner=char)
self.assertEqual(moi.name, "Mantle of Inspiration (14HP)")
def test_psychic_blades(self):
for lvl in range(1, 5):
char = character.Character(classes=["bard"], levels=[lvl])
pb = bard.PsychicBlades(owner=char)
self.assertEqual(pb.name, "Psychic Blades (2d6)")
for lvl in range(5, 10):
char = character.Character(classes=["bard"], levels=[lvl])
pb = bard.PsychicBlades(owner=char)
self.assertEqual(pb.name, "Psychic Blades (3d6)")
for lvl in range(10, 15):
char = character.Character(classes=["bard"], levels=[lvl])
pb = bard.PsychicBlades(owner=char)
self.assertEqual(pb.name, "Psychic Blades (5d6)")
for lvl in range(15, 20):
char = character.Character(classes=["bard"], levels=[lvl])
pb = bard.PsychicBlades(owner=char)
self.assertEqual(pb.name, "Psychic Blades (8d6)")
+51 -3
View File
@@ -1,12 +1,55 @@
import unittest import unittest
import os import os
from pathlib import Path
from dungeonsheets import make_sheets, character from dungeonsheets import make_sheets, character, monsters
from dungeonsheets.classes import monk from dungeonsheets.classes import monk
EG_DIR = Path(__file__).parent.parent.resolve() / "examples"
CHARFILE = EG_DIR / "rogue1.py"
GMFILE = EG_DIR / "gm.py"
EG_DIR = os.path.abspath(os.path.join(os.path.split(__file__)[0], "../examples/"))
CHARFILE = os.path.join(EG_DIR, "rogue1.py") class MakeSheetsTestCase(unittest.TestCase):
char_pdf = Path(f"{CHARFILE.stem}.pdf")
gm_pdf = Path(f"{GMFILE.stem}.pdf").resolve()
def tearDown(self):
if self.char_pdf.exists():
self.char_pdf.unlink()
if self.gm_pdf.exists():
self.gm_pdf.unlink()
def test_main(self):
make_sheets.main(args=[str(CHARFILE), "--debug"])
def test_make_sheets(self):
# Character PDF
make_sheets.make_sheet(sheet_file=CHARFILE)
# Was the PDF created?
self.assertTrue(self.char_pdf.exists(),
f"Character PDF ({self.char_pdf.resolve()}) not created.")
# GM PDF
make_sheets.make_sheet(sheet_file=GMFILE)
self.assertTrue(self.gm_pdf.exists)
# Was the PDF created?
self.assertTrue(self.gm_pdf.exists(),
f"GM PDF ({self.gm_pdf.resolve()}) not created.")
def test_make_fancy_sheets(self):
# Character PDF
make_sheets.make_sheet(sheet_file=CHARFILE,
fancy_decorations=True)
# Was the PDF created?
self.assertTrue(self.char_pdf.exists(),
f"Character PDF ({self.char_pdf.resolve()}) not created.")
# GM PDF
make_sheets.make_sheet(sheet_file=GMFILE,
fancy_decorations=True)
self.assertTrue(self.gm_pdf.exists)
# Was the PDF created?
self.assertTrue(self.gm_pdf.exists(),
f"GM PDF ({self.gm_pdf.resolve()}) not created.")
class PdfOutputTestCase(unittest.TestCase): class PdfOutputTestCase(unittest.TestCase):
@@ -80,3 +123,8 @@ class TexCreatorTestCase(unittest.TestCase):
tex = make_sheets.create_druid_shapes_tex(character=char) tex = make_sheets.create_druid_shapes_tex(character=char)
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)
def test_create_monsters_tex(self):
monsters_ = [monsters.GiantEagle()]
tex = make_sheets.create_monsters_tex(monsters=monsters_)
self.assertIn(r"Giant eagle", tex)