Added Druid wild shapes, tweaked armor, and fixed some bugs.

Druid's can now add ``wild_shapes = `` to their character file. "Light
leather armor" is now just "Leather Armor".
This commit is contained in:
Mark Wolfman
2018-10-21 02:09:15 -05:00
parent 0140290b51
commit 0e543deee9
20 changed files with 592 additions and 50 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.0
0.6.0
+10 -6
View File
@@ -8,6 +8,10 @@ class Shield():
return self.name
class WoodenShield(Shield):
name = 'Wooden shield'
class NoShield(Shield):
"""If a character is carrying no shield."""
name = "No shield"
@@ -64,22 +68,22 @@ class LightPaddedArmor(Armor):
stealth_disadvantage = True
class LightLeatherArmor(Armor):
name = "Light leather armor"
class LeatherArmor(Armor):
name = "Leather armor"
cost = "10 gp"
base_armor_class = 11
weight = 10
class LightStuddedArmor(Armor):
name = "Light studded armor"
class StuddedArmor(Armor):
name = "Studded armor"
cost = "45 gp"
base_armor_class = 12
weight = 13
class MediumHideArmor(Armor):
name = "Medium hide armor"
class HideArmor(Armor):
name = "Hide armor"
cost = "10 gp"
base_armor_class = 12
dexterity_mod_max = 2
+110 -7
View File
@@ -5,7 +5,7 @@ import warnings
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
from . import weapons, race, spells, armor
from . import weapons, race, spells, armor, monsters, exceptions
from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield
@@ -86,6 +86,8 @@ 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``"""
@@ -98,6 +100,82 @@ 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)
@@ -126,7 +204,8 @@ class Character():
try:
_spells.append(findattr(spells, spell_name))
except AttributeError:
msg = f'Spell "{spell_name}" not defined. Please add it to ``spells.py``'
msg = (f'Spell "{spell_name}" not defined. '
f'Please add it to ``spells.py``')
warnings.warn(msg)
# Create temporary spell
_spells.append(spells.create_spell(name=spell_name, level=9))
@@ -213,12 +292,12 @@ class Character():
"""
if new_armor not in ('', None):
try:
if isinstance(new_armor, armor.Armor):
new_armor = new_armor
else:
NewArmor = findattr(armor, new_armor)
except AttributeError:
# Not a string, so just treat it as Armor
NewArmor = new_armor
self.armor = NewArmor()
new_armor = NewArmor()
self.armor = new_armor
def wield_shield(self, shield):
"""Accepts a string or Shield class and replaces the current armor.
@@ -347,6 +426,8 @@ class Druid(Character):
class_name = 'Druid'
hit_dice_faces = 8
saving_throw_proficiencies = ('intelligence', 'wisdom')
spellcasting_ability = 'wisdom'
languages = 'Druidic'
_proficiencies_text = (
'Light armor', 'medium armor',
'shields (druids will not wear armor or use shields made of metal)',
@@ -357,6 +438,28 @@ class Druid(Character):
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
class_skill_choices = ('Arcana', 'Animal Handling', 'Insight',
'Medicine', 'Nature', 'Perception', 'Religion', 'Survival')
spell_slots_by_level = {
1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (3, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (3, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (4, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (4, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (4, 4, 3, 3, 3, 3, 1, 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),
}
class Fighter(Character):
+2
View File
@@ -5,6 +5,8 @@ sheet by running ``makesheets`` from the command line.
"""
dungeonsheets_version = "{{ dungeonsheets_version }}"
name = '{{ char.name }}'
character_class = '{{ char.class_name }}'
player_name = '{{ char.player_name }}'
+10 -2
View File
@@ -16,6 +16,13 @@ import jinja2
from dungeonsheets import character, race, dice, background
def read_version():
version = open(os.path.join(os.path.dirname(__file__), '../VERSION')).read()
version = version.replace('\n', '')
return version
char_classes = {
'Barbarian': character.Barbarian,
'Bard': character.Bard,
@@ -69,7 +76,8 @@ class App(npyscreen.NPSAppManaged):
def save_character(self):
# Create the template context
context = dict(
char=self.character
char=self.character,
dungeonsheets_version=read_version(),
)
# Render the template
src_path = os.path.dirname(__file__)
@@ -353,7 +361,7 @@ class BasicInfoForm(npyscreen.ActionForm):
self.parentApp.setNextForm('CLASS')
def on_cancel(self):
self.parentApp.setNextForm(None)
raise KeyboardInterrupt
class SaveForm(npyscreen.ActionForm):
+71
View File
@@ -0,0 +1,71 @@
\documentclass[twocolumn,lettersize]{article}
%% \usepackage{fullpage}
\usepackage[margin=1.5cm]{geometry}
\usepackage[dvipsnames]{color}
\usepackage{indentfirst}
\definecolor{mygrey}{gray}{0.7}
\title{Wild Shapes}
\date{}
\author{[[ character.name ]]}
\begin{document}
\twocolumn[
\begin{@twocolumnfalse}
\maketitle
\section*{Known Beasts}
[% for shape in character.all_wild_shapes|sort(attribute="name") %]%
[[ shape.name ]][% if not loop.last %], [% endif %]%
[% endfor %]%
\vspace{3ex}
\end{@twocolumnfalse}
]
[% for shape in character.wild_shapes|sort(attribute='challenge_rating') %]
\section*{[[ shape.name ]]}
\subsection*{[[ shape.description ]]}
\begin{tabular}{c | c | c}
Armor Class & Hit Points & Speed \\
\hline
[[ shape.armor_class ]] &
[[ shape.hit_points_max ]] ([[ shape.hit_dice ]]) &
[[ shape.speed ]] \\
[% if shape.swim_speed %]
& & [[ shape.swim_speed ]] swim \\
[% endif %]
[% if shape.fly_speed %]
& & [[ shape.fly_speed ]] fly \\
[% endif %]
\end{tabular}
\vspace{0.2cm}
\begin{tabular}{c | c | c}
STR & DEX & CON \\
\hline
[[ shape.strength.value ]] ([[ shape.strength.modifier|mod_str ]]) &
[[ shape.dexterity.value ]] ([[ shape.dexterity.modifier|mod_str ]]) &
[[ shape.constitution.value ]] ([[ shape.constitution.modifier|mod_str ]]) \\
\end{tabular}
\vspace{0.2cm}
\begin{tabular}{l l}
\textbf{Skills:} & [[ shape.skills ]] \\
\textbf{Senses:} & [[ shape.senses ]] \\
\end{tabular}
\vspace{0.2cm}
[[ shape.__doc__ | rst_to_latex ]]
[% endfor %]
\end{document}
+3
View File
@@ -9,3 +9,6 @@ class LatexError(OSError):
class LatexNotFoundError(LatexError):
"""PDFLatex did not execute correctly."""
class MonsterError(AttributeError):
"""Error retriving or using a D&D Monster."""
+49 -12
View File
@@ -8,9 +8,11 @@ import os
import subprocess
import warnings
import re
from io import StringIO
from fdfgen import forge_fdf
import pdfrw
from jinja2 import Environment, PackageLoader
from dungeonsheets import character, exceptions
from dungeonsheets.stats import mod_str
@@ -19,6 +21,29 @@ from dungeonsheets.stats import mod_str
"""Program to take character definitions and build a PDF of the
character sheet."""
bold_re = re.compile(r'\*\*([^*]+)\*\*')
it_re = re.compile(r'\*([^*]+)\*')
tt_re = re.compile(r'``([^`]+)``')
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)
return tex
jinja_env = Environment(
loader=PackageLoader('dungeonsheets', ''),
block_start_string='[%',
block_end_string='%]',
variable_start_string='[[',
variable_end_string=']]',
)
jinja_env.filters['rst_to_latex'] = rst_to_latex
jinja_env.filters['mod_str'] = mod_str
CHECKBOX_ON = 'Yes'
CHECKBOX_OFF = 'Off'
@@ -51,7 +76,7 @@ def load_character_file(filename):
if ext != '.py':
raise ValueError(f"Character definition {filename} is not a python file.")
# Check if this file contains the version string
version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-4.]+)[\'"]')
version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
with open(filename, mode='r') as f:
version = None
for line in f:
@@ -62,7 +87,7 @@ def load_character_file(filename):
if version is None:
# Not a valid DND character file
raise exceptions.CharacterFileFormatError(
"No ``dungeonsheets_version = `` entry.")
f"No ``dungeonsheets_version = `` entry in `{filename}`.")
# Import the module to extract the information
spec = importlib.util.spec_from_file_location('module', filename)
module = importlib.util.module_from_spec(spec)
@@ -75,16 +100,17 @@ def load_character_file(filename):
return char_props
def create_druid_shapes_pdf(character, basename):
template = jinja_env.get_template('druid_shapes_template.tex')
return create_latex_pdf(character, basename, template)
def create_spellbook_pdf(character, basename):
from jinja2 import Environment, PackageLoader
env = Environment(
loader=PackageLoader('dungeonsheets', ''),
block_start_string='[%',
block_end_string='%]',
variable_start_string='[[',
variable_end_string=']]',
)
template = env.get_template('spellbook_template.tex')
template = jinja_env.get_template('spellbook_template.tex')
return create_latex_pdf(character, basename, template)
def create_latex_pdf(character, basename, template):
tex = template.render(character=character)
# Create tex document
tex_file = f'{basename}.tex'
@@ -300,7 +326,8 @@ def create_character_pdf(character, basename, flatten=False):
fields[dmg_field] = f'{weapon.damage} {weapon.damage_type}'
# Other attack information
attack_str = f'Armor: {character.armor}'
attack_str += f'Shield: {character.shield}\n\n'
attack_str += '\n\r'
attack_str += f'Shield: {character.shield}'
attack_str += character.attacks_and_spellcasting
fields['AttacksSpellcasting'] = text_box(attack_str)
# Other proficiencies and languages
@@ -444,6 +471,16 @@ def make_sheet(character_file, flatten=False):
f'for {char.name}')
else:
sheets.append(spellbook_base + '.pdf')
# Create a list of Druid wild_shapes
if len(char.wild_shapes) > 0:
shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes'
try:
create_druid_shapes_pdf(character=char, basename=shapes_base)
except exceptions.LatexNotFoundError as e:
log.warning('``pdflatex`` not available. Skipping wild shapes list '
f'for {char.name}')
else:
sheets.append(shapes_base + '.pdf')
# Combine sheets into final pdf
final_pdf = os.path.splitext(character_file)[0] + '.pdf'
merge_pdfs(sheets, final_pdf, clean_up=True)
+69
View File
@@ -0,0 +1,69 @@
"""A collection of monsters. Also useful for building a list of wild
shape forms."""
from .stats import Ability
class Monster():
"""A monster that may be encountered when adventuring."""
name = "Generic Monster"
description = ""
challenge_rating = 0
armor_class = 0
skills = "Perception +3, Stealth +4"
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
speed = 30
swim_speed = 0
fly_speed = 0
hp_max = 10
hit_dice = '1d6'
@property
def is_beast(self):
is_beast = 'beast' in self.description.lower()
return is_beast
class Crocodile(Monster):
name = "Crocodile"
class GiantEagle(Monster):
name = "Giant eagle"
class Wolf(Monster):
"""**Keen Hearing and Smell.** The wolf has advantage on Wisdom
(Perception) checks that rely on hearing or smell.
**Pack Tactics.** The wolf has advantage on an attack roll against a
creature if at least one of the wolf's allies is within 5 ft. of
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
creature, it must succeed on a DC 11 Strength saving throw or be
knocked prone
"""
name = "Wolf"
description = "Medium beast, unaligned"
challenge_rating = 1/4
armor_class = 13
skills = "Perception +3, Stealth +4"
senses = "Passive Perception 13"
strength = Ability(12)
dexterity = Ability(15)
constitution = Ability(12)
intelligence = Ability(6)
wisdom = Ability(12)
charisma = Ability(6)
speed = 40
hp_max = 11
hit_dice = '2d8+2'
+9 -5
View File
@@ -6,8 +6,8 @@
\definecolor{mygrey}{gray}{0.7}
\title{Spells and Incantations}
\author{[[ character.name ]]}
\date{}
\begin{document}
@@ -29,9 +29,13 @@
[% else %] %
\textit{[[ spl.magic_school ]] Cantrip} %
[% endif %] %
[% if spl.ritual %] %
\textit{(ritual)}
[% endif %]
[% if spl.ritual and spl.concentration %]%
(\textit{ritual}, \textit{concentration})%
[% elif spl.ritual %]%
(\textit{ritual})%
[% elif spl.concentration %]%
(\textit{concentration})%
[% endif %]%
\noindent
\textbf{Casting Time:} [[ spl.casting_time ]] \\
@@ -39,7 +43,7 @@
\textbf{Components:} [[ spl.component_string() ]] \\
\textbf{Duration:} [[ spl.duration ]]
[[ spl.__doc__ ]]
[[ spl.__doc__|rst_to_latex ]]
} %\color
[% endfor %]
+107 -9
View File
@@ -1540,6 +1540,35 @@ class Counterspell(Spell):
classes = ()
class CreateOrDestroyWater(Spell):
"""You either create or destroy water.
**Create Water.** You create up to 10 gallons of clean water
within range in an open container. Alternatively, the water falls
as rain in a 30-foot cube within range, extinguishing exposed
flames in the area.
**Destroy Water.** You destroy up to 10 gallons of water in an open
container within range. Alternatively, you destroy fog in a
30-foot cube within range.
**At Higher Levels.** When you cast this spell using a spell slot
of 2nd level or higher, you create or destroy 10 additional
gallons of water, or the size of the cube increases by 5 feet, for
each slot level above 1st.
"""
level = 1
name = "Create or Destroy Water"
casting_time = "1 action"
casting_range = "30 ft (30 ft cube)"
components = ("V", "S", "M")
materials = "a drop of water if creating water or a few grains of sand if destroying it"
duration = "instantaneous"
magic_school = "Transmutation"
classes = ('Cleric', 'Druid')
class CreateUndead(Spell):
"""You can cast this spell only at night. Choose up to three corpses
of Medium or Small humanoids within range. Each corpse becomes a
@@ -1585,11 +1614,11 @@ class CreateUndead(Spell):
class CureWounds(Spell):
"""A creature you touch regains a number of hit points equal to 1d8 +
your spellcasting ability modifier. This spell has no effect on
undead or constructs. At Higher Levels. When you cast this spell
using a spell slot of 2nd level or higher, the healing increases
by 1d8 for each slot level above 1st.
"""A creature you touch regains a number of hit points equal to
``1d8`` + your spellcasting ability modifier. This spell has no
effect on undead or constructs. At Higher Levels. When you cast
this spell using a spell slot of 2nd level or higher, the healing
increases by ``1d8`` for each slot level above 1st.
"""
name = "Cure Wounds"
@@ -1725,7 +1754,6 @@ class DetectMagic(Spell):
classes = ('Bard', 'Cleric', 'Druid', 'Paladin', 'Ranger', 'Sorceror', 'Wizard', )
class DimensionDoor(Spell):
"""You teleport yourself from your current location to any other spot
within range. You arrive at exactly the spot desired. It can be a
@@ -2061,6 +2089,31 @@ class ElementalWeapon(Spell):
classes = ('Paladin', )
class Entangle(Spell):
"""Grasping weeds and vines sprout from the ground in a 20-foot square
starting from a point within range. For the duration, these plants
turn the ground in the area into difficult terrain.
A creature in the area when you cast the spell must succeed on a
Strength saving throw or be restrained by the entangling plants
until the spell ends. A creature restrained by the plants can use
its action to make a Strength check against your spell save DC. On
a success, it frees itself.
When the spell ends, the conjured plants wilt away.
"""
level = 1
name = "Entangle"
casting_time = "1 action"
casting_range = "90 ft (20 ft area)"
components = ("V", "S")
concentration = True
duration = "instantaneous"
magic_school = "Conjuration"
classes = ('Druid')
class Etherealness(Spell):
"""You step into the border regions of the Ethereal Plane, in the area
where it overlaps with your current plane. You remain in the
@@ -3529,9 +3582,10 @@ class PhantasmalForce(Spell):
class PoisonSpray(Spell):
"""You extend your hand toward a creature you can see within range and
project a puff of noxious gas from your palm. The creature must
succeed on a Constitution saving throw or take 1d12 poison
damage. This spells damage increases by 1d12 when you reach 5th
level (2d12), 11th level (3d12), and 17th level (4d12).
succeed on a Constitution saving throw or take ``1d12`` poison
damage. This spells damage increases by ``1d12`` when you reach
5th level (``2d12``), 11th level (``3d12``), and 17th level
(``4d12``).
"""
name = "Poison Spray"
@@ -3888,6 +3942,29 @@ class Sanctuary(Spell):
classes = ()
class Shillelagh(Spell):
"""The wood of a club or quarterstaff you are holding is imbued with
nature's power. For the duration, you can use your spellcasting
ability instead of Strength for the attack and damage rolls of
melee attacks using that weapon, and the weapon's damage die
becomes a ``d8``. The weapon also becomes magical, if it isn't
already. The spell ends if you cast it again or if you let go of
the weapon.
"""
level = 0
name = "Shillelagh"
casting_time = "1 bonus action"
casting_range = "Touch"
components = ("V", "S", "M")
materials = "mistletoe, a shamrock leaf, and a club or quarterstaff"
duration = "1 minute"
concentration = False
ritual = False
magic_school = "Transmutation"
classes = ('Druid')
class Shatter(Spell):
"""A sudden loud ringing noise, painfully intense, erupts from a point
of your choice within range. Each creature in a 10-foot-radius
@@ -4064,6 +4141,27 @@ class SpareTheDying(Spell):
classes = ()
class SpeakWithAnimals(Spell):
"""You gain the ability to comprehend and verbally communicate with
beasts for the duration. The knowledge and awareness of many
beasts is limited by their intelligence, but at minimum, beasts
can give you information about nearby locations and monsters,
including whatever they can perceive or have perceived within the
past day. You might be able to persuade a beast to perform a small
favor for you, at the GM's discretion.
"""
level = 1
name = "Speak with Animals"
casting_time = "1 action"
casting_range = "Self"
components = ("V", "S")
duration = "10 minutes"
ritual = True
magic_school = "Divination"
classes = ('Bard', 'Druid', 'Ranger')
class SpeakWithDead(Spell):
"""You grant the semblance of life and intelligence to a corpse of
your choice within range, allowing it to answer the questions you
+1 -1
View File
@@ -57,7 +57,7 @@ class Ability():
modifier = math.floor((score - 10) / 2)
# Check for proficiency
saving_throw = modifier
if self.ability_name is not None:
if self.ability_name is not None and hasattr(character, 'saving_throw_proficiencies'):
is_proficient = (self.ability_name in character.saving_throw_proficiencies)
if is_proficient:
saving_throw += character.proficiency_bonus
Binary file not shown.
+72
View File
@@ -0,0 +1,72 @@
"""This file describes the heroic adventurer Dain Torunn.
Modify this file as you level up and then re-generate the character
sheet by running ``makesheets`` from the command line.
"""
dungeonsheets_version = "0.5.0"
name = 'Dain Torunn'
character_class = 'Druid'
player_name = 'Emily'
background = "Sailor"
race = "Hill Dwarf"
level = 2
alignment = "Neutral good"
xp = 1176
hp_max = 18
# Ability Scores
strength = 10
dexterity = 14
constitution = 15
intelligence = 11
wisdom = 16
charisma = 13
skill_proficiencies = ('nature', 'insight', 'athletics', 'perception')
# Proficiencies and languages
languages = "Common, Dwarvish"
# Inventory
# TODO: Get yourself some money
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
# TODO: Put your equipped weapons and armor here
weapons = () # Example: ('shortsword', 'longsword')
armor = "" # Eg "light leather armor"
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"]
# 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 = ()
# Backstory
# TODO: Describe your backstory here
personality_traits = """I am a leaf on the wind,
watch how I...
"""
ideals = """
"""
bonds = """
"""
flaws = """
"""
features_and_traits = """
"""
Binary file not shown.
+1 -1
View File
@@ -36,7 +36,7 @@ ep = 50
gp = 120
pp = 0
weapons = ('shortsword', 'shortbow')
armor = 'light leather armor'
armor = 'leather armor'
shield = 'shield'
equipment = (
"""Shortsword, shortbow, 20 arrows, leather armor, thieves tools,
Binary file not shown.
Binary file not shown.
+64 -6
View File
@@ -2,10 +2,10 @@
from unittest import TestCase
from dungeonsheets import race
from dungeonsheets.character import Character, Wizard
from dungeonsheets import race, monsters, exceptions
from dungeonsheets.character import Character, Wizard, Druid
from dungeonsheets.weapons import Weapon, Shortsword
from dungeonsheets.armor import Armor, LightLeatherArmor, Shield
from dungeonsheets.armor import Armor, LeatherArmor, Shield
class TestCharacter(TestCase):
@@ -30,7 +30,7 @@ class TestCharacter(TestCase):
self.assertEqual(len(char.weapons), 1)
self.assertTrue(isinstance(char.weapons[0], Shortsword))
# Check that armor and shield gets set_attrs
char.set_attrs(armor='light leather armor', shield='shield')
char.set_attrs(armor='leather armor', shield='shield')
self.assertFalse(isinstance(char.armor, str))
self.assertFalse(isinstance(char.shield, str))
# Check that race gets set to an object
@@ -131,14 +131,72 @@ class TestCharacter(TestCase):
self.assertEqual(char.spell_slots(spell_level=1), 3)
self.assertEqual(char.spell_slots(spell_level=2), 0)
def test_wild_shapes(self):
char = Druid()
# Druid level 2
char.level = 2
# Set reasonable wild shapes
char.wild_shapes = ['Wolf']
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
# Check what happens if a non-existent wild_shape is added
with self.assertRaises(exceptions.MonsterError):
char.wild_shapes = ['Wolf', 'Hyperion Loader']
# Check what happens if a valid monster class is directly added
char.wild_shapes = [monsters.Wolf(), ]
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
# Check that invalid monsters aren't accepted
char.wild_shapes = ['Wolf', 'giant eagle']
self.assertEqual(len(char.wild_shapes), 1)
self.assertIsInstance(char.wild_shapes[0], monsters.Wolf)
def test_can_assume_shape(self):
class Beast(monsters.Monster):
description = 'beast'
new_druid = Druid(level=1)
low_druid = Druid(level=2)
mid_druid = Druid(level=4)
high_druid = Druid(level=8)
beast = Beast()
# Check that level 1 druid automatically fails
self.assertFalse(new_druid.can_assume_shape(beast))
# Check if a basic beast can be transformed
self.assertTrue(low_druid.can_assume_shape(beast))
# Check that challenge rating is checked
hard_beast = Beast()
hard_beast.challenge_rating = 1/2
really_hard_beast = Beast()
really_hard_beast.challenge_rating = 1
self.assertFalse(low_druid.can_assume_shape(hard_beast))
self.assertFalse(low_druid.can_assume_shape(really_hard_beast))
self.assertTrue(mid_druid.can_assume_shape(hard_beast))
self.assertFalse(mid_druid.can_assume_shape(really_hard_beast))
self.assertTrue(high_druid.can_assume_shape(hard_beast))
self.assertTrue(high_druid.can_assume_shape(really_hard_beast))
# Check that swim speed is enforced
swim_beast = Beast()
swim_beast.swim_speed = 15
self.assertFalse(low_druid.can_assume_shape(swim_beast))
self.assertTrue(mid_druid.can_assume_shape(swim_beast))
self.assertTrue(high_druid.can_assume_shape(swim_beast))
# Check that fly speed is enforced
fly_beast = Beast()
fly_beast.fly_speed = 15
self.assertFalse(low_druid.can_assume_shape(fly_beast))
self.assertFalse(mid_druid.can_assume_shape(fly_beast))
self.assertTrue(high_druid.can_assume_shape(fly_beast))
# Check that non-beasts are not allowed
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('light leather armor')
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(LightLeatherArmor)
char.wear_armor(LeatherArmor())
self.assertEqual(char.armor_class, 14)
# Test equipped armor with max dexterity mod_str
char.armor.dexterity_mod_max = 1
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python
from unittest import TestCase
from dungeonsheets import monsters, exceptions
class MonsterTestCase(TestCase):
def test_ability_scores(self):
wolf = monsters.Wolf()
self.assertEqual(wolf.strength.value, 12)
self.assertEqual(wolf.strength.modifier, 1)
self.assertEqual(wolf.strength.saving_throw, 1)