def make_sheet( sheet_file: File, flatten: bool = False, output_format: str = "pdf", fancy_decorations: bool = False, debug: bool = False, use_tex_template: bool = False, ): """Make a character or GM sheet into a PDF. Parameters ---------- sheet_file File (.py) to load character from. Will save PDF using same name flatten : bool, optional 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 : bool, optional Use fancy page layout and decorations for extra sheets, namely the dnd style file: ://github.com/rpgtex/DND-5e-LaTeX-Template. debug : bool, optional Provide extra info and preserve temporary files. use_tex_template : bool, optional (experimental) Use the DnD LaTeX character sheet instead of the fillable PDF. """ # Parse the file sheet_file = Path(sheet_file) sheet_props = readers.read_sheet_file(sheet_file) # Create the sheet if sheet_props.get("sheet_type", "") == "gm": ret = make_gm_sheet( gm_file=sheet_file, output_format=output_format, fancy_decorations=fancy_decorations, debug=debug, ) else: ret = make_character_sheet(char_file=sheet_file, flatten=flatten, output_format=output_format, fancy_decorations=fancy_decorations, debug=debug, use_tex_template=use_tex_template) return ret
def make_sheet( sheet_file: File, flatten: bool = False, fancy_decorations: bool = False, debug: bool = False, ): """Make a character or GM sheet into a PDF. Parameters ---------- sheet_file File (.py) to load character from. Will save PDF using same name flatten : bool, optional If true, the resulting PDF will look better and won't be fillable form. fancy_decorations : bool, optional Use fancy page layout and decorations for extra sheets, namely the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. debug : bool, optional 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 test_load_json_spells(self): charfile = SPELLCASTER_JSON_FILE with warnings.catch_warnings(record=True): result = read_sheet_file(charfile) expected_data = dict( spells_prepared=[ "cure wounds", ], spells=[ "spare the dying", "fire bolt", "absorb elements", "alarm", "catapult", "cure wounds", "detect magic", "disguise self", "expeditious retreat", "faerie fire", "false life", "feather fall", "grease", "identify", "jump", "longstrider", "purify food and drink", "sanctuary", "snare", ], ) for key, val in expected_data.items(): this_result = result[key] # Force evaluation of generators if isinstance(this_result, types.GeneratorType): this_result = list(this_result) self.assertEqual(this_result, val, key)
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, use_tex_template: bool = False, ): """Prepare a PDF character sheet from the given character file. Parameters ---------- basename The basename for saving files (PDFs, etc). 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. 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. debug Provide extra info and preserve temporary files. """ # Load properties from file if character is None: character_props = readers.read_sheet_file(char_file) character = _char.Character.load(character_props) # Load image file if present portrait_file = "" if character.portrait: portrait_file = char_file.stem + ".jpeg" # Set the fields in the FDF basename = char_file.stem char_base = basename + "_char" person_base = basename + "_person" sheets = [char_base + ".pdf"] pages = [] # Prepare the tex/html content content_suffix = format_suffixes[output_format] # Create a list of features and magic items content = make_character_content(character=character, content_format=content_suffix, fancy_decorations=fancy_decorations) # Typeset combined LaTeX file if output_format == "pdf": if use_tex_template: msavage_sheet(character=character, basename=char_base, portrait_file=portrait_file, debug=debug) # Fillable PDF forms else: sheets.append(person_base + ".pdf") char_pdf = create_character_pdf_template(character=character, basename=char_base, flatten=flatten) pages.append(char_pdf) person_pdf = create_personality_pdf_template( character=character, basename=person_base, portrait_file=portrait_file, flatten=flatten) pages.append(person_pdf) if character.is_spellcaster and not (use_tex_template): # Create spell sheet spell_base = "{:s}_spells".format(basename) create_spells_pdf_template(character=character, basename=spell_base, flatten=flatten) sheets.append(spell_base + ".pdf") # Combined with additional LaTeX pages with detailed character info features_base = "{:s}_features".format(basename) 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=not (debug)) 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'")
def make_gm_sheet( gm_file: Union[str, Path], output_format: str = "pdf", fancy_decorations: bool = False, debug: bool = False, ): """Prepare a PDF character sheet from the given character file. Parameters ---------- gm_file The file with the gm_sheet definitions. 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. debug Provide extra info and preserve temporary files. """ # Parse the GM file and filename gm_file = Path(gm_file) basename = gm_file.stem gm_props = readers.read_sheet_file(gm_file) session_title = gm_props.pop("session_title", f"GM Notes: {basename}") # Create the intro tex content_suffix = format_suffixes[output_format] content = [ jinja_env.get_template(f"preamble.{content_suffix}").render( use_dnd_decorations=fancy_decorations, title=session_title, ) ] # Add the party stats table and session summary party = [] for char_file in gm_props.pop("party", []): # Check if it's already resolved if isinstance(char_file, Creature): member = char_file elif isinstance(char_file, type) and issubclass(char_file, Creature): # Needs to be instantiated member = char_file() else: # Resolve the file path char_file = Path(char_file) if not char_file.is_absolute(): char_file = gm_file.parent / char_file char_file = char_file.resolve() # Load the character file log.debug(f"Loading party member: {char_file}") character_props = readers.read_sheet_file(char_file) member = _char.Character.load(character_props) party.append(member) summary = gm_props.pop("summary", "") content.append( create_party_summary_content( party, summary_rst=summary, suffix=content_suffix, use_dnd_decorations=fancy_decorations, )) # Parse any extra homebrew sections, etc. content.append( create_extra_gm_content(sections=gm_props.pop("extra_content", []), suffix=content_suffix, use_dnd_decorations=fancy_decorations)) # Add the monsters monsters_ = [] for monster in gm_props.pop("monsters", []): if isinstance(monster, monsters.Monster): # It's already a monster, so just add it new_monster = monster else: try: MyMonster = find_content(monster, valid_classes=[monsters.Monster]) except exceptions.ContentNotFound: msg = f"Monster '{monster}' not found. Please add it to ``monsters.py``" warnings.warn(msg) continue else: new_monster = MyMonster() monsters_.append(new_monster) if len(monsters_) > 0: content.append( create_monsters_content(monsters_, suffix=content_suffix, use_dnd_decorations=fancy_decorations)) # Add the random tables tables = [ find_content(s, valid_classes=[random_tables.RandomTable]) for s in gm_props.pop("random_tables", []) ] content.append( create_random_tables_content( tables=tables, suffix=content_suffix, use_dnd_decorations=fancy_decorations, )) # Add the closing TeX content.append( jinja_env.get_template(f"postamble.{format_suffixes[output_format]}"). render(use_dnd_decorations=fancy_decorations)) # Warn about any unhandled sheet properties gm_props.pop("dungeonsheets_version") gm_props.pop("sheet_type") if len(gm_props.keys()) > 0: msg = f"Unhandled attributes in '{str(gm_file)}': {','.join(gm_props.keys())}" log.warning(msg) warnings.warn(msg) # Produce the combined output depending on the format requested if output_format == "pdf": # Typeset combined LaTeX file try: if len(content) > 2: latex.create_latex_pdf( tex="".join(content), basename=basename, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError: log.warning(f"``pdflatex`` not available. Skipping {basename}") elif output_format == "epub": chapters = {session_title: "".join(content)} # Make sheets in the epub for each party member for char in party: char_html = make_character_content( char, "html", fancy_decorations=fancy_decorations) chapters[char.name] = "".join(char_html) # Create the combined HTML file epub.create_epub( chapters=chapters, basename=basename, title=session_title, use_dnd_decorations=fancy_decorations, ) else: raise exceptions.UnknownOutputFormat( f"Unknown output format requested: {output_format}. Valid options are:" " 'pdf', 'epub'")
def make_sheet( character_file, character=None, flatten=False, fancy_decorations=False, debug=False ): """Prepare a PDF character sheet from the given character file. Parameters ---------- basename The basename for saving files (PDFs, etc). 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. 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. debug Provide extra info and preserve temporary files. """ # Load properties from file if character is None: character_props = readers.read_sheet_file(char_file) character = _char.Character.load(character_props) # Load image file if present portrait_file="" if character.portrait: portrait_file=char_file.stem + ".jpeg" # Set the fields in the FDF char_base = os.path.splitext(character_file)[0] + "_char" sheets = [char_base + ".pdf"] pages = [] char_pdf = create_character_pdf( character=character, basename=char_base, flatten=flatten ) pages.append(char_pdf) if character.is_spellcaster: # Create spell sheet spell_base = "{:s}_spells".format(os.path.splitext(character_file)[0]) create_spells_pdf(character=character, basename=spell_base, flatten=flatten) sheets.append(spell_base + ".pdf") if len(character.features) > 0: feat_base = "{:s}_feats".format(os.path.splitext(character_file)[0]) try: create_features_pdf( character=character, basename=feat_base, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError as e: log.warning( "``pdflatex`` not available. Skipping features book " f"for {character.name}" ) else: sheets.append(feat_base + ".pdf") if character.is_spellcaster: # Create spell book spellbook_base = os.path.splitext(character_file)[0] + "_spellbook" try: create_spellbook_pdf( character=character, basename=spellbook_base, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError as e: log.warning( "``pdflatex`` not available. Skipping spellbook " f"for {character.name}" ) else: sheets.append(spellbook_base + ".pdf") # Create a list of Artificer infusions infusions = getattr(character, "infusions", []) if len(infusions) > 0: infusions_base = os.path.splitext(character_file)[0] + "_infusions" try: create_infusions_pdf( character=character, basename=infusions_base, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError as e: log.warning( "``pdflatex`` not available. Skipping infusions list " f"for {character.name}" ) else: sheets.append(infusions_base + ".pdf") # Create a list of Druid wild_shapes wild_shapes = getattr(character, "wild_shapes", []) if len(wild_shapes) > 0: shapes_base = os.path.splitext(character_file)[0] + "_wild_shapes" try: create_druid_shapes_pdf( character=character, basename=shapes_base, keep_temp_files=debug, use_dnd_decorations=fancy_decorations, ) except exceptions.LatexNotFoundError as e: log.warning( "``pdflatex`` not available. Skipping wild shapes list " f"for {character.name}" ) else: sheets.append(shapes_base + ".pdf") # Combine sheets into final pdf final_pdf = os.path.splitext(character_file)[0] + ".pdf" merge_pdfs(sheets, final_pdf, clean_up=True)
def test_load_json_file(self): charfile = ROLL20_JSON_FILE with warnings.catch_warnings(record=True): result = read_sheet_file(charfile) expected_data = dict( name="Ulthar Jenkins", classes=["Barbarian"], level=2, background="Soldier", alignment="Lawful Evil", race="Hill Dwarf", xp=557, strength=13, dexterity=12, constitution=19, intelligence=8, hp_max=32, skill_proficiencies=[ "athletics", "survival", ], weapon_proficiencies=[ "simple weapons", "martial weapons", "battleaxe", "handaxe", "light hammer", "warhammer", "unarmed strike", ], _proficiencies_text=[ "Brewer's Supplies", ], languages="common, dwarvish", cp=26, sp=55, ep=0, gp=207, pp=0, weapons=["handaxe", "javelin", "warhammer"], magic_items=(), armor="", shield="", personality_traits=( "Can easily dismember a body\n\nKnow fight battle tactics"), ideals="Vengence", bonds="friends and adventurers.", flaws="Bloodthirsty and wants to solve every problem by murder", equipment=( "warhammer, handaxe, explorer's pack, javelin (4), backpack, " "bedroll, mess kit, tinderbox, torch (10), rations (10), " "waterskin, hempen rope"), attacks_and_spellcasting="", spells_prepared=[], spells=[], ) for key, val in expected_data.items(): this_result = result[key] # Force evaluation of generators if isinstance(this_result, types.GeneratorType): this_result = list(this_result) self.assertEqual(this_result, val, key)
def test_load_bad_file(self): """This file is not a valid character, so should fail.""" this_file = __file__ with self.assertRaises(exceptions.CharacterFileFormatError): read_sheet_file(this_file)
def test_load_python_character(self): charfile = CHAR_PYTHON_FILE result = read_sheet_file(charfile) self.assertEqual(result["strength"], 10)
def test_load_python_gm_sheet(self): gmfile = GM_PYTHON_FILE result = read_sheet_file(gmfile) self.assertEqual(result["sheet_type"], "gm")
def test_load_json_file(self): charfile = FOUNDRY_JSON_FILE with warnings.catch_warnings(record=True): result = read_sheet_file(charfile) expected_data = dict( name="Sam Lloyd", classes=["Bard"], levels=[6], background="Attorney", alignment="Lawful Neutral", race="Variant", xp=0, strength=6, dexterity=12, constitution=14, intelligence=10, wisdom=12, charisma=18, hp_max=47, skill_proficiencies=[ "deception", "insight", "investigation", "perception", "persuasion", "sleight_of_hand", ], skill_expertise=[ "insight", "persuasion", ], weapon_proficiencies=[ "simple weapons", "martial weapons", "crossbow", "knives", ], _proficiencies_text=[ "artisan's tools", "disguise kit", "forger's kit", "gaming set", "herbalism kit", "musical instrument", "navigator's tools", "poisoner's kit", "thieves' tools", "vehicle (land or water)", "chopsticks", "juggling balls" ], saving_throw_proficiencies=["dexterity", "charisma"], languages="common, elvish, law jargon, spanish", cp=0, sp=0, ep=0, gp=162, pp=2, weapons=["rapier"], magic_items=(), armor="padded armor", shield="shield", personality_traits="Loves a good lawyer joke.", ideals="Every form in triplicate.", bonds="Just show up to your court date and it won't be a problem.", flaws="Too many to list.", equipment= ("rations(7), ring of acid resistance, cartographer's tools, bag of holding, diamonds(20), padded armor, shield" ), attacks_and_spellcasting="", spells_prepared=[ "Bane", "Faerie Fire", "Thunderwave", "Detect Thoughts" ], spells=[ "Vicious Mockery", "Message", "Prestidigitation", "Bane", "Faerie Fire", "Thunderwave", "Healing Word", "Blindness/Deafness", "Detect Thoughts", "Hold Person", "Fear", "Heat Metal" ], ) for key, val in expected_data.items(): this_result = result[key] # Force evaluation of generators if isinstance(this_result, types.GeneratorType): this_result = list(this_result) self.assertEqual(this_result, val, key)
def test_cascading_sheets(self): with self.inherited_sheets() as (child, parent): char_props = read_sheet_file(child) self.assertEqual(char_props["name"], "Douglass Adams") self.assertEqual(char_props["background"], "entertainer")