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',
|
||||
'ray of sickness', 'shield', 'sleep',)
|
||||
|
||||
.. note::
|
||||
|
||||
Some character classes have modified spellcasting mechanics that
|
||||
affects how these entries are intepreted.
|
||||
|
||||
- `Druid`_
|
||||
|
||||
|
||||
|
||||
Personality and Backstory
|
||||
=========================
|
||||
@@ -210,6 +218,47 @@ source file more readable, but are not required.
|
||||
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
|
||||
|
||||
.. _issue: https://github.com/canismarko/dungeon-sheets/issues
|
||||
|
||||
+96
-78
@@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
import warnings
|
||||
import math
|
||||
|
||||
from .stats import Ability, Skill, findattr
|
||||
from .dice import read_dice_str
|
||||
@@ -86,8 +87,6 @@ class Character():
|
||||
spellcasting_ability = None
|
||||
spells = tuple()
|
||||
spells_prepared = tuple()
|
||||
# Druid wilf shape transofmration options
|
||||
_wild_shapes = ()
|
||||
|
||||
def __init__(self, **attrs):
|
||||
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
||||
@@ -100,82 +99,6 @@ class Character():
|
||||
def __repr__(self):
|
||||
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
|
||||
def speed(self):
|
||||
return getattr(self.race, 'speed', 30)
|
||||
@@ -424,6 +347,8 @@ class Cleric(Character):
|
||||
|
||||
class Druid(Character):
|
||||
class_name = 'Druid'
|
||||
circle = "" # Moon, land
|
||||
_wild_shapes = ()
|
||||
hit_dice_faces = 8
|
||||
saving_throw_proficiencies = ('intelligence', 'wisdom')
|
||||
spellcasting_ability = 'wisdom'
|
||||
@@ -460,6 +385,99 @@ class Druid(Character):
|
||||
19: (4, 4, 3, 3, 3, 3, 2, 1, 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):
|
||||
|
||||
@@ -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 ]]}
|
||||
[% if shape.description %]
|
||||
\subsection*{[[ shape.description ]]}
|
||||
[% endif %]
|
||||
|
||||
\begin{tabular}{c | c | c}
|
||||
Armor Class & Hit Points & Speed \\
|
||||
@@ -57,15 +65,17 @@
|
||||
|
||||
\vspace{0.2cm}
|
||||
|
||||
\begin{tabular}{l l}
|
||||
\begin{tabular}{p{0.1\textwidth} p{0.32\textwidth}}
|
||||
\textbf{Skills:} & [[ shape.skills ]] \\
|
||||
\textbf{Senses:} & [[ shape.senses ]] \\
|
||||
\textbf{Languages:} & [[ shape.languages ]] \\
|
||||
\end{tabular}
|
||||
|
||||
\vspace{0.2cm}
|
||||
|
||||
[[ shape.__doc__ | rst_to_latex ]]
|
||||
|
||||
} %\color
|
||||
[% endfor %]
|
||||
|
||||
\end{document}
|
||||
|
||||
@@ -24,13 +24,19 @@ character sheet."""
|
||||
bold_re = re.compile(r'\*\*([^*]+)\*\*')
|
||||
it_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):
|
||||
"""Basic markup of RST to LaTeX code."""
|
||||
tex = rst
|
||||
tex = bold_re.sub(r'\\textbf{\1}', tex)
|
||||
tex = it_re.sub(r'\\textit{\1}', tex)
|
||||
tex = tt_re.sub(r'\\texttt{\1}', tex)
|
||||
if rst is None:
|
||||
tex = ""
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
@@ -437,7 +443,6 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
|
||||
|
||||
|
||||
def make_sheet(character_file, flatten=False):
|
||||
|
||||
"""Prepare a PDF character sheet from the given character file.
|
||||
|
||||
Parameters
|
||||
@@ -472,7 +477,8 @@ def make_sheet(character_file, flatten=False):
|
||||
else:
|
||||
sheets.append(spellbook_base + '.pdf')
|
||||
# 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'
|
||||
try:
|
||||
create_druid_shapes_pdf(character=char, basename=shapes_base)
|
||||
|
||||
+122
-3
@@ -12,6 +12,8 @@ class Monster():
|
||||
challenge_rating = 0
|
||||
armor_class = 0
|
||||
skills = "Perception +3, Stealth +4"
|
||||
senses = ""
|
||||
languages = ""
|
||||
strength = Ability()
|
||||
dexterity = Ability()
|
||||
constitution = Ability()
|
||||
@@ -30,12 +32,129 @@ class Monster():
|
||||
return is_beast
|
||||
|
||||
|
||||
class Crocodile(Monster):
|
||||
name = "Crocodile"
|
||||
class Ankylosaurus(Monster):
|
||||
"""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):
|
||||
"""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"
|
||||
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):
|
||||
@@ -105,7 +224,7 @@ class Wolf(Monster):
|
||||
the creature and the ally isn't incapacitated. Actions
|
||||
|
||||
**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
|
||||
knocked prone
|
||||
|
||||
|
||||
Binary file not shown.
+4
-5
@@ -9,6 +9,7 @@ dungeonsheets_version = "0.5.0"
|
||||
|
||||
name = 'Dain Torunn'
|
||||
character_class = 'Druid'
|
||||
circle = 'moon'
|
||||
player_name = 'Emily'
|
||||
background = "Sailor"
|
||||
race = "Hill Dwarf"
|
||||
@@ -45,13 +46,11 @@ shield = "" # Eg "shield"
|
||||
equipment = "TODO: Describe your equipment from your Druid class and Sailor background."
|
||||
|
||||
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
|
||||
# Example: spells = ('magic missile', 'mage armor')
|
||||
spells = () # Todo: Learn some spells
|
||||
# Which spells have been prepared (not including cantrips)
|
||||
spells_prepared = ()
|
||||
# Which spells have been prepared (including cantrips)
|
||||
spells_prepared = ('shillelagh', 'poison spray', 'druidcraft','speak with animals', 'entangle', 'cure wounds', 'create or destroy water')
|
||||
|
||||
# Backstory
|
||||
# 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
|
||||
|
||||
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.weapons import Weapon, Shortsword
|
||||
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=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):
|
||||
char = Druid()
|
||||
# Druid level 2
|
||||
@@ -149,6 +195,16 @@ class TestCharacter(TestCase):
|
||||
self.assertEqual(len(char.wild_shapes), 1)
|
||||
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):
|
||||
class Beast(monsters.Monster):
|
||||
description = 'beast'
|
||||
@@ -188,35 +244,3 @@ class TestCharacter(TestCase):
|
||||
not_beast = monsters.Monster()
|
||||
not_beast.description = "monster"
|
||||
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']
|
||||
make_sheets.create_character_pdf(character=char, basename=self.basename)
|
||||
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