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
Пример #2
0
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
Пример #3
0
 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'")
Пример #6
0
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)
Пример #7
0
 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)
Пример #8
0
 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)
Пример #9
0
 def test_load_python_character(self):
     charfile = CHAR_PYTHON_FILE
     result = read_sheet_file(charfile)
     self.assertEqual(result["strength"], 10)
Пример #10
0
 def test_load_python_gm_sheet(self):
     gmfile = GM_PYTHON_FILE
     result = read_sheet_file(gmfile)
     self.assertEqual(result["sheet_type"], "gm")
Пример #11
0
 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)
Пример #12
0
 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")