From e7ccb6f9f3e5c52d39c9e053bbd5f37fd9e0a0f5 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 11 Aug 2021 21:56:03 -0500 Subject: [PATCH] Added more flexible way of specifying random tables in the GM sheet. --- dungeonsheets/content.py | 161 ++++----- dungeonsheets/forms/preamble.tex | 26 +- .../forms/random_tables_template.tex | 277 +-------------- dungeonsheets/latex.py | 15 +- dungeonsheets/make_sheets.py | 21 +- dungeonsheets/monsters/monsters.py | 4 +- dungeonsheets/random_tables.py | 326 ++++++++++++++++++ tests/test_latex.py | 3 + tests/test_random_tables.py | 26 ++ 9 files changed, 493 insertions(+), 366 deletions(-) create mode 100644 dungeonsheets/random_tables.py create mode 100644 tests/test_random_tables.py diff --git a/dungeonsheets/content.py b/dungeonsheets/content.py index 0bffb13..f494d17 100644 --- a/dungeonsheets/content.py +++ b/dungeonsheets/content.py @@ -26,6 +26,87 @@ class Content(ABC): dungeonsheets_version = __version__ name = "Generic content" + @staticmethod + def _resolve_mechanic(mechanic, SuperClass, warning_message=None): + """Take a raw entry in a character sheet and turn it into a usable object. + + Eg: spells can be defined in many ways. This function accepts all + of those options and returns an actual *Spell* class that can be + used by a character:: + + >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) + + >>> _resolve_mechanic("mage_hand", SuperClass=None) + + >>> from dungeonsheets import spells + >>> class MySpell(spells.Spell): pass + >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) + + >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) + + The acceptable entries for *mechanic*, in priority order, are: + 1. A subclass of *SuperClass* + 2. A string with the name of defined content + 3. The name of an unknown spell (creates generic object using *factory*) + + *SuperClass* can be ``None`` to match any class, however this will + raise an exception if more than one content type has this + name. For example, "shield" can refer to both the armor or the + spell, so ``_resolve_mechanic("shield")`` will raise an exception. + + Parameters + ========== + mechanic : str, type + The thing to be resolved, either a string with the name of the + mechanic, or a subclass of *ParentClass* describing the + mechanic. + SuperClass : type + Class to determine whether *mechanic* should just be allowed + through as is. + error_message : str, optional + A string whose ``str.format()`` method (receiving one positional + argument *mechanic*) will be used for displaying a warning when an + unknown mechanic is resolved. If omitted, no warning will be + displayed. + + Returns + ======= + Mechanic + A class representing the resolved game mechanic. This will + likely be a subclass of *SuperClass* if the other parameters are + well behaved, but this is not enforced. + + """ + is_already_resolved = isinstance(mechanic, type) and issubclass( + mechanic, SuperClass + ) + if is_already_resolved: + Mechanic = mechanic + elif SuperClass is not None and isinstance(mechanic, SuperClass): + # Has been instantiated for some reason + Mechanic = type(Mechanic) + else: + try: + # Retrieve pre-defined mechanic + valid_classes = [SuperClass] if SuperClass is not None else [] + Mechanic = find_content(mechanic, valid_classes=valid_classes) + except ValueError: + # No pre-defined mechanic available + if warning_message is not None: + # Emit the warning + msg = warning_message.format(mechanic) + warnings.warn(msg) + else: + # Create a generic message so we can make a docstring later. + msg = f'Mechanic "{mechanic}" not defined. Please add it.' + # Create generic mechanic from the factory + class_name = "".join([s.title() for s in mechanic.split("_")]) + mechanic_name = mechanic.replace("_", " ").title() + attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} + Mechanic = type(class_name, (SuperClass,), attrs) + return Mechanic + + class Creature(Content): """A thing with stats. Use Monster or Character, not this class @@ -119,83 +200,3 @@ class Creature(Content): self.medicine, self.nature, self.perception, self.performance, self.persuasion, self.religion, self.sleight_of_hand, self.stealth, self.survival] - - @staticmethod - def _resolve_mechanic(mechanic, SuperClass, warning_message=None): - """Take a raw entry in a character sheet and turn it into a usable object. - - Eg: spells can be defined in many ways. This function accepts all - of those options and returns an actual *Spell* class that can be - used by a character:: - - >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) - - >>> _resolve_mechanic("mage_hand", SuperClass=None) - - >>> from dungeonsheets import spells - >>> class MySpell(spells.Spell): pass - >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) - - >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) - - The acceptable entries for *mechanic*, in priority order, are: - 1. A subclass of *SuperClass* - 2. A string with the name of defined content - 3. The name of an unknown spell (creates generic object using *factory*) - - *SuperClass* can be ``None`` to match any class, however this will - raise an exception if more than one content type has this - name. For example, "shield" can refer to both the armor or the - spell, so ``_resolve_mechanic("shield")`` will raise an exception. - - Parameters - ========== - mechanic : str, type - The thing to be resolved, either a string with the name of the - mechanic, or a subclass of *ParentClass* describing the - mechanic. - SuperClass : type - Class to determine whether *mechanic* should just be allowed - through as is. - error_message : str, optional - A string whose ``str.format()`` method (receiving one positional - argument *mechanic*) will be used for displaying a warning when an - unknown mechanic is resolved. If omitted, no warning will be - displayed. - - Returns - ======= - Mechanic - A class representing the resolved game mechanic. This will - likely be a subclass of *SuperClass* if the other parameters are - well behaved, but this is not enforced. - - """ - is_already_resolved = isinstance(mechanic, type) and issubclass( - mechanic, SuperClass - ) - if is_already_resolved: - Mechanic = mechanic - elif SuperClass is not None and isinstance(mechanic, SuperClass): - # Has been instantiated for some reason - Mechanic = type(Mechanic) - else: - try: - # Retrieve pre-defined mechanic - valid_classes = [SuperClass] if SuperClass is not None else [] - Mechanic = find_content(mechanic, valid_classes=valid_classes) - except ValueError: - # No pre-defined mechanic available - if warning_message is not None: - # Emit the warning - msg = warning_message.format(mechanic) - warnings.warn(msg) - else: - # Create a generic message so we can make a docstring later. - msg = f'Mechanic "{mechanic}" not defined. Please add it.' - # Create generic mechanic from the factory - class_name = "".join([s.title() for s in mechanic.split("_")]) - mechanic_name = mechanic.replace("_", " ").title() - attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} - Mechanic = type(class_name, (SuperClass,), attrs) - return Mechanic diff --git a/dungeonsheets/forms/preamble.tex b/dungeonsheets/forms/preamble.tex index a36be1e..1e7bb72 100644 --- a/dungeonsheets/forms/preamble.tex +++ b/dungeonsheets/forms/preamble.tex @@ -21,7 +21,19 @@ \usepackage[dvipsnames]{color} \setlength{\zerosep}{-1em} [% endif %] -\definecolor{mygrey}{gray}{0.7} + \definecolor{mygrey}{gray}{0.7} + +%% Unicode definitions for superscripts/subscripts +\DeclareUnicodeCharacter{00B9}{\textsuperscript{1}} +\DeclareUnicodeCharacter{00B2}{\textsuperscript{2}} +\DeclareUnicodeCharacter{00B3}{\textsuperscript{3}} +\DeclareUnicodeCharacter{2074}{\textsuperscript{4}} +\DeclareUnicodeCharacter{2075}{\textsuperscript{5}} +\DeclareUnicodeCharacter{2076}{\textsuperscript{6}} +\DeclareUnicodeCharacter{2077}{\textsuperscript{7}} +\DeclareUnicodeCharacter{2078}{\textsuperscript{8}} +\DeclareUnicodeCharacter{2079}{\textsuperscript{9}} +\DeclareUnicodeCharacter{2070}{\textsuperscript{0}} %%% Fallback definitions for Docutils-specific commands [% raw %] @@ -39,6 +51,18 @@ \end{center} \fi } +\newenvironment{DUlineblock}[1]{% + \list{}{\setlength{\partopsep}{\parskip} + \addtolength{\partopsep}{\baselineskip} + \setlength{\topsep}{0pt} + \setlength{\itemsep}{0.15\baselineskip} + \setlength{\parsep}{0pt} + \setlength{\leftmargin}{#1}} + \raggedright +} + +% titlereference standard role +\providecommand*{\DUroletitlereference}[1]{\textsl{#1}} % title for topics, admonitions, unsupported section levels, and sidebar \providecommand*{\DUtitle}[2][class-arg]{% diff --git a/dungeonsheets/forms/random_tables_template.tex b/dungeonsheets/forms/random_tables_template.tex index df266d0..ec8bde3 100644 --- a/dungeonsheets/forms/random_tables_template.tex +++ b/dungeonsheets/forms/random_tables_template.tex @@ -1,277 +1,10 @@ \pdfbookmark[0]{Random Tables}{Random Tables} \section*{Random Tables} -[% if conjure_animals %] +[% for table in tables %] +\pdfbookmark[0]{[[ table.name ]]}{Random Table - [[ table.name ]]} +\subsection*{[[ table.name ]]} - %% https://the-azure-triskele.obsidianportal.com/wikis/conjure-animals-table - \pdfbookmark[1]{Conjure Animals}{Random Tables - Conjure Animals} - \subsection*{Conjure Animals} +[[ table.__doc__ | rst_to_latex(format_dice=False, use_dnd_decorations=use_dnd_decorations) ]] - %% Which category of beasts to summon - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d4 & Number of Beasts \\ - [% else %] - \begin{tabular}{c | l} - 1d4 & Number of Beasts \\ - \hline\hline - [% endif %] - 1 & One beast of challenge rating 2 \\ - 2 & Two beasts of challenge rating 1 \\ - 3 & Four beasts of challenge rating 1/2 \\ - 4 & Eight beasts of challenge rating 1/4 or lower \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - - %% CR2 Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d20 & CR2 Beasts \\ - [% else %] - \begin{tabular}{c | l} - 1d20 & Challenge Rating 2 Beasts \\ - \hline\hline - [% endif %] - 1-2 & Allosaurus \\ - 3-4 & Giant Boar \\ - 5-6 & Giant Constrictor Snake \\ - 7-8 & Giant Elk \\ - 9-10 & Hunter Shark \\ - 11 & Plesiosaurus \\ - 12-13 & Polar Bear \\ - 14-15 & Rhinoceros \\ - 16-17 & Saber-toothed Tiger \\ - 18-19 & Swarm of Poisonous Snakes \\ - 20 & Roll on CR 1 Beast Table \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - - %% CR1 Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d12 & Challenge Rating 1 Beasts \\ - [% else %] - \begin{tabular}{c | l} - 1d12 & Challenge Rating 1 Beasts \\ - \hline\hline - [% endif %] - 1 & Brown Bear \\ - 2 & Dire Wolf \\ - 3 & Fire Snake \\ - 4 & Giant Eagle \\ - 5 & Giant Hyena \\ - 6 & Giant Octopus \\ - 7 & Giant Spider \\ - 8 & Giant Toad \\ - 9 & Giant Vulture \\ - 10 & Lion \\ - 11 & Tiger \\ - 12 & Roll on CR 1/2 Beast Table \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - %% CR1/2 Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d20 & Challenge Rating $\frac{1}{2}$ Beasts \\ - [% else %] - \begin{tabular}{c | l} - 1d20 & Challenge Rating $\frac{1}{2}$ Beasts \\ - \hline\hline - [% endif %] - 1-2 & Ape \\ - 3-4 & Black Bear \\ - 5-6 & Crocodile \\ - 7-8 & Giant Goat \\ - 9-10 & Giant Sea Horse \\ - 11-12 & Giant Wasp \\ - 13-14 & Reef Shark \\ - 15-16 & Swarm of Insects (below) \\ - 17-18 & Warhorse \\ - 19 & Worg \\ - 20 & Roll on Lesser Beast Menu Table \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - - %% Swarm of insects (mostly for flavor) - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d6 & Swarm of Insects \\ - [% else %] - \begin{tabular}{c | l} - 1d6 & Swarm of Insects \\ - \hline\hline - [% endif %] - 1 & Ant \\ - 2 & Beatles \\ - 3 & Centipedes \\ - 4 & Locusts \\ - 5 & Spiders \\ - 6 & Wasps \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - - %% Challenge Rating 1/4 and Lesser Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d6 & CR $\frac{1}{4}$ and Lesser Beast Menu \\ - [% else %] - \begin{tabular}{c | l} - 1d6 & CR $\frac{1}{4}$ and Lesser Beast Menu \\ - \hline\hline - [% endif %] - 1-2 & Menu A \\ - 3-4 & Menu B \\ - 5-6 & Menu C \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - - %% CR1/4 and Lesser Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d20 & Lesser Beast Menu A \\ - [% else %] - \begin{tabular}{c | l} - 1d12 & Lesser Beast Menu A \\ - \hline\hline - [% endif %] - 1 & Axe Beak \\ - 2 & Baboon \\ - 3 & Badger \\ - 4 & Bat \\ - 5 & Blood Hawk \\ - 6 & Boar \\ - 7 & Camel \\ - 8 & Cat \\ - 9 & Chicken* \\ - 10 & Constrictor Snake \\ - 11 & Crab \\ - 12 & Deer \\ - 13 & Draft Horse \\ - 14 & Eagle \\ - 15 & Elk \\ - 16 & Flying Snake \\ - 17 & Frog \\ - 18 & Giant Badger \\ - 19 & Giant Bat \\ - 20 & Giant Centipede \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - - \begin{description} - \item [*Chicken:] Raven stats with Advantage on checks to wake - up targets instead of mimicry - \end{description} - %% CR1/4 and Lesser Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d20 & Lesser Beast Menu B \\ - [% else %] - \begin{tabular}{c | l} - 1d12 & Lesser Beast Menu B \\ - \hline\hline - [% endif %] - 1 & Giant Crab \\ - 2 & Giant Fire Beetle \\ - 3 & Giant Frog \\ - 4 & Giant Lizard \\ - 5 & Giant Owl \\ - 6 & Giant Poisonous Snake \\ - 7 & Giant Rat \\ - 8 & Giant Weasel \\ - 9 & Giant Wolf Spider \\ - 10 & Goat \\ - 11 & Hawk \\ - 12 & Hyena \\ - 13 & Jackal \\ - 14 & Lemur* \\ - 15 & Lizard \\ - 16 & Mastiff \\ - 17 & Mule \\ - 18 & Newt** \\ - 19 & Octopus \\ - 20 & Octopus, Cascadian Tree*** \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - -\begin{description} - \item [*Lemur] Weasel stats with a common Climb speed instead of a - bite attack - \item [**Newt:] Lizard stats with Amphibious instead of a bite - attack - \item [***Octopus, Cascadian Tree:] Octopus stats with Amphibious - and a 10 ft land speed instead of camouflage -\end{description} - - %% CR1/4 and Lesser Beasts - [% if use_dnd_decorations %] - \begin{DndTable}{c l} - 1d20 & Lesser Beast Menu C \\ - [% else %] - \begin{tabular}{c | l} - 1d12 & Lesser Beast Menu C \\ - \hline\hline - [% endif %] - 1 & Owl \\ - 2 & Panther \\ - 3 & Poisonous Snake \\ - 4 & Pony \\ - 5 & Pteranodon \\ - 6 & Quipper \\ - 7 & Rat \\ - 8 & Raven \\ - 9 & Riding Horse \\ - 10 & Scorpion \\ - 11 & Sea Horse \\ - 12 & Shocker Lizard* \\ - 13 & Spider \\ - 14 & Swarm of Bats \\ - 15 & Swarm of Rats \\ - 16 & Swarm of Ravens \\ - 17 & Turtle** \\ - 18 & Vulture \\ - 19 & Weasel \\ - 20 & Wolf \\ - [% if use_dnd_decorations %] - \end{DndTable} - [% else %] - \end{tabular} - [% endif %] - -\begin{description} -\item [*Shocker Lizard] Lizard stats with Static Electricity ranged - attack of 1d6 Electricity damage Close/Medium. -\item [**Turtle] Lizard stats with 14 natural armor and no climb - speed. -\end{description} - -[% endif %] +[% endfor %] diff --git a/dungeonsheets/latex.py b/dungeonsheets/latex.py index 35ac51e..600e950 100644 --- a/dungeonsheets/latex.py +++ b/dungeonsheets/latex.py @@ -167,7 +167,7 @@ def latex_parts( return parts -def rst_to_latex(rst, top_heading_level=0): +def rst_to_latex(rst, top_heading_level: int=0, format_dice: bool = True, use_dnd_decorations=False): """Basic markup of reST to LaTeX code. The translation between reST headings and LaTeX headings is @@ -184,7 +184,10 @@ def rst_to_latex(rst, top_heading_level=0): top_heading_level : optional The highest level heading that will be added to the LaTeX as described above. - + format_dice + If true, dice strings (e.g. "1d4") will be formatted in + monospace font. + Returns ======= tex : str @@ -196,7 +199,13 @@ def rst_to_latex(rst, top_heading_level=0): tex = "" else: # Mark hit dice in monospace font - rst = dice_re.sub(r"``\1``", rst) + if format_dice: + rst = dice_re.sub(r"``\1``", rst) tex_parts = latex_parts(rst) tex = tex_parts["body"] + # Apply fancy D&D decorations + if use_dnd_decorations: + tex = re.sub(r"p{[0-9.]+\\DUtablewidth}", "l ", tex, flags=re.M) + tex = tex.replace(r"\begin{supertabular}[c]", r"\begin{DndTable}") + tex = tex.replace(r"\end{supertabular}", r"\end{DndTable}") return tex diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index fccf809..639bf49 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -1,5 +1,8 @@ #!/usr/bin/env python +"""Program to take character definitions and build a PDF of the +character sheet.""" + import logging import argparse import os @@ -19,8 +22,8 @@ from dungeonsheets import ( epub, monsters, forms, + random_tables, ) -from dungeonsheets.forms import mod_str from dungeonsheets.content_registry import find_content from dungeonsheets.fill_pdf_template import ( create_character_pdf_template, @@ -30,8 +33,6 @@ from dungeonsheets.fill_pdf_template import ( from dungeonsheets.character import Character from dungeonsheets.content import Creature -"""Program to take character definitions and build a PDF of the -character sheet.""" log = logging.getLogger(__name__) @@ -70,6 +71,7 @@ class CharacterRenderer(): return template.render(character=character, use_dnd_decorations=use_dnd_decorations, ordinals=ORDINALS) + create_character_sheet_content = CharacterRenderer("character_sheet_template.{suffix}") create_subclasses_content = CharacterRenderer("subclasses_template.{suffix}") create_features_content = CharacterRenderer("features_template.{suffix}") @@ -105,13 +107,15 @@ def create_party_summary_content( def create_random_tables_content( - conjure_animals: bool, + tables: Sequence[random_tables.RandomTable], suffix: str, use_dnd_decorations: bool = False, ) -> str: template = jinja_env.get_template(f"random_tables_template.{suffix}") return template.render( - conjure_animals=conjure_animals, use_dnd_decorations=use_dnd_decorations + conjure_animals=True, + tables=tables, + use_dnd_decorations=use_dnd_decorations, ) @@ -268,12 +272,13 @@ def make_gm_sheet( ) ) # Add the random tables - random_tables = [ - s.replace(" ", "_").lower() for s in gm_props.pop("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( - conjure_animals=("conjure_animals" in random_tables), + tables=tables, suffix=content_suffix, use_dnd_decorations=fancy_decorations, ) diff --git a/dungeonsheets/monsters/monsters.py b/dungeonsheets/monsters/monsters.py index c3e86ac..a6f864e 100644 --- a/dungeonsheets/monsters/monsters.py +++ b/dungeonsheets/monsters/monsters.py @@ -59,13 +59,13 @@ def challenge_rating_to_xp(cr): class SpellFactory(ABCMeta): """Meta class to resolve spell strings into the ``spells.Spell``. - + For classes using this metaclass, the *spell* attribute, if present, should be a list of spells that the entity knows. For each entry on that list, anything that is not already a spell class (so probably a string) will be resolved into the corresponding ``spells.Spell`` class. - + """ def __init__(self, name, bases, attrs): for idx, spell in enumerate(self.spells): diff --git a/dungeonsheets/random_tables.py b/dungeonsheets/random_tables.py new file mode 100644 index 0000000..9747485 --- /dev/null +++ b/dungeonsheets/random_tables.py @@ -0,0 +1,326 @@ +from abc import ABCMeta +from typing import Sequence + +from dungeonsheets.content import Content +from dungeonsheets.content_registry import default_content_registry + + +default_content_registry.add_module(__name__) + + +class SubtableFactory(ABCMeta): + """Meta class to append subtables to the docstring of a RandomTable.. + + For classes using this metaclass, the *subtables* attribute, if + present, should be a list of subtables that are to be + included. For each entry on that list, it will first be resolved + into a RandomTable class, if appropriate, then its docstring will + be added to the docstring of the calling class. + + """ + def __init__(self, name, bases, attrs): + # Resolve subtables to RandomTable classes + for idx, subtable in enumerate(self.subtables): + TheTable = self._resolve_mechanic(subtable, SuperClass=RandomTable) + self.subtables[idx] = TheTable + # Append docstrings for subtables + docstring = self.__doc__ if self.__doc__ is not None else "" + for table in self.subtables: + docstring += f"\n\n**{table.name}**\n\n{table.__doc__}\n" + self.__doc__ = docstring + + +class RandomTable(Content, metaclass=SubtableFactory): + """A generic table for rolling treasure, monsters, etc. + + Additional tables can be included by using the *subtables* + attribute. A use case for this is to create a table for rolling + random treasure, which may include subtables for gems, art, magic + items, etc. By including these as subtables, each subtable could + be included by itself if the verbosity of the full *Treasure* + table is not needed. + + Attributes + ========== + subtables + A sequence of other random tables that will be included in this + table. + + """ + name = "Generic Random Table" + subtables: Sequence = [] + + +class ConjureAnimals(RandomTable): + """ + +-----+-----------------------------------------------+ + | 1d4 | Number of Beasts | + +=====+===============================================+ + | 1 | One beast of challenge rating 2 | + +-----+-----------------------------------------------+ + | 2 | Two beasts of challenge rating 1 | + +-----+-----------------------------------------------+ + | 3 | Four beasts of challenge rating 1/2 | + +-----+-----------------------------------------------+ + | 4 | Eight beasts of challenge rating 1/4 or lower | + +-----+-----------------------------------------------+ + + +-------+---------------------------+ + | 1d20 | CR2 Beasts | + +=======+===========================+ + | 1-2 | Allosaurus | + +-------+---------------------------+ + | 3-4 | Giant Boar | + +-------+---------------------------+ + | 5-6 | Giant Constrictor Snake | + +-------+---------------------------+ + | 7-8 | Giant Elk | + +-------+---------------------------+ + | 9-10 | Hunter Shark | + +-------+---------------------------+ + | 11 | Plesiosaurus | + +-------+---------------------------+ + | 12-13 | Polar Bear | + +-------+---------------------------+ + | 14-15 | Rhinoceros | + +-------+---------------------------+ + | 16-17 | Saber-toothed Tiger | + +-------+---------------------------+ + | 18-19 | Swarm of Poisonous Snakes | + +-------+---------------------------+ + | 20 | Roll on CR 1 Beast Table | + +-------+---------------------------+ + + +------+----------------------------+ + | 1d12 | Challenge Rating 1 Beasts | + +======+============================+ + | 1 | Brown Bear | + +------+----------------------------+ + | 2 | Dire Wolf | + +------+----------------------------+ + | 3 | Fire Snake | + +------+----------------------------+ + | 4 | Giant Eagle | + +------+----------------------------+ + | 5 | Giant Hyena | + +------+----------------------------+ + | 6 | Giant Octopus | + +------+----------------------------+ + | 7 | Giant Spider | + +------+----------------------------+ + | 8 | Giant Toad | + +------+----------------------------+ + | 9 | Giant Vulture | + +------+----------------------------+ + | 10 | Lion | + +------+----------------------------+ + | 11 | Tiger | + +------+----------------------------+ + | 12 | Roll on CR 1/2 Beast Table | + +------+----------------------------+ + + +-------+---------------------------------+ + | 1d20 | Challenge Rating 1/2 Beasts | + +=======+=================================+ + | 1-2 | Ape | + +-------+---------------------------------+ + | 3-4 | Black Bear | + +-------+---------------------------------+ + | 5-6 | Crocodile | + +-------+---------------------------------+ + | 7-8 | Giant Goat | + +-------+---------------------------------+ + | 9-10 | Giant Sea Horse | + +-------+---------------------------------+ + | 11-12 | Giant Wasp | + +-------+---------------------------------+ + | 13-14 | Reef Shark | + +-------+---------------------------------+ + | 15-16 | Swarm of Insects (below) | + +-------+---------------------------------+ + | 17-18 | Warhorse | + +-------+---------------------------------+ + | 19 | Worg | + +-------+---------------------------------+ + | 20 | Roll on Lesser Beast Menu Table | + +-------+---------------------------------+ + + +-----+------------------+ + | 1d6 | Swarm of Insects | + +=====+==================+ + | 1 | Ant | + +-----+------------------+ + | 2 | Beatles | + +-----+------------------+ + | 3 | Centipedes | + +-----+------------------+ + | 4 | Locusts | + +-----+------------------+ + | 5 | Spiders | + +-----+------------------+ + | 6 | Wasps | + +-----+------------------+ + + +-----+------------------------------+ + | 1d6 | CR 1/4 and Lesser Beast Menu | + +=====+==============================+ + | 1-2 | Menu A | + +-----+------------------------------+ + | 3-4 | Menu B | + +-----+------------------------------+ + | 5-6 | Menu C | + +-----+------------------------------+ + + +------+---------------------+ + | 1d20 | Lesser Beast Menu A | + +======+=====================+ + | 1 | Axe Beak | + +------+---------------------+ + | 2 | Baboon | + +------+---------------------+ + | 3 | Badger | + +------+---------------------+ + | 4 | Bat | + +------+---------------------+ + | 5 | Blood Hawk | + +------+---------------------+ + | 6 | Boar | + +------+---------------------+ + | 7 | Camel | + +------+---------------------+ + | 8 | Cat | + +------+---------------------+ + | 9 | Chicken¹ | + +------+---------------------+ + | 10 | Constrictor Snake | + +------+---------------------+ + | 11 | Crab | + +------+---------------------+ + | 12 | Deer | + +------+---------------------+ + | 13 | Draft Horse | + +------+---------------------+ + | 14 | Eagle | + +------+---------------------+ + | 15 | Elk | + +------+---------------------+ + | 16 | Flying Snake | + +------+---------------------+ + | 17 | Frog | + +------+---------------------+ + | 18 | Giant Badger | + +------+---------------------+ + | 19 | Giant Bat | + +------+---------------------+ + | 20 | Giant Centipede | + +------+---------------------+ + + ¹Chicken + Raven stats with Advantage on checks to wake up targets instead + of mimicry + + +------+--------------------------+ + | 1d20 | Lesser Beast Menu B | + +======+==========================+ + | 1 | Giant Crab | + +------+--------------------------+ + | 2 | Giant Fire Beetle | + +------+--------------------------+ + | 3 | Giant Frog | + +------+--------------------------+ + | 4 | Giant Lizard | + +------+--------------------------+ + | 5 | Giant Owl | + +------+--------------------------+ + | 6 | Giant Poisonous Snake | + +------+--------------------------+ + | 7 | Giant Rat | + +------+--------------------------+ + | 8 | Giant Weasel | + +------+--------------------------+ + | 9 | Giant Wolf Spider | + +------+--------------------------+ + | 10 | Goat | + +------+--------------------------+ + | 11 | Hawk | + +------+--------------------------+ + | 12 | Hyena | + +------+--------------------------+ + | 13 | Jackal | + +------+--------------------------+ + | 14 | Lemur² | + +------+--------------------------+ + | 15 | Lizard | + +------+--------------------------+ + | 16 | Mastiff | + +------+--------------------------+ + | 17 | Mule | + +------+--------------------------+ + | 18 | Newt³ | + +------+--------------------------+ + | 19 | Octopus | + +------+--------------------------+ + | 20 | Octopus, Cascadian Tree⁴ | + +------+--------------------------+ + + ²Lemur + Weasel stats with a common Climb speed instead of a bite attack + ³Newt + Lizard stats with Amphibious instead of a bite attack + ⁴Octopus, Cascadian Tree: + Octopus stats with Amphibious and a 10 ft land speed instead of + camouflage + + +------+---------------------+ + | 1d20 | Lesser Beast Menu C | + +======+=====================+ + | 1 | Owl | + +------+---------------------+ + | 2 | Panther | + +------+---------------------+ + | 3 | Poisonous Snake | + +------+---------------------+ + | 4 | Pony | + +------+---------------------+ + | 5 | Pteranodon | + +------+---------------------+ + | 6 | Quipper | + +------+---------------------+ + | 7 | Rat | + +------+---------------------+ + | 8 | Raven | + +------+---------------------+ + | 9 | Riding Horse | + +------+---------------------+ + | 10 | Scorpion | + +------+---------------------+ + | 11 | Sea Horse | + +------+---------------------+ + | 12 | Shocker Lizard⁵ | + +------+---------------------+ + | 13 | Spider | + +------+---------------------+ + | 14 | Swarm of Bats | + +------+---------------------+ + | 15 | Swarm of Rats | + +------+---------------------+ + | 16 | Swarm of Ravens | + +------+---------------------+ + | 17 | Turtle⁶ | + +------+---------------------+ + | 18 | Vulture | + +------+---------------------+ + | 19 | Weasel | + +------+---------------------+ + | 20 | Wolf | + +------+---------------------+ + + ⁵Shocker Lizard + Lizard stats with Static Electricity ranged attack of 1d6 + Electricity damage Close/Medium. + ⁶Turtle + Lizard stats with 14 natural armor and no climb speed. + + """ + # https://the-azure-triskele.obsidianportal.com/wikis/conjure-animals-table + name = "Conjure Animals" diff --git a/tests/test_latex.py b/tests/test_latex.py index a7efbaf..a00db78 100644 --- a/tests/test_latex.py +++ b/tests/test_latex.py @@ -103,6 +103,9 @@ class MarkdownTestCase(unittest.TestCase): self.assertNotIn("endfoot", tex) self.assertNotIn("endhead", tex) self.assertNotIn("endfirsthead", tex) + # Check that fancy decorations uses the DndTable environment + tex = latex.rst_to_latex(table_rst, use_dnd_decorations=True) + self.assertIn(r"\begin{DndTable}{l l l }", tex) def test_rst_all_spells(self): for spell in spells.all_spells(): diff --git a/tests/test_random_tables.py b/tests/test_random_tables.py new file mode 100644 index 0000000..78429ed --- /dev/null +++ b/tests/test_random_tables.py @@ -0,0 +1,26 @@ +import unittest + +from dungeonsheets import random_tables + + +class ChildTable(random_tables.RandomTable): + """I'm a table too, but where is everyone else?""" + name = "Child Table" + + +class ParentTable(random_tables.RandomTable): + """Hello, world. I'm a table.""" + name = "Parent Table" + subtables = [ChildTable] + + +class RandomTableTests(unittest.TestCase): + def test_docstring(self): + self.assertIn("Hello, world", ParentTable.__doc__) + parent_table = ParentTable() + self.assertIn("Hello, world", parent_table.__doc__) + + def test_subtables(self): + # Check that docstrings are combined + # parent_table = ParentTable() + self.assertIn("**Child Table**", ParentTable.__doc__)