Added more flexible way of specifying random tables in the GM sheet.

This commit is contained in:
Mark Wolfman
2021-08-11 21:56:03 -05:00
parent 37a159f39f
commit e7ccb6f9f3
9 changed files with 493 additions and 366 deletions
+81 -80
View File
@@ -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
+25 -1
View File
@@ -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]{%
+5 -272
View File
@@ -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 %]
+12 -3
View File
@@ -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
+13 -8
View File
@@ -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,
)
+2 -2
View File
@@ -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):
+326
View File
@@ -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"
+3
View File
@@ -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():
+26
View File
@@ -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__)