mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 13:15:53 +02:00
Added feature enhancements for Druid's
- Circle now properly reflects the druid's available wild_shapes - Unavaiable wild_shapes are not listed on the sheet but ghosted. - ``spells`` is now longer relevant, only use ``spells_prepared`` in the character file.
This commit is contained in:
@@ -151,6 +151,14 @@ correspond to spells described in the `player's handbook`_.
|
|||||||
spells_prepared = ('blindness deafness', 'false life', 'mage armor',
|
spells_prepared = ('blindness deafness', 'false life', 'mage armor',
|
||||||
'ray of sickness', 'shield', 'sleep',)
|
'ray of sickness', 'shield', 'sleep',)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Some character classes have modified spellcasting mechanics that
|
||||||
|
affects how these entries are intepreted.
|
||||||
|
|
||||||
|
- `Druid`_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Personality and Backstory
|
Personality and Backstory
|
||||||
=========================
|
=========================
|
||||||
@@ -210,6 +218,47 @@ source file more readable, but are not required.
|
|||||||
priests there for assistance that won’t endanger them.""")
|
priests there for assistance that won’t endanger them.""")
|
||||||
|
|
||||||
|
|
||||||
|
Class-Specific Features
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Druid
|
||||||
|
-----
|
||||||
|
|
||||||
|
At level 2, druids choose a **circle**. This choice can affect
|
||||||
|
available wild_forms, and spellcasting abilities. The ``circle`` entry
|
||||||
|
should be set appropriately.
|
||||||
|
|
||||||
|
Druid's can transform into **wild shapes**, allowing them to adopt
|
||||||
|
some of the abilities of their new form. To aid in keeping track on
|
||||||
|
the possible shapes, Druids can have a listing for
|
||||||
|
``wild_shapes``. This list should contain names of beasts listed in
|
||||||
|
:py:mod:`dungeonsheets.monsters`, or instances of a subclass of
|
||||||
|
:py:class:`dungeonsheets.monsters.Monster`. If given, an extra *monster
|
||||||
|
sheet* will be produced as part of the PDF. Beasts familiar to the
|
||||||
|
druid but not yet accessible should still be listed to aid in record
|
||||||
|
keeping; they will be greyed-out on the sheet.
|
||||||
|
|
||||||
|
Additionally, druids don't learn spells, instead **druids can prepare
|
||||||
|
any spell available** provided it meets their level requirements. As
|
||||||
|
such, the listing for ``spells`` is not needed and **all prepared
|
||||||
|
spells and known cantrips** should be listed in the
|
||||||
|
``spells_prepared`` entry.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# We're a moon druid, why not
|
||||||
|
circle = 'Moon'
|
||||||
|
|
||||||
|
# Spells are empty because we don't learn any spells
|
||||||
|
spells = []
|
||||||
|
# This one has all prepared spells and cantrips
|
||||||
|
spells_prepared = ['druidcraft', 'cure wounds']
|
||||||
|
|
||||||
|
# List of all the known wild shapes
|
||||||
|
wild_shapes = ["wolf", "crocodile", 'ape', 'ankylosaurus']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
|
.. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
|
||||||
|
|
||||||
.. _issue: https://github.com/canismarko/dungeon-sheets/issues
|
.. _issue: https://github.com/canismarko/dungeon-sheets/issues
|
||||||
|
|||||||
+96
-78
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
|
import math
|
||||||
|
|
||||||
from .stats import Ability, Skill, findattr
|
from .stats import Ability, Skill, findattr
|
||||||
from .dice import read_dice_str
|
from .dice import read_dice_str
|
||||||
@@ -86,8 +87,6 @@ class Character():
|
|||||||
spellcasting_ability = None
|
spellcasting_ability = None
|
||||||
spells = tuple()
|
spells = tuple()
|
||||||
spells_prepared = tuple()
|
spells_prepared = tuple()
|
||||||
# Druid wilf shape transofmration options
|
|
||||||
_wild_shapes = ()
|
|
||||||
|
|
||||||
def __init__(self, **attrs):
|
def __init__(self, **attrs):
|
||||||
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
||||||
@@ -100,82 +99,6 @@ class Character():
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.class_name}: {self.name}>"
|
return f"<{self.class_name}: {self.name}>"
|
||||||
|
|
||||||
@property
|
|
||||||
def all_wild_shapes(self):
|
|
||||||
"""Return all wild shapes, regardless of validity."""
|
|
||||||
return self._wild_shapes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wild_shapes(self):
|
|
||||||
"""Return a list of valid wild shapes for this Druid."""
|
|
||||||
valid_shapes = []
|
|
||||||
for shape in self._wild_shapes:
|
|
||||||
# Check if shape can be transformed into
|
|
||||||
if self.can_assume_shape(shape):
|
|
||||||
valid_shapes.append(shape)
|
|
||||||
return valid_shapes
|
|
||||||
|
|
||||||
@wild_shapes.setter
|
|
||||||
def wild_shapes(self, new_shapes):
|
|
||||||
actual_shapes = []
|
|
||||||
# Retrieve the actual monster classes if possible
|
|
||||||
for shape in new_shapes:
|
|
||||||
if isinstance(shape, monsters.Monster):
|
|
||||||
# Already a monster shape so just add it as is
|
|
||||||
new_shape = shape
|
|
||||||
else:
|
|
||||||
# Not already a monster so see if we can find one
|
|
||||||
try:
|
|
||||||
NewMonster = findattr(monsters, shape)
|
|
||||||
new_shape = NewMonster()
|
|
||||||
except AttributeError:
|
|
||||||
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``'
|
|
||||||
raise exceptions.MonsterError(msg)
|
|
||||||
actual_shapes.append(new_shape)
|
|
||||||
# Save the updated list for later
|
|
||||||
self._wild_shapes = actual_shapes
|
|
||||||
|
|
||||||
def can_assume_shape(self, shape: monsters.Monster)-> bool:
|
|
||||||
"""Determine if a given shape meets the requirements for transforming.
|
|
||||||
|
|
||||||
See Pg 66 of player's handbook.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
==========
|
|
||||||
shape
|
|
||||||
A monster that the Druid wishes to transform into.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
=======
|
|
||||||
can_assume
|
|
||||||
True if the monster meets the C/R, swim and flying speed
|
|
||||||
restrictions.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Determine acceptable states based on druid level
|
|
||||||
if self.level < 2:
|
|
||||||
max_cr = -1
|
|
||||||
max_swim = 0
|
|
||||||
max_fly = 0
|
|
||||||
elif self.level < 4:
|
|
||||||
max_cr = 1/4
|
|
||||||
max_swim = 0
|
|
||||||
max_fly = 0
|
|
||||||
elif self.level < 8:
|
|
||||||
max_cr = 1/2
|
|
||||||
max_swim = None
|
|
||||||
max_fly = 0
|
|
||||||
else:
|
|
||||||
max_cr = None
|
|
||||||
max_swim = None
|
|
||||||
max_fly = None
|
|
||||||
# Check if the beast shape can be assumed
|
|
||||||
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr)
|
|
||||||
valid_swim = (max_swim is None or shape.swim_speed <= max_swim)
|
|
||||||
valid_fly = (max_fly is None or shape.fly_speed <= max_fly)
|
|
||||||
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly
|
|
||||||
return can_assume
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed(self):
|
def speed(self):
|
||||||
return getattr(self.race, 'speed', 30)
|
return getattr(self.race, 'speed', 30)
|
||||||
@@ -424,6 +347,8 @@ class Cleric(Character):
|
|||||||
|
|
||||||
class Druid(Character):
|
class Druid(Character):
|
||||||
class_name = 'Druid'
|
class_name = 'Druid'
|
||||||
|
circle = "" # Moon, land
|
||||||
|
_wild_shapes = ()
|
||||||
hit_dice_faces = 8
|
hit_dice_faces = 8
|
||||||
saving_throw_proficiencies = ('intelligence', 'wisdom')
|
saving_throw_proficiencies = ('intelligence', 'wisdom')
|
||||||
spellcasting_ability = 'wisdom'
|
spellcasting_ability = 'wisdom'
|
||||||
@@ -460,6 +385,99 @@ class Druid(Character):
|
|||||||
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
|
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
|
||||||
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
|
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_wild_shapes(self):
|
||||||
|
"""Return all wild shapes, regardless of validity."""
|
||||||
|
return self._wild_shapes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wild_shapes(self):
|
||||||
|
"""Return a list of valid wild shapes for this Druid."""
|
||||||
|
valid_shapes = []
|
||||||
|
for shape in self._wild_shapes:
|
||||||
|
# Check if shape can be transformed into
|
||||||
|
if self.can_assume_shape(shape):
|
||||||
|
valid_shapes.append(shape)
|
||||||
|
return valid_shapes
|
||||||
|
|
||||||
|
@wild_shapes.setter
|
||||||
|
def wild_shapes(self, new_shapes):
|
||||||
|
actual_shapes = []
|
||||||
|
# Retrieve the actual monster classes if possible
|
||||||
|
for shape in new_shapes:
|
||||||
|
if isinstance(shape, monsters.Monster):
|
||||||
|
# Already a monster shape so just add it as is
|
||||||
|
new_shape = shape
|
||||||
|
else:
|
||||||
|
# Not already a monster so see if we can find one
|
||||||
|
try:
|
||||||
|
NewMonster = findattr(monsters, shape)
|
||||||
|
new_shape = NewMonster()
|
||||||
|
except AttributeError:
|
||||||
|
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``'
|
||||||
|
raise exceptions.MonsterError(msg)
|
||||||
|
actual_shapes.append(new_shape)
|
||||||
|
# Save the updated list for later
|
||||||
|
self._wild_shapes = actual_shapes
|
||||||
|
|
||||||
|
def can_assume_shape(self, shape: monsters.Monster)-> bool:
|
||||||
|
"""Determine if a given shape meets the requirements for transforming.
|
||||||
|
|
||||||
|
See Pg 66 of player's handbook.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
==========
|
||||||
|
shape
|
||||||
|
A monster that the Druid wishes to transform into.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
=======
|
||||||
|
can_assume
|
||||||
|
True if the monster meets the C/R, swim and flying speed
|
||||||
|
restrictions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Determine acceptable states based on druid level
|
||||||
|
if self.level < 2:
|
||||||
|
max_cr = -1
|
||||||
|
max_swim = 0
|
||||||
|
max_fly = 0
|
||||||
|
elif self.level < 4:
|
||||||
|
max_cr = 1/4
|
||||||
|
max_swim = 0
|
||||||
|
max_fly = 0
|
||||||
|
elif self.level < 8:
|
||||||
|
max_cr = 1/2
|
||||||
|
max_swim = None
|
||||||
|
max_fly = 0
|
||||||
|
else:
|
||||||
|
max_cr = 1
|
||||||
|
max_swim = None
|
||||||
|
max_fly = None
|
||||||
|
# Make adjustments for moon cirlce druids
|
||||||
|
if self.circle.lower() == "moon":
|
||||||
|
if 2 <= self.level < 6:
|
||||||
|
max_cr = 1
|
||||||
|
elif self.level >= 6:
|
||||||
|
max_cr = math.floor(self.level / 3)
|
||||||
|
# Check if the beast shape can be assumed
|
||||||
|
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr)
|
||||||
|
valid_swim = (max_swim is None or shape.swim_speed <= max_swim)
|
||||||
|
valid_fly = (max_fly is None or shape.fly_speed <= max_fly)
|
||||||
|
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly
|
||||||
|
return can_assume
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spells(self):
|
||||||
|
return tuple(S() for S in self.spells_prepared)
|
||||||
|
|
||||||
|
@spells.setter
|
||||||
|
def spells(self, val):
|
||||||
|
if len(val) > 0:
|
||||||
|
warnings.warn("Druids cannot learn spells, "
|
||||||
|
"use ``spells_prepared`` instead.",
|
||||||
|
RuntimeWarning)
|
||||||
|
|
||||||
|
|
||||||
class Fighter(Character):
|
class Fighter(Character):
|
||||||
|
|||||||
@@ -26,9 +26,17 @@
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[% for shape in character.wild_shapes|sort(attribute='challenge_rating') %]
|
[% for shape in character.all_wild_shapes|sort(attribute='challenge_rating') %]
|
||||||
|
[% if not character.can_assume_shape(shape) %]
|
||||||
|
{\color{mygrey}
|
||||||
|
[% else %]
|
||||||
|
{
|
||||||
|
[% endif %]
|
||||||
|
|
||||||
\section*{[[ shape.name ]]}
|
\section*{[[ shape.name ]]}
|
||||||
|
[% if shape.description %]
|
||||||
\subsection*{[[ shape.description ]]}
|
\subsection*{[[ shape.description ]]}
|
||||||
|
[% endif %]
|
||||||
|
|
||||||
\begin{tabular}{c | c | c}
|
\begin{tabular}{c | c | c}
|
||||||
Armor Class & Hit Points & Speed \\
|
Armor Class & Hit Points & Speed \\
|
||||||
@@ -57,15 +65,17 @@
|
|||||||
|
|
||||||
\vspace{0.2cm}
|
\vspace{0.2cm}
|
||||||
|
|
||||||
\begin{tabular}{l l}
|
\begin{tabular}{p{0.1\textwidth} p{0.32\textwidth}}
|
||||||
\textbf{Skills:} & [[ shape.skills ]] \\
|
\textbf{Skills:} & [[ shape.skills ]] \\
|
||||||
\textbf{Senses:} & [[ shape.senses ]] \\
|
\textbf{Senses:} & [[ shape.senses ]] \\
|
||||||
|
\textbf{Languages:} & [[ shape.languages ]] \\
|
||||||
\end{tabular}
|
\end{tabular}
|
||||||
|
|
||||||
\vspace{0.2cm}
|
\vspace{0.2cm}
|
||||||
|
|
||||||
[[ shape.__doc__ | rst_to_latex ]]
|
[[ shape.__doc__ | rst_to_latex ]]
|
||||||
|
|
||||||
|
} %\color
|
||||||
[% endfor %]
|
[% endfor %]
|
||||||
|
|
||||||
\end{document}
|
\end{document}
|
||||||
|
|||||||
@@ -24,13 +24,19 @@ character sheet."""
|
|||||||
bold_re = re.compile(r'\*\*([^*]+)\*\*')
|
bold_re = re.compile(r'\*\*([^*]+)\*\*')
|
||||||
it_re = re.compile(r'\*([^*]+)\*')
|
it_re = re.compile(r'\*([^*]+)\*')
|
||||||
tt_re = re.compile(r'``([^`]+)``')
|
tt_re = re.compile(r'``([^`]+)``')
|
||||||
|
# A dice string, with optinal backticks: ``1d6 + 3``
|
||||||
|
dice_re = re.compile(r'`*(\d+d\d+(?:\s*\+\s*\d+)?)`*')
|
||||||
|
|
||||||
def rst_to_latex(rst):
|
def rst_to_latex(rst):
|
||||||
"""Basic markup of RST to LaTeX code."""
|
"""Basic markup of RST to LaTeX code."""
|
||||||
tex = rst
|
if rst is None:
|
||||||
tex = bold_re.sub(r'\\textbf{\1}', tex)
|
tex = ""
|
||||||
tex = it_re.sub(r'\\textit{\1}', tex)
|
else:
|
||||||
tex = tt_re.sub(r'\\texttt{\1}', tex)
|
tex = rst
|
||||||
|
tex = bold_re.sub(r'\\textbf{\1}', tex)
|
||||||
|
tex = it_re.sub(r'\\textit{\1}', tex)
|
||||||
|
tex = dice_re.sub(r'\\texttt{\1}', tex)
|
||||||
|
tex = tt_re.sub(r'\\texttt{\1}', tex)
|
||||||
return tex
|
return tex
|
||||||
|
|
||||||
|
|
||||||
@@ -437,7 +443,6 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
|
|||||||
|
|
||||||
|
|
||||||
def make_sheet(character_file, flatten=False):
|
def make_sheet(character_file, flatten=False):
|
||||||
|
|
||||||
"""Prepare a PDF character sheet from the given character file.
|
"""Prepare a PDF character sheet from the given character file.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -472,7 +477,8 @@ def make_sheet(character_file, flatten=False):
|
|||||||
else:
|
else:
|
||||||
sheets.append(spellbook_base + '.pdf')
|
sheets.append(spellbook_base + '.pdf')
|
||||||
# Create a list of Druid wild_shapes
|
# Create a list of Druid wild_shapes
|
||||||
if len(char.wild_shapes) > 0:
|
wild_shapes = getattr(char, 'wild_shapes', [])
|
||||||
|
if len(wild_shapes) > 0:
|
||||||
shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes'
|
shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes'
|
||||||
try:
|
try:
|
||||||
create_druid_shapes_pdf(character=char, basename=shapes_base)
|
create_druid_shapes_pdf(character=char, basename=shapes_base)
|
||||||
|
|||||||
+122
-3
@@ -12,6 +12,8 @@ class Monster():
|
|||||||
challenge_rating = 0
|
challenge_rating = 0
|
||||||
armor_class = 0
|
armor_class = 0
|
||||||
skills = "Perception +3, Stealth +4"
|
skills = "Perception +3, Stealth +4"
|
||||||
|
senses = ""
|
||||||
|
languages = ""
|
||||||
strength = Ability()
|
strength = Ability()
|
||||||
dexterity = Ability()
|
dexterity = Ability()
|
||||||
constitution = Ability()
|
constitution = Ability()
|
||||||
@@ -30,12 +32,129 @@ class Monster():
|
|||||||
return is_beast
|
return is_beast
|
||||||
|
|
||||||
|
|
||||||
class Crocodile(Monster):
|
class Ankylosaurus(Monster):
|
||||||
name = "Crocodile"
|
"""Thick armor plating covers the body of the plant-eating dinosaur
|
||||||
|
ankylosaurus, which defends itself against predators with a
|
||||||
|
knobbed tail that delivers a devastating strike.
|
||||||
|
|
||||||
|
**Tail:** *Melee Weapon Attack:* +7 to hit, reach 10 ft., one
|
||||||
|
target. *Hit:* 18 (4d6+4) bludgeoning damage. If the target is a
|
||||||
|
creature, it must succeed on a DC 14 Strength saving throw or be
|
||||||
|
knocked prone.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = "Ankylosaurus"
|
||||||
|
description = "Huge beast, unaligned"
|
||||||
|
challenge_rating = 3
|
||||||
|
armor_class = 15
|
||||||
|
skills = ""
|
||||||
|
senses = "Passive perception 11"
|
||||||
|
strength = Ability(19)
|
||||||
|
dexterity = Ability(11)
|
||||||
|
constitution = Ability(15)
|
||||||
|
intelligence = Ability(2)
|
||||||
|
wisdom = Ability(12)
|
||||||
|
charisma = Ability(5)
|
||||||
|
speed = 30
|
||||||
|
swim_speed = 0
|
||||||
|
fly_speed = 0
|
||||||
|
hp_max = 68
|
||||||
|
hit_dice = '8d12+16'
|
||||||
|
|
||||||
|
|
||||||
|
class Ape(Monster):
|
||||||
|
"""**Multiattack:** The ape makes two fist attacks.
|
||||||
|
|
||||||
|
**Fist:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
|
||||||
|
target. *Hit:* 6 (1d6+3) bludgeoning damage.
|
||||||
|
|
||||||
|
**Rock:** *Ranged Weapon Attack:* +5 to hit, range 25/50 ft., one
|
||||||
|
target. *Hit:* 6 (1d6+3) bludgeoning damage.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = "Ape"
|
||||||
|
description = "Medium beast, unaligned"
|
||||||
|
challenge_rating = 1 / 2
|
||||||
|
armor_class = 12
|
||||||
|
skills = "Athletics +5, Perception +3"
|
||||||
|
senses = "Passive perception 13"
|
||||||
|
strength = Ability(16)
|
||||||
|
dexterity = Ability(14)
|
||||||
|
constitution = Ability(14)
|
||||||
|
intelligence = Ability(6)
|
||||||
|
wisdom = Ability(12)
|
||||||
|
charisma = Ability(7)
|
||||||
|
speed = 30
|
||||||
|
swim_speed = 0
|
||||||
|
fly_speed = 0
|
||||||
|
hp_max = 19
|
||||||
|
hit_dice = '3d8+6'
|
||||||
|
|
||||||
|
|
||||||
|
class Crocodile(Monster):
|
||||||
|
"""**Hold Breath:** The crocodile can hold its breath for 15 minutes.
|
||||||
|
|
||||||
|
**Bite:** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one
|
||||||
|
creature. *Hit:* 7 (1d10+2) piercing damage, and the target is
|
||||||
|
Grappled (escape DC 12). Until this grapple ends, the target is
|
||||||
|
Restrained, and the crocodile can't bite another target.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = "Crocodile"
|
||||||
|
description = "Large beast, unaligned"
|
||||||
|
challenge_rating = 1/2
|
||||||
|
armor_class = 12
|
||||||
|
skills = "Stealth +2"
|
||||||
|
senses = "Passive perception 10"
|
||||||
|
strength = Ability(15)
|
||||||
|
dexterity = Ability(10)
|
||||||
|
constitution = Ability(13)
|
||||||
|
intelligence = Ability(2)
|
||||||
|
wisdom = Ability(10)
|
||||||
|
charisma = Ability(5)
|
||||||
|
speed = 30
|
||||||
|
swim_speed = 30
|
||||||
|
fly_speed = 0
|
||||||
|
hp_max = 19
|
||||||
|
hit_dice = '3d10+3'
|
||||||
|
|
||||||
class GiantEagle(Monster):
|
class GiantEagle(Monster):
|
||||||
|
"""A giant eagle is a noble creature that speaks its own language and
|
||||||
|
understands Speech in the Common tongue. A mated pair of giant
|
||||||
|
eagles typically has up to four eggs or young in their nest (treat
|
||||||
|
the young as normal eagles).
|
||||||
|
|
||||||
|
**Keen Sight:** The eagle has advantage on Wisdom (Perception)
|
||||||
|
checks that rely on sight.
|
||||||
|
|
||||||
|
**Multiattack:** The eagle makes two attacks: one with its beak
|
||||||
|
and one with its talons.
|
||||||
|
|
||||||
|
**Beak:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
|
||||||
|
target. *Hit:* 6 (1d6 + 3) piercing damage.
|
||||||
|
|
||||||
|
**Talons:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
|
||||||
|
target. *Hit:* 10 (2d6 + 3) slashing damage.
|
||||||
|
|
||||||
|
"""
|
||||||
name = "Giant eagle"
|
name = "Giant eagle"
|
||||||
|
description = "Large beast, neutral good"
|
||||||
|
challenge_rating = 1
|
||||||
|
armor_class = 13
|
||||||
|
skills = "Perception +4"
|
||||||
|
senses = "Passive perception 14"
|
||||||
|
languages = "Giant Eagle, understands common and Auran but can't speak."
|
||||||
|
strength = Ability(16)
|
||||||
|
dexterity = Ability(17)
|
||||||
|
constitution = Ability(13)
|
||||||
|
intelligence = Ability(8)
|
||||||
|
wisdom = Ability(14)
|
||||||
|
charisma = Ability(10)
|
||||||
|
speed = 10
|
||||||
|
swim_speed = 0
|
||||||
|
fly_speed = 80
|
||||||
|
hp_max = 26
|
||||||
|
hit_dice = '4d10+4'
|
||||||
|
|
||||||
|
|
||||||
class Spider(Monster):
|
class Spider(Monster):
|
||||||
@@ -105,7 +224,7 @@ class Wolf(Monster):
|
|||||||
the creature and the ally isn't incapacitated. Actions
|
the creature and the ally isn't incapacitated. Actions
|
||||||
|
|
||||||
**Bite.** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one
|
**Bite.** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one
|
||||||
target. *Hit:* (2d4 + 2) piercing damage. If the target is a
|
target. *Hit:* (2d4+2) piercing damage. If the target is a
|
||||||
creature, it must succeed on a DC 11 Strength saving throw or be
|
creature, it must succeed on a DC 11 Strength saving throw or be
|
||||||
knocked prone
|
knocked prone
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+4
-5
@@ -9,6 +9,7 @@ dungeonsheets_version = "0.5.0"
|
|||||||
|
|
||||||
name = 'Dain Torunn'
|
name = 'Dain Torunn'
|
||||||
character_class = 'Druid'
|
character_class = 'Druid'
|
||||||
|
circle = 'moon'
|
||||||
player_name = 'Emily'
|
player_name = 'Emily'
|
||||||
background = "Sailor"
|
background = "Sailor"
|
||||||
race = "Hill Dwarf"
|
race = "Hill Dwarf"
|
||||||
@@ -45,13 +46,11 @@ shield = "" # Eg "shield"
|
|||||||
equipment = "TODO: Describe your equipment from your Druid class and Sailor background."
|
equipment = "TODO: Describe your equipment from your Druid class and Sailor background."
|
||||||
|
|
||||||
attacks_and_spellcasting = "TODO: Describe specifics for how your Druid attacks."
|
attacks_and_spellcasting = "TODO: Describe specifics for how your Druid attacks."
|
||||||
wild_shapes = ["wolf", "crocodile", "giant eagle"]
|
wild_shapes = ["wolf", "crocodile", "giant eagle", 'ape', 'ankylosaurus']
|
||||||
|
|
||||||
# List of known spells
|
# List of known spells
|
||||||
# Example: spells = ('magic missile', 'mage armor')
|
# Which spells have been prepared (including cantrips)
|
||||||
spells = () # Todo: Learn some spells
|
spells_prepared = ('shillelagh', 'poison spray', 'druidcraft','speak with animals', 'entangle', 'cure wounds', 'create or destroy water')
|
||||||
# Which spells have been prepared (not including cantrips)
|
|
||||||
spells_prepared = ()
|
|
||||||
|
|
||||||
# Backstory
|
# Backstory
|
||||||
# TODO: Describe your backstory here
|
# TODO: Describe your backstory here
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+57
-33
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
import warnings
|
||||||
|
|
||||||
from dungeonsheets import race, monsters, exceptions
|
from dungeonsheets import race, monsters, exceptions, spells
|
||||||
from dungeonsheets.character import Character, Wizard, Druid
|
from dungeonsheets.character import Character, Wizard, Druid
|
||||||
from dungeonsheets.weapons import Weapon, Shortsword
|
from dungeonsheets.weapons import Weapon, Shortsword
|
||||||
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
||||||
@@ -131,6 +132,51 @@ class TestCharacter(TestCase):
|
|||||||
self.assertEqual(char.spell_slots(spell_level=1), 3)
|
self.assertEqual(char.spell_slots(spell_level=1), 3)
|
||||||
self.assertEqual(char.spell_slots(spell_level=2), 0)
|
self.assertEqual(char.spell_slots(spell_level=2), 0)
|
||||||
|
|
||||||
|
def test_equip_armor(self):
|
||||||
|
char = Character(dexterity=16)
|
||||||
|
char.wear_armor('leather armor')
|
||||||
|
self.assertTrue(isinstance(char.armor, Armor))
|
||||||
|
# Now make sure the armor class is correct
|
||||||
|
self.assertEqual(char.armor_class, 14)
|
||||||
|
# Try passing an Armor object directly
|
||||||
|
char.wear_armor(LeatherArmor())
|
||||||
|
self.assertEqual(char.armor_class, 14)
|
||||||
|
# Test equipped armor with max dexterity mod_str
|
||||||
|
char.armor.dexterity_mod_max = 1
|
||||||
|
self.assertEqual(char.armor_class, 12)
|
||||||
|
|
||||||
|
def test_wield_shield(self):
|
||||||
|
char = Character(dexterity=16)
|
||||||
|
char.wield_shield('shield')
|
||||||
|
self.assertTrue(isinstance(char.shield, Shield), msg=char.shield)
|
||||||
|
# Now make sure the armor class is correct
|
||||||
|
self.assertEqual(char.armor_class, 15)
|
||||||
|
# Try passing an Armor object directly
|
||||||
|
char.wield_shield(Shield)
|
||||||
|
self.assertEqual(char.armor_class, 15)
|
||||||
|
|
||||||
|
def test_speed(self):
|
||||||
|
# Check that the speed pulls from the character's race
|
||||||
|
char = Character(race='halfling')
|
||||||
|
self.assertEqual(char.speed, 25)
|
||||||
|
# Check that a character with no race defaults to 30 feet
|
||||||
|
char = Character()
|
||||||
|
char.race = None
|
||||||
|
self.assertEqual(char.speed, 30)
|
||||||
|
|
||||||
|
|
||||||
|
class DruidTestCase(TestCase):
|
||||||
|
def test_learned_spells(self):
|
||||||
|
"""For a druid, learning spells is not necessary and this field should
|
||||||
|
be ignored."""
|
||||||
|
char = Druid()
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings('ignore', message="Druids cannot learn spells")
|
||||||
|
char.set_attrs(spells=['invisibility'],
|
||||||
|
spells_prepared=['druidcraft'])
|
||||||
|
self.assertEqual(len(char.spells), 1)
|
||||||
|
self.assertIsInstance(char.spells[0], spells.Druidcraft)
|
||||||
|
|
||||||
def test_wild_shapes(self):
|
def test_wild_shapes(self):
|
||||||
char = Druid()
|
char = Druid()
|
||||||
# Druid level 2
|
# Druid level 2
|
||||||
@@ -149,6 +195,16 @@ class TestCharacter(TestCase):
|
|||||||
self.assertEqual(len(char.wild_shapes), 1)
|
self.assertEqual(len(char.wild_shapes), 1)
|
||||||
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
|
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
|
||||||
|
|
||||||
|
def test_moon_druid_wild_shapes(self):
|
||||||
|
# Moon druid level 2 gets beasts up to CR 1
|
||||||
|
char = Druid(level=2, wild_shapes=['Ape'], circle='moon')
|
||||||
|
self.assertEqual(len(char.wild_shapes), 1)
|
||||||
|
self.assertIsInstance(char.wild_shapes[0], monsters.Ape)
|
||||||
|
# Moon druid above level 6 gets beasts up to CR level / 3
|
||||||
|
char = Druid(level=9, wild_shapes=['ankylosaurus'], circle='moon')
|
||||||
|
self.assertEqual(len(char.wild_shapes), 1)
|
||||||
|
self.assertIsInstance(char.wild_shapes[0], monsters.Ankylosaurus)
|
||||||
|
|
||||||
def test_can_assume_shape(self):
|
def test_can_assume_shape(self):
|
||||||
class Beast(monsters.Monster):
|
class Beast(monsters.Monster):
|
||||||
description = 'beast'
|
description = 'beast'
|
||||||
@@ -188,35 +244,3 @@ class TestCharacter(TestCase):
|
|||||||
not_beast = monsters.Monster()
|
not_beast = monsters.Monster()
|
||||||
not_beast.description = "monster"
|
not_beast.description = "monster"
|
||||||
self.assertFalse(low_druid.can_assume_shape(not_beast))
|
self.assertFalse(low_druid.can_assume_shape(not_beast))
|
||||||
|
|
||||||
def test_equip_armor(self):
|
|
||||||
char = Character(dexterity=16)
|
|
||||||
char.wear_armor('leather armor')
|
|
||||||
self.assertTrue(isinstance(char.armor, Armor))
|
|
||||||
# Now make sure the armor class is correct
|
|
||||||
self.assertEqual(char.armor_class, 14)
|
|
||||||
# Try passing an Armor object directly
|
|
||||||
char.wear_armor(LeatherArmor())
|
|
||||||
self.assertEqual(char.armor_class, 14)
|
|
||||||
# Test equipped armor with max dexterity mod_str
|
|
||||||
char.armor.dexterity_mod_max = 1
|
|
||||||
self.assertEqual(char.armor_class, 12)
|
|
||||||
|
|
||||||
def test_wield_shield(self):
|
|
||||||
char = Character(dexterity=16)
|
|
||||||
char.wield_shield('shield')
|
|
||||||
self.assertTrue(isinstance(char.shield, Shield), msg=char.shield)
|
|
||||||
# Now make sure the armor class is correct
|
|
||||||
self.assertEqual(char.armor_class, 15)
|
|
||||||
# Try passing an Armor object directly
|
|
||||||
char.wield_shield(Shield)
|
|
||||||
self.assertEqual(char.armor_class, 15)
|
|
||||||
|
|
||||||
def test_speed(self):
|
|
||||||
# Check that the speed pulls from the character's race
|
|
||||||
char = Character(race='halfling')
|
|
||||||
self.assertEqual(char.speed, 25)
|
|
||||||
# Check that a character with no race defaults to 30 feet
|
|
||||||
char = Character()
|
|
||||||
char.race = None
|
|
||||||
self.assertEqual(char.speed, 30)
|
|
||||||
|
|||||||
@@ -30,3 +30,20 @@ class PdfOutputTeestCase(unittest.TestCase):
|
|||||||
char.saving_throw_proficiencies = ['strength']
|
char.saving_throw_proficiencies = ['strength']
|
||||||
make_sheets.create_character_pdf(character=char, basename=self.basename)
|
make_sheets.create_character_pdf(character=char, basename=self.basename)
|
||||||
self.assertTrue(os.path.exists(pdf_name), f'{pdf_name} not created.')
|
self.assertTrue(os.path.exists(pdf_name), f'{pdf_name} not created.')
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownTestCase(unittest.TestCase):
|
||||||
|
"""Check that conversion of markdown formats to LaTeX code works
|
||||||
|
correctly."""
|
||||||
|
|
||||||
|
def test_rst_bold(self):
|
||||||
|
text = make_sheets.rst_to_latex('**hello**')
|
||||||
|
self.assertEqual(text, '\\textbf{hello}')
|
||||||
|
|
||||||
|
def test_hit_dice(self):
|
||||||
|
text = make_sheets.rst_to_latex('1d6+3')
|
||||||
|
self.assertEqual(text, '\\texttt{1d6+3}')
|
||||||
|
|
||||||
|
def test_no_text(self):
|
||||||
|
text = make_sheets.rst_to_latex(None)
|
||||||
|
self.assertEqual(text, '')
|
||||||
|
|||||||
Reference in New Issue
Block a user