diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 67afed8..be8bcd0 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -709,6 +709,123 @@ class Character(Creature): s = "(See Features Page)\n\n--" + s s += "\n\n=================\n\n" return s + + @property + def features_summary(self): + # save space for informed features and traits + if hasattr(self, "features_and_traits"): + info_list = ["**Other Features**"] + info_list += [text.strip() for text in self.features_and_traits.split("\n") + if not(text.isspace())] + N = len(info_list) + for text in info_list: + if len(text) > 26: # 26 is just a guess for expected size of lines + N += 1 + if N > 30: + return "\n".join(info_list[:30]) + "\n(...)" + N = 30 - N + else: + info_list = [] + N = 30 + if len(self.class_list) > 1: + featS = ["**Multiclass**:"] + for cl in self.class_list: + description = cl.name + if cl.subclass: + description += f"/{cl.subclass}" + description += f" {cl.level}" + featS.append(description) + else: + featS = [] + featS += ["**Features**"] + featS += [f.name for f in self.features] + if len(featS) > N: + featS = featS[:N] + ["(...)"] + featS += info_list + return "\n\n".join(featS) + + @property + def equipment_text(self): + eq_list = [] + if hasattr(self, "magic_items"): + eq_list += ["**Magic Items**"] + eq_list += [item.name for item in self.magic_items] + if hasattr(self, "equipment"): + eq_list += ["**Other Equipment**"] + eq_list += [text.strip() for text in self.equipment.split("\n") + if not(text.isspace())] + return "\n\n".join(eq_list) + + @property + def proficiencies_by_type(self): + prof_dict = {} + w_pro = set(self.weapon_proficiencies) + if weapons.MartialWeapon in w_pro: + prof_dict["Weapons"] = ["All weapons"] + elif weapons.SimpleWeapon in w_pro: + prof_dict["Weapons"] = ["Simple weapons"] + for w in w_pro: + if not(issubclass(w, weapons.SimpleWeapon)): + prof_dict["Weapons"] += [w.name] + else: + prof_dict["Weapons"] = [w.name for w in w_pro] + if "Weapons" in prof_dict.keys(): + prof_dict["Weapons"] = ", ".join(prof_dict["Weapons"]) + "." + armor_types = ["all armor", "light armor", "medium armor", + "heavy armor"] + prof_set = set([prof.lower().strip().strip('.') + for prof in self.proficiencies_text.split(',')]) + prof_dict["Armor"] = [ar for ar in armor_types if ar in prof_set] + if len(prof_dict["Armor"]) > 2 or 'all armor' in prof_set: + prof_dict["Armor"] = ["All armor"] + if 'shields' in prof_set: + prof_dict["Armor"] += ["shields"] + prof_dict["Armor"] = ", ".join(prof_dict["Armor"]) + "." + if hasattr(self, 'chosen_tools'): + prof_dict["Other"] = self.chosen_tools + return prof_dict + + @property + def spell_casting_info(self): + """Returns a ready-to-use dictionary for spellsheets.""" + level_names = ["Cantrip", + 'FirstLevelSpell', + 'SecondLevelSpell', + 'ThirdLevelSpell', + 'FourthLevelSpell', + 'FifthLevelSpell', + 'SixthLevelSpell', + 'SeventhLevelSpell', + 'EighthLevelSpell', + 'NinthLevelSpell'] + spell_info = {'head':{ + "classes_and_levels": " / ".join( + [c.name + " " + str(c.level) for c in self.spellcasting_classes] + ), + "abilities": " / ".join( + [c.spellcasting_ability.upper()[:3] + for c in self.spellcasting_classes] + ), + "DCs": " / ".join( + [str(self.spell_save_dc(c)) + for c in self.spellcasting_classes] + ), + "bonuses": " / ".join( + ["{:+d}".format(self.spell_attack_bonus(c)) + for c in self.spellcasting_classes] + ), + }} + slots = {level_names[k]:self.spell_slots(k) for k in range(1, 10) + if self.spell_slots(k) > 0} + spell_info["slots"] = slots + spell_list = {} + for s in self.spells: + prepared = s in self.spells_prepared + level_info = level_names[s.level] + info_there = spell_list.get(level_info, []) + spell_list[level_info] = info_there + [(s.name, prepared)] + spell_info["list"] = spell_list + return spell_info @property def magic_items_text(self): diff --git a/dungeonsheets/latex.py b/dungeonsheets/latex.py index 35ac51e..5aed3b5 100644 --- a/dungeonsheets/latex.py +++ b/dungeonsheets/latex.py @@ -49,6 +49,7 @@ def create_latex_pdf( basename: str, keep_temp_files: bool = False, use_dnd_decorations: bool = False, + comm1: str = "pdflatex" ): # Create tex document tex_file = f"{basename}.tex" @@ -59,7 +60,7 @@ def create_latex_pdf( pdf_file = Path(f"{basename}.pdf") output_dir = pdf_file.resolve().parent tex_command_line = [ - "pdflatex", + comm1, "--output-directory", str(output_dir), "-halt-on-error", @@ -200,3 +201,68 @@ def rst_to_latex(rst, top_heading_level=0): tex_parts = latex_parts(rst) tex = tex_parts["body"] return tex + +def rst_to_boxlatex(rst): + """Adapted rst translation from dungeonsheets latex module, removing + dice parsing and indentation.""" + + if rst is None: + return "" + tex_parts = latex_parts(rst) + tex = tex_parts["body"] + tex = tex.replace('\n\n', ' \\\\\n') + return tex + +def msavage_spell_info(char): + """Generates the spellsheet for msavage template.""" + headinfo = char.spell_casting_info["head"] + sc_classes = r"\SpellcastingClass{" \ + + headinfo["classes_and_levels"] \ + + "}" + sc_abilities = r"\SpellcastingAbility{" \ + + headinfo["abilities"] \ + + "}" + sc_savedc = r"\SpellSaveDC{" \ + + headinfo["DCs"] \ + + "}" + sc_atk = r"\SpellAttackBonus{" \ + + headinfo["bonuses"] \ + + "}" + tex1 = "\n".join([sc_classes, sc_abilities, sc_savedc, sc_atk]) + "\n" + spellslots = char.spell_casting_info["slots"] + texT = [] + for k, v in spellslots.items(): + texT.append("\\" + k + "SlotsTotal{" + str(v) + "}") + tex2 = "\n".join(texT) + "\n" + texT = [] + level_names = level_names = ["Cantrip", + 'FirstLevelSpell', + 'SecondLevelSpell', + 'ThirdLevelSpell', + 'FourthLevelSpell', + 'FifthLevelSpell', + 'SixthLevelSpell', + 'SeventhLevelSpell', + 'EighthLevelSpell', + 'NinthLevelSpell'] + sheet_spaces = dict(zip(level_names, + [8, 13, 13, 13, 13, 9, 9, 9, 7, 7])) + comp_letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", + "L", "M"] + spellList = char.spell_casting_info["list"] + for k, v in spellList.items(): + slots_max = sheet_spaces[k] + if len(v) > slots_max: + vsel = sorted(v, key=lambda x: x[1], reverse=True) + else: + vsel = v[:] + for spinfo, slot in zip(vsel[:slots_max], comp_letters): + slot_command = "\\"+k+'Slot'+slot + slot_command_name = slot_command+"{"+spinfo[0]+"}" + if k == "Cantrip": + texT = texT + [slot_command_name] + continue + slot_command_prep = slot_command+"Prepared"+"{"+str(spinfo[1])+"}" + texT = texT + [slot_command_name, slot_command_prep] + tex3 = "\n".join(texT) + '\n' + return "\n".join([tex1, tex2, tex3]) diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index fccf809..f2c2c8a 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -55,7 +55,8 @@ jinja_env = forms.jinja_environment() jinja_env.filters["rst_to_latex"] = latex.rst_to_latex jinja_env.filters["rst_to_html"] = epub.rst_to_html jinja_env.filters["to_heading_id"] = epub.to_heading_id - +jinja_env.filters["boxed"] = latex.rst_to_boxlatex +jinja_env.filters["spellsheetparser"] = latex.msavage_spell_info # Custom types File = Union[Path, str] @@ -137,6 +138,7 @@ def make_sheet( 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 @@ -173,6 +175,7 @@ def make_sheet( output_format=output_format, fancy_decorations=fancy_decorations, debug=debug, + use_tex_template=use_tex_template ) return ret @@ -406,6 +409,27 @@ def make_character_content( ) return content +def msavage_sheet(character, basename, portrait_file="", debug=False): + """Another adaption. All changes can be easily included as options + in the orignal functions, though.""" + + # Load image file if present + portrait_command="" + if character.portrait and portrait_file: + portrait_command = r"\includegraphics[width=5.75cm]{"+ \ + portrait_file + "}" + + + tex = jinja_env.get_template("MSavage_template.tex").render( + char=character, portrait=portrait_command + ) + latex.create_latex_pdf( + tex, + basename=basename, + keep_temp_files=debug, + use_dnd_decorations=True, + comm1="xelatex" + ) def make_character_sheet( char_file: Union[str, Path], @@ -414,6 +438,7 @@ def make_character_sheet( 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. @@ -448,7 +473,7 @@ def make_character_sheet( basename = char_file.stem char_base = basename + "_char" person_base = basename + "_person" - sheets = [char_base + ".pdf", person_base + ".pdf"] + sheets = [char_base + ".pdf"] pages = [] # Prepare the tex/html content content_suffix = format_suffixes[output_format] @@ -458,16 +483,24 @@ def make_character_sheet( 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 - char_pdf = create_character_pdf_template( + 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: + ) + 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( @@ -486,7 +519,7 @@ def make_character_sheet( ) sheets.append(features_base + ".pdf") final_pdf = f"{basename}.pdf" - merge_pdfs(sheets, final_pdf, clean_up=True) + merge_pdfs(sheets, final_pdf, clean_up=not(debug)) except exceptions.LatexNotFoundError: log.warning( f"``pdflatex`` not available. Skipping features for {character.name}"