Merge branch 'canismarko:master' into master

This commit is contained in:
bw-mutley
2021-09-02 09:17:16 -03:00
committed by GitHub
20 changed files with 1897 additions and 916 deletions
+2
View File
@@ -55,6 +55,8 @@ the following random tables are available.
- **"conjure animals"** - A list of options to choose from when a
player casts the *Conjure Animals* spell.
- **"treasure"** - Tables for rolling treasure dropped by individuals
or hoards.
.. code-block:: python
:caption: Example:
+2 -4
View File
@@ -77,6 +77,7 @@ class Armor:
cost = "0 gp"
base_armor_class = 10
dexterity_mod_max = None
dexterity_applied = True
strength_required = None
stealth_disadvantage = False
weight = 0 # In lbs
@@ -112,6 +113,7 @@ class MediumArmor(Armor):
class HeavyArmor(Armor):
name = "Heavy Armor"
dexterity_applied = False
class PaddedArmor(LightArmor):
@@ -182,7 +184,6 @@ class RingMail(HeavyArmor):
name = "Ring Mail"
cost = "30 gp"
base_armor_class = 14
dexterity_mod_max = 0
stealth_disadvantage = True
weight = 40
@@ -191,7 +192,6 @@ class ChainMail(HeavyArmor):
name = "Chain Mail"
cost = "75 gp"
base_armor_class = 16
dexterity_mod_max = 0
strength_required = 13
stealth_disadvantage = True
weight = 55
@@ -201,7 +201,6 @@ class SplintArmor(HeavyArmor):
name = "Splint Armor"
cost = "200 gp"
base_armor_class = 17
dexterity_mod_max = 0
strength_required = 15
stealth_disadvantage = True
weight = 60
@@ -211,7 +210,6 @@ class PlateMail(HeavyArmor):
name = "Plate Mail"
cost = "1,500 gp"
base_armor_class = 18
dexterity_mod_max = 0
strength_required = 15
stealth_disadvantage = True
weight = 65
+3
View File
@@ -1,3 +1,5 @@
import warnings
from collections import defaultdict
from dungeonsheets.features import Feature, FeatureSelector
@@ -67,6 +69,7 @@ class CharClass:
for sc in self.subclasses_available:
if subclass_str.lower() in sc.name.lower():
return sc(owner=self.owner)
warnings.warn(f"Could not find subclass {subclass_str}.")
return None
def apply_subclass(self, feature_choices=[]):
+34
View File
@@ -139,6 +139,39 @@ class Scout(SubClass):
features_by_level[17] = [features.SuddenStrike]
class Soulknife(SubClass):
"""Most assassins strike with physical weapons, and many burglars and
spies use thieves' tools to infiltrate secure locations. In
contrast, a Soulknife strikes and infiltrates with the mind,
cutting through barriers both physical and psychic. These rogues
discover psionic power within themselves and channel it to do
their roguish work. They find easy employment as members of
thieves' guilds, though they are often mistrusted by rogues who
are leery of anyone using strange mind powers to conduct their
business. Most governments would also be happy to employ a
Soulknife as a spy.
Amid the trees of ancient forests on the Material Plane and in the
Feywild, some wood elves walk the path of the Soulknife, serving
as silent, lethal guardians of their woods. In the endless war
among the gith, a githzerai is encouraged to become a Soulknife
when stealth is required against the githyanki foe.
As a Soulknife, your psionic abilities might have haunted you
since you were a child, only revealing their full potential as you
experienced the stress of adventure. Or you might have sought out
a reclusive order of psychic adepts and spent years learning how
to manifest your power.
"""
name = "Soulknife"
features_by_level = defaultdict(list)
features_by_level[3] = [features.PsionicPower, features.PsychicBlades]
features_by_level[9] = [features.SoulBlades]
features_by_level[13] = [features.PsychicVeil]
features_by_level[17] = [features.RendMind]
class Swashbuckler(SubClass):
"""You focus your training on the art of the blade, relying on speed,
elegance, and charm in equal parts. While some warriors are brutes clad in
@@ -216,6 +249,7 @@ class Rogue(CharClass):
Inquisitive,
Mastermind,
Scout,
Soulknife,
Swashbuckler,
)
+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
+6 -2
View File
@@ -251,7 +251,7 @@ def html_parts(
return parts
def rst_to_html(rst, top_heading_level=0):
def rst_to_html(rst, top_heading_level: int = 0, format_dice: bool = True):
"""Basic markup of reST to HTML code.
The translation between reST headings and LaTeX headings is
@@ -268,6 +268,9 @@ def rst_to_html(rst, top_heading_level=0):
top_heading_level : optional
The highest level heading that will be added to the HTML as
described above.
format_dice
If true, dice strings (e.g. "1d4") will be formatted in
monospace font.
Returns
=======
@@ -280,7 +283,8 @@ def rst_to_html(rst, top_heading_level=0):
html = ""
else:
# Mark hit dice in monospace font
rst = dice_re.sub(r"``\1``", rst)
if format_dice:
rst = dice_re.sub(r"``\1``", rst)
_html_parts = html_parts(rst)
html = _html_parts["body"]
return html
+131
View File
@@ -610,3 +610,134 @@ class MasterDuelist(Feature):
name = "Master Duelist"
source = "Rogue (Swashbuckler)"
# Soulknife
class PsionicPower(Feature):
"""You harbor a wellspring of psionic energy within yourself. This·
energy is represented by your Psionic Energy dice, which are each
a ``d6``. You have a number of these dice equal to twice your
proficiency bonus, and they fuel various psionic powers you have,
which are detailed below.
Some of your powers expend the Psionic Energy die they use, as
specified in a power's description, and you can't use a power if
it requires you to use a die when your dice are all expended. You
regain all your expended Psionic Energy dice when you finish a
long rest. In addition, as a bonus action, you can regain one
expended Psionic Energy die, but you can't do so again until you
finish a short or long rest.
When you reach certain levels in this class, the size of your
Psionic Energy dice increases: at 5th level (``d8``), 11th level
(``dlO``), and 17th level (``d12``).
The powers below use your Psionic Energy dice.
Psi-Bolstered Knack
When your nonpsionic training fails you, your psionic power can
help: if you fail an ability check using a skill or tool with
which you have proficiency, you can roll one Psionic Energy die
and add the number rolled to the check, potentially turning
failure into success. You expend the die only if the roll
succeeds.
Psychic Whispers
You can establish telepathic communication between yourself and
others-perfect for quiet infiltration. As an action, choose one
or more creatures you can see, up to a number of creatures equal
to your proficiency bonus, and then roll one Psionic Energy
die. For a number of hours equal to the number rolled, the
chosen creatures can speak telepathically with you, and you can
speak telepathically with them. To send or receive a message (no
action required), you and the other creature must be within 1
mile of each other. A creature can't use this telepathy if it
can't speak any languages, and a creature can end the telepathic
connection at any time (no action required). You and the
creature don't need to speak a common language to understand
each other.
The first time you use this power after each long rest, you
don't expend the Psionic Energy die. All other times you use the
power, you expend the die.
"""
name = "Psionic Power"
source = "Rogue (Soulknife)"
class PsychicBlades(Feature):
"""You can manifest your psionic power as shimmering blades of psychic
energy. Whenever you take the Attack action, you can manifest a
psychic blade from your free hand and make the attack with that
blade. This magic blade is a simple melee weapon with the finesse
and thrown properties. It has a normal range of 60 feet and no
long range, and on a hit, it deals psychic damage equal to 1d6
plus the ability modifier you used for the attack roll. The blade
vanishes immediately after it hits or misses its target, and it
leaves no mark on its target if it deals damage.
After you attack with the blade, you can make a melee or ranged
weapon attack with a second psychic blade as a bonus action on the
same turn, provided your other hand is free to create it. The
damage die of this bonus attack is 1d4, instead of 1d6.
"""
name = "Psychic Blades"
source = "Rogue (Soulknife)"
class SoulBlades(Feature):
"""Your Psychic Blades are now an expression of your psi-suffused
soul, giving you these powers that use your Psionic Energy dice:
Homing Strikes
If you make an attack roll with your Psychic Blades and miss the
target, you can roll one Psionic Energy die and add the number
rolled to the attack roll. If this causes the attack to hit, you
expend the Psionic Energy die.
Psychic Teleportation
As a bonus action, you manifest one of your Psychic Blades,
expend one Psionic Energy die and roll it, and throw the blade
at an unoccupied space you can see, up to a number of feet away
equal to 10 times the number rolled. You then teleport to that
space, and the blade vanishes.
"""
name = "Soul Blades"
source = "Rogue (Soulknife)"
class PsychicVeil(Feature):
"""You can weave a veil of psychic static to mask yourself. As an
action, you can magically become invisible, along with anything
you are wearing or carrying, for 1 hour or until you dismiss this
effect (no action required). This invisibility ends early
immediately after you deal damage to a creature or you force a
creature to make a saving throw.
Once you use this feature, you can't do so again until you finish
a long rest, unless you expend a Psionic Energy die to use this
feature again.
"""
name = "Psychic Veil"
source = "Rogue (Soulknife)"
class RendMind(Feature):
"""You can sweep your Psychic Blades directly through a creature's
mind. When you use your Psychic Blades to deal Sneak Attack damage
to a creature, you can force that target to make a Wisdom saving
throw (DC equal to 8 + your proficiency bonus + your Dexterity
modifier). If the save fails, the target is stunned for 1
minute. The stunned target can repeat the saving throw at the end
of each of its turns, ending the effect on itself on a success.
Once you use this feature, you can't do so again until you finish
a long rest, unless you expend three Psionic Energy dice to use it
again.
"""
name = "Rend Mind"
source = "Rogue (Soulknife)"
+5 -5
View File
@@ -6,7 +6,7 @@ h1, h2, h3, h4, h5, h6 {
}
body {
/* background: #f4ecdb; */
background-image: url("../images/paper.jpg");
/* background-image: url("../images/paper.jpg"); */
background-size: 100%;
}
p {
@@ -156,14 +156,14 @@ table p {
}
th {
padding-left: 5px;
padding-right: 5px;
padding-left: 10px;
padding-right: 10px;
text-align: center;
}
td {
padding-left: 5px;
padding-right: 5px;
padding-left: 10px;
padding-right: 10px;
text-align: center;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 67 KiB

+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]{%
+4 -535
View File
@@ -1,539 +1,8 @@
<h1 id="gm-random-tables">Random Tables</h1>
[% if conjure_animals %]
[% for table in tables %]
<h2 id="gm-random-tables-[[ table.name | to_heading_id ]]">[[ table.name ]]</h2>
<!-- https://the-azure-triskele.obsidianportal.com/wikis/conjure-animals-table -->
<h2 id="gm-random-tables-conjure-animals">Conjure Animals</h2>
[[ table.__doc__ | rst_to_html(format_dice=False) ]]
<!-- Which category of beasts to summon -->
<table>
<tr>
<th>1d4</th>
<th>Number of Beasts</th>
</tr>
<tr>
<td>1</td>
<td>One beast of challenge rating 2</td>
</tr>
<tr>
<td>2</td>
<td>Two beasts of challenge rating 1</td>
</tr>
<tr>
<td>3</td>
<td>Four beasts of challenge rating 1/2</td>
</tr>
<tr>
<td>4</td>
<td>Eight beasts of challenge rating 1/4 or lower</td>
</tr>
</table>
<!-- CR2 Beasts -->
<table>
<tr>
<th>1d20</th>
<th>CR2 Beasts</th>
</tr>
<tr>
<td>1-2</td>
<td>Allosaurus</td>
</tr>
<tr>
<td>3-4</td>
<td>Giant Boar</td>
</tr>
<tr>
<td>5-6</td>
<td>Giant Constrictor Snake</td>
</tr>
<tr>
<td>7-8</td>
<td>Giant Elk</td>
</tr>
<tr>
<td>9-10</td>
<td>Hunter Shark</td>
</tr>
<tr>
<td>11</td>
<td>Plesiosaurus</td>
</tr>
<tr>
<td>12-13</td>
<td>Polar Bear</td>
</tr>
<tr>
<td>14-15</td>
<td>Rhinoceros</td>
</tr>
<tr>
<td>16-17</td>
<td>Saber-toothed Tiger</td>
</tr>
<tr>
<td>18-19</td>
<td>Swarm of Poisonous Snakes</td>
</tr>
<tr>
<td>20</td>
<td>Roll on CR 1 Beast Table</td>
</tr>
</table>
<!-- CR1 Beasts -->
<table>
<tr>
<th>1d12</th>
<th>Challenge Rating 1 Beasts</th>
</tr>
<tr>
<td>1</td>
<td>Brown Bear</td>
</tr>
<tr>
<td>2</td>
<td>Dire Wolf</td>
</tr>
<tr>
<td>3</td>
<td>Fire Snake</td>
</tr>
<tr>
<td>4</td>
<td>Giant Eagle</td>
</tr>
<tr>
<td>5</td>
<td>Giant Hyena</td>
</tr>
<tr>
<td>6</td>
<td>Giant Octopus</td>
</tr>
<tr>
<td>7</td>
<td>Giant Spider</td>
</tr>
<tr>
<td>8</td>
<td>Giant Toad</td>
</tr>
<tr>
<td>9</td>
<td>Giant Vulture</td>
</tr>
<tr>
<td>10</td>
<td>Lion</td>
</tr>
<tr>
<td>11</td>
<td>Tiger</td>
</tr>
<tr>
<td>12</td>
<td>Roll on CR ½ Beast Table</td>
</tr>
</table>
<table>
<tr>
<th>1d20</th>
<th>Challenge Rating ½ Beasts</th>
</tr>
<tr>
<td>1-2</td>
<td>Ape</td>
</tr>
<tr>
<td>3-4</td>
<td>Black Bear</td>
</tr>
<tr>
<td>5-6</td>
<td>Crocodile</td>
</tr>
<tr>
<td>7-8</td>
<td>Giant Goat</td>
</tr>
<tr>
<td>9-10</td>
<td>Giant Sea Horse</td>
</tr>
<tr>
<td>11-12</td>
<td>Giant Wasp</td>
</tr>
<tr>
<td>13-14</td>
<td>Reef Shark</td>
</tr>
<tr>
<td>15-16</td>
<td>Swarm of Insects (below)</td>
</tr>
<tr>
<td>17-18</td>
<td>Warhorse</td>
</tr>
<tr>
<td>19</td>
<td>Worg</td>
</tr>
<tr>
<td>20</td>
<td>Roll on Lesser Beast Menu Table</td>
</tr>
</table>
<!-- Swarm of insects (mostly for flavor) -->
<table>
<tr>
<th>1d6</th>
<th>Swarm of Insects</th>
</tr>
<tr>
<td>1</td>
<td>Ant</td>
</tr>
<tr>
<td>2</td>
<td>Beatles</td>
</tr>
<tr>
<td>3</td>
<td>Centipedes</td>
</tr>
<tr>
<td>4</td>
<td>Locusts</td>
</tr>
<tr>
<td>5</td>
<td>Spiders</td>
</tr>
<tr>
<td>6</td>
<td>Wasps</td>
</tr>
</table>
<!-- Challenge Rating 1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d6</th>
<th>CR ¼ and Lesser Beast Menu</th>
</tr>
<tr>
<td>1-2</td>
<td>Menu A</td>
</tr>
<tr>
<td>3-4</td>
<td>Menu B</td>
</tr>
<tr>
<td>5-6</td>
<td>Menu C</td>
</tr>
</table>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu A</th>
</tr>
<tr>
<td>1</td>
<td>Axe Beak</td>
</tr>
<tr>
<td>2</td>
<td>Baboon</td>
</tr>
<tr>
<td>3</td>
<td>Badger</td>
</tr>
<tr>
<td>4</td>
<td>Bat</td>
</tr>
<tr>
<td>5</td>
<td>Blood Hawk</td>
</tr>
<tr>
<td>6</td>
<td>Boar</td>
</tr>
<tr>
<td>7</td>
<td>Camel</td>
</tr>
<tr>
<td>8</td>
<td>Cat</td>
</tr>
<tr>
<td>9</td>
<td>Chicken*</td>
</tr>
<tr>
<td>10</td>
<td>Constrictor Snake</td>
</tr>
<tr>
<td>11</td>
<td>Crab</td>
</tr>
<tr>
<td>12</td>
<td>Deer</td>
</tr>
<tr>
<td>13</td>
<td>Draft Horse</td>
</tr>
<tr>
<td>14</td>
<td>Eagle</td>
</tr>
<tr>
<td>15</td>
<td>Elk</td>
</tr>
<tr>
<td>16</td>
<td>Flying Snake</td>
</tr>
<tr>
<td>17</td>
<td>Frog</td>
</tr>
<tr>
<td>18</td>
<td>Giant Badger</td>
</tr>
<tr>
<td>19</td>
<td>Giant Bat</td>
</tr>
<tr>
<td>20</td>
<td>Giant Centipede</td>
</tr>
</table>
<dl class="random-table-definitions">
<dt>*Chicken</dt>
<dd>Raven stats with Advantage on checks to wake
up targets instead of mimicry</dd>
</dl>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu B</th>
</tr>
<tr>
<td>1</td>
<td>Giant Crab </td>
</tr>
<tr>
<td>2</td>
<td>Giant Fire Beetle </td>
</tr>
<tr>
<td>3</td>
<td>Giant Frog</td>
</tr>
<tr>
<td>4</td>
<td>Giant Lizard</td>
</tr>
<tr>
<td>5</td>
<td>Giant Owl</td>
</tr>
<tr>
<td>6</td>
<td>Giant Poisonous Snake</td>
</tr>
<tr>
<td>7</td>
<td>Giant Rat</td>
</tr>
<tr>
<td>8</td>
<td>Giant Weasel</td>
</tr>
<tr>
<td>9</td>
<td>Giant Wolf Spider</td>
</tr>
<tr>
<td>10</td>
<td>Goat</td>
</tr>
<tr>
<td>11</td>
<td>Hawk</td>
</tr>
<tr>
<td>12</td>
<td>Hyena</td>
</tr>
<tr>
<td>13</td>
<td>Jackal</td>
</tr>
<tr>
<td>14</td>
<td>Lemur*</td>
</tr>
<tr>
<td>15</td>
<td>Lizard</td>
</tr>
<tr>
<td>16</td>
<td>Mastiff</td>
</tr>
<tr>
<td>17</td>
<td>Mule</td>
</tr>
<tr>
<td>18</td>
<td>Newt**</td>
</tr>
<tr>
<td>19</td>
<td>Octopus</td>
</tr>
<tr>
<td>20</td>
<td>Octopus, Cascadian Tree***</td>
</tr>
</table>
<dl class="random-table-definitions">
<dt>*Lemur</dt>
<dd>Weasel stats with a common Climb speed instead of a
bite attack</dd>
<dt>**Newt</dt>
<dd>Lizard stats with Amphibious instead of a bite
attack</dd>
<dt>***Octopus, Cascadian Tree</dt>
<dd>Octopus stats with Amphibious
and a 10 ft land speed instead of camouflage</dd>
</dl>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu C</th>
</tr>
<tr>
<td>1</td>
<td>Owl</td>
</tr>
<tr>
<td>2</td>
<td>Panther</td>
</tr>
<tr>
<td>3</td>
<td>Poisonous Snake</td>
</tr>
<tr>
<td>4</td>
<td>Pony</td>
</tr>
<tr>
<td>5</td>
<td>Pteranodon</td>
</tr>
<tr>
<td>6</td>
<td>Quipper</td>
</tr>
<tr>
<td>7</td>
<td>Rat</td>
</tr>
<tr>
<td>8</td>
<td>Raven</td>
</tr>
<tr>
<td>9</td>
<td>Riding Horse</td>
</tr>
<tr>
<td>10</td>
<td>Scorpion</td>
</tr>
<tr>
<td>11</td>
<td>Sea Horse</td>
</tr>
<tr>
<td>12</td>
<td>Shocker Lizard*</td>
</tr>
<tr>
<td>13</td>
<td>Spider</td>
</tr>
<tr>
<td>14</td>
<td>Swarm of Bats</td>
</tr>
<tr>
<td>15</td>
<td>Swarm of Rats</td>
</tr>
<tr>
<td>16</td>
<td>Swarm of Ravens</td>
</tr>
<tr>
<td>17</td>
<td>Turtle**</td>
</tr>
<tr>
<td>18</td>
<td>Vulture</td>
</tr>
<tr>
<td>19</td>
<td>Weasel</td>
</tr>
<tr>
<td>20</td>
<td>Wolf</td>
</tr>
</table>
<dl class="random-table-definitions">
<dt>*Shocker Lizard</dt>
<dd>Lizard stats with Static Electricity ranged attack of 1d6
Electricity damage Close/Medium.</dd>
<dt>**Turtle</dt>
<dd>Lizard stats with 14 natural armor and no climb speed.</dd>
</dl>
[% endif %]
[% endfor %]
+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
@@ -168,7 +168,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
@@ -185,7 +185,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
@@ -197,9 +200,15 @@ 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
def rst_to_boxlatex(rst):
+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__)
@@ -71,6 +72,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}")
@@ -106,13 +108,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,
)
@@ -271,12 +275,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):
+3
View File
@@ -136,6 +136,9 @@ class DarkElf(_Elf):
spells_known = (spells.DancingLights,)
Drow = DarkElf
# Halflings
class _Halfling(Race):
name = "Halfling"
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -195,10 +195,11 @@ class ArmorClass:
armor = actor.armor or NoArmor()
ac = armor.base_armor_class
# calculate and apply modifiers
if armor.dexterity_mod_max is None:
ac += actor.dexterity.modifier
else:
ac += min(actor.dexterity.modifier, armor.dexterity_mod_max)
if armor.dexterity_applied:
if armor.dexterity_mod_max is None:
ac += actor.dexterity.modifier
else:
ac += min(actor.dexterity.modifier, armor.dexterity_mod_max)
if actor.has_feature(NaturalArmor):
ac = max(ac, 13 + actor.dexterity.modifier)
shield = actor.shield or NoShield()
+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__)