mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 21:23:31 +02:00
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:
+10
-6
@@ -8,6 +8,10 @@ class Shield():
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class WoodenShield(Shield):
|
||||||
|
name = 'Wooden shield'
|
||||||
|
|
||||||
|
|
||||||
class NoShield(Shield):
|
class NoShield(Shield):
|
||||||
"""If a character is carrying no shield."""
|
"""If a character is carrying no shield."""
|
||||||
name = "No shield"
|
name = "No shield"
|
||||||
@@ -64,22 +68,22 @@ class LightPaddedArmor(Armor):
|
|||||||
stealth_disadvantage = True
|
stealth_disadvantage = True
|
||||||
|
|
||||||
|
|
||||||
class LightLeatherArmor(Armor):
|
class LeatherArmor(Armor):
|
||||||
name = "Light leather armor"
|
name = "Leather armor"
|
||||||
cost = "10 gp"
|
cost = "10 gp"
|
||||||
base_armor_class = 11
|
base_armor_class = 11
|
||||||
weight = 10
|
weight = 10
|
||||||
|
|
||||||
|
|
||||||
class LightStuddedArmor(Armor):
|
class StuddedArmor(Armor):
|
||||||
name = "Light studded armor"
|
name = "Studded armor"
|
||||||
cost = "45 gp"
|
cost = "45 gp"
|
||||||
base_armor_class = 12
|
base_armor_class = 12
|
||||||
weight = 13
|
weight = 13
|
||||||
|
|
||||||
|
|
||||||
class MediumHideArmor(Armor):
|
class HideArmor(Armor):
|
||||||
name = "Medium hide armor"
|
name = "Hide armor"
|
||||||
cost = "10 gp"
|
cost = "10 gp"
|
||||||
base_armor_class = 12
|
base_armor_class = 12
|
||||||
dexterity_mod_max = 2
|
dexterity_mod_max = 2
|
||||||
|
|||||||
+110
-7
@@ -5,7 +5,7 @@ import warnings
|
|||||||
|
|
||||||
from .stats import Ability, Skill, findattr
|
from .stats import Ability, Skill, findattr
|
||||||
from .dice import read_dice_str
|
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 .weapons import Weapon
|
||||||
from .armor import Armor, NoArmor, Shield, NoShield
|
from .armor import Armor, NoArmor, Shield, NoShield
|
||||||
|
|
||||||
@@ -86,6 +86,8 @@ 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``"""
|
||||||
@@ -98,6 +100,82 @@ 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)
|
||||||
@@ -126,7 +204,8 @@ class Character():
|
|||||||
try:
|
try:
|
||||||
_spells.append(findattr(spells, spell_name))
|
_spells.append(findattr(spells, spell_name))
|
||||||
except AttributeError:
|
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)
|
warnings.warn(msg)
|
||||||
# Create temporary spell
|
# Create temporary spell
|
||||||
_spells.append(spells.create_spell(name=spell_name, level=9))
|
_spells.append(spells.create_spell(name=spell_name, level=9))
|
||||||
@@ -213,12 +292,12 @@ class Character():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if new_armor not in ('', None):
|
if new_armor not in ('', None):
|
||||||
try:
|
if isinstance(new_armor, armor.Armor):
|
||||||
|
new_armor = new_armor
|
||||||
|
else:
|
||||||
NewArmor = findattr(armor, new_armor)
|
NewArmor = findattr(armor, new_armor)
|
||||||
except AttributeError:
|
new_armor = NewArmor()
|
||||||
# Not a string, so just treat it as Armor
|
self.armor = new_armor
|
||||||
NewArmor = new_armor
|
|
||||||
self.armor = NewArmor()
|
|
||||||
|
|
||||||
def wield_shield(self, shield):
|
def wield_shield(self, shield):
|
||||||
"""Accepts a string or Shield class and replaces the current armor.
|
"""Accepts a string or Shield class and replaces the current armor.
|
||||||
@@ -347,6 +426,8 @@ class Druid(Character):
|
|||||||
class_name = 'Druid'
|
class_name = 'Druid'
|
||||||
hit_dice_faces = 8
|
hit_dice_faces = 8
|
||||||
saving_throw_proficiencies = ('intelligence', 'wisdom')
|
saving_throw_proficiencies = ('intelligence', 'wisdom')
|
||||||
|
spellcasting_ability = 'wisdom'
|
||||||
|
languages = 'Druidic'
|
||||||
_proficiencies_text = (
|
_proficiencies_text = (
|
||||||
'Light armor', 'medium armor',
|
'Light armor', 'medium armor',
|
||||||
'shields (druids will not wear armor or use shields made of metal)',
|
'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)
|
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
|
||||||
class_skill_choices = ('Arcana', 'Animal Handling', 'Insight',
|
class_skill_choices = ('Arcana', 'Animal Handling', 'Insight',
|
||||||
'Medicine', 'Nature', 'Perception', 'Religion', 'Survival')
|
'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):
|
class Fighter(Character):
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ sheet by running ``makesheets`` from the command line.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
dungeonsheets_version = "{{ dungeonsheets_version }}"
|
||||||
|
|
||||||
name = '{{ char.name }}'
|
name = '{{ char.name }}'
|
||||||
character_class = '{{ char.class_name }}'
|
character_class = '{{ char.class_name }}'
|
||||||
player_name = '{{ char.player_name }}'
|
player_name = '{{ char.player_name }}'
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import jinja2
|
|||||||
|
|
||||||
from dungeonsheets import character, race, dice, background
|
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 = {
|
char_classes = {
|
||||||
'Barbarian': character.Barbarian,
|
'Barbarian': character.Barbarian,
|
||||||
'Bard': character.Bard,
|
'Bard': character.Bard,
|
||||||
@@ -69,7 +76,8 @@ class App(npyscreen.NPSAppManaged):
|
|||||||
def save_character(self):
|
def save_character(self):
|
||||||
# Create the template context
|
# Create the template context
|
||||||
context = dict(
|
context = dict(
|
||||||
char=self.character
|
char=self.character,
|
||||||
|
dungeonsheets_version=read_version(),
|
||||||
)
|
)
|
||||||
# Render the template
|
# Render the template
|
||||||
src_path = os.path.dirname(__file__)
|
src_path = os.path.dirname(__file__)
|
||||||
@@ -353,7 +361,7 @@ class BasicInfoForm(npyscreen.ActionForm):
|
|||||||
self.parentApp.setNextForm('CLASS')
|
self.parentApp.setNextForm('CLASS')
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.parentApp.setNextForm(None)
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
class SaveForm(npyscreen.ActionForm):
|
class SaveForm(npyscreen.ActionForm):
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -9,3 +9,6 @@ class LatexError(OSError):
|
|||||||
|
|
||||||
class LatexNotFoundError(LatexError):
|
class LatexNotFoundError(LatexError):
|
||||||
"""PDFLatex did not execute correctly."""
|
"""PDFLatex did not execute correctly."""
|
||||||
|
|
||||||
|
class MonsterError(AttributeError):
|
||||||
|
"""Error retriving or using a D&D Monster."""
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import warnings
|
import warnings
|
||||||
import re
|
import re
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
from fdfgen import forge_fdf
|
from fdfgen import forge_fdf
|
||||||
import pdfrw
|
import pdfrw
|
||||||
|
from jinja2 import Environment, PackageLoader
|
||||||
|
|
||||||
from dungeonsheets import character, exceptions
|
from dungeonsheets import character, exceptions
|
||||||
from dungeonsheets.stats import mod_str
|
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
|
"""Program to take character definitions and build a PDF of the
|
||||||
character sheet."""
|
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_ON = 'Yes'
|
||||||
CHECKBOX_OFF = 'Off'
|
CHECKBOX_OFF = 'Off'
|
||||||
@@ -51,7 +76,7 @@ def load_character_file(filename):
|
|||||||
if ext != '.py':
|
if ext != '.py':
|
||||||
raise ValueError(f"Character definition {filename} is not a python file.")
|
raise ValueError(f"Character definition {filename} is not a python file.")
|
||||||
# Check if this file contains the version string
|
# 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:
|
with open(filename, mode='r') as f:
|
||||||
version = None
|
version = None
|
||||||
for line in f:
|
for line in f:
|
||||||
@@ -62,7 +87,7 @@ def load_character_file(filename):
|
|||||||
if version is None:
|
if version is None:
|
||||||
# Not a valid DND character file
|
# Not a valid DND character file
|
||||||
raise exceptions.CharacterFileFormatError(
|
raise exceptions.CharacterFileFormatError(
|
||||||
"No ``dungeonsheets_version = `` entry.")
|
f"No ``dungeonsheets_version = `` entry in `{filename}`.")
|
||||||
# Import the module to extract the information
|
# Import the module to extract the information
|
||||||
spec = importlib.util.spec_from_file_location('module', filename)
|
spec = importlib.util.spec_from_file_location('module', filename)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
@@ -75,16 +100,17 @@ def load_character_file(filename):
|
|||||||
return char_props
|
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):
|
def create_spellbook_pdf(character, basename):
|
||||||
from jinja2 import Environment, PackageLoader
|
template = jinja_env.get_template('spellbook_template.tex')
|
||||||
env = Environment(
|
return create_latex_pdf(character, basename, template)
|
||||||
loader=PackageLoader('dungeonsheets', ''),
|
|
||||||
block_start_string='[%',
|
|
||||||
block_end_string='%]',
|
def create_latex_pdf(character, basename, template):
|
||||||
variable_start_string='[[',
|
|
||||||
variable_end_string=']]',
|
|
||||||
)
|
|
||||||
template = env.get_template('spellbook_template.tex')
|
|
||||||
tex = template.render(character=character)
|
tex = template.render(character=character)
|
||||||
# Create tex document
|
# Create tex document
|
||||||
tex_file = f'{basename}.tex'
|
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}'
|
fields[dmg_field] = f'{weapon.damage} {weapon.damage_type}'
|
||||||
# Other attack information
|
# Other attack information
|
||||||
attack_str = f'Armor: {character.armor}'
|
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
|
attack_str += character.attacks_and_spellcasting
|
||||||
fields['AttacksSpellcasting'] = text_box(attack_str)
|
fields['AttacksSpellcasting'] = text_box(attack_str)
|
||||||
# Other proficiencies and languages
|
# Other proficiencies and languages
|
||||||
@@ -444,6 +471,16 @@ def make_sheet(character_file, flatten=False):
|
|||||||
f'for {char.name}')
|
f'for {char.name}')
|
||||||
else:
|
else:
|
||||||
sheets.append(spellbook_base + '.pdf')
|
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
|
# Combine sheets into final pdf
|
||||||
final_pdf = os.path.splitext(character_file)[0] + '.pdf'
|
final_pdf = os.path.splitext(character_file)[0] + '.pdf'
|
||||||
merge_pdfs(sheets, final_pdf, clean_up=True)
|
merge_pdfs(sheets, final_pdf, clean_up=True)
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
\definecolor{mygrey}{gray}{0.7}
|
\definecolor{mygrey}{gray}{0.7}
|
||||||
|
|
||||||
\title{Spells and Incantations}
|
\title{Spells and Incantations}
|
||||||
|
|
||||||
\author{[[ character.name ]]}
|
\author{[[ character.name ]]}
|
||||||
|
\date{}
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
|
|
||||||
@@ -29,9 +29,13 @@
|
|||||||
[% else %] %
|
[% else %] %
|
||||||
\textit{[[ spl.magic_school ]] Cantrip} %
|
\textit{[[ spl.magic_school ]] Cantrip} %
|
||||||
[% endif %] %
|
[% endif %] %
|
||||||
[% if spl.ritual %] %
|
[% if spl.ritual and spl.concentration %]%
|
||||||
\textit{(ritual)}
|
(\textit{ritual}, \textit{concentration})%
|
||||||
[% endif %]
|
[% elif spl.ritual %]%
|
||||||
|
(\textit{ritual})%
|
||||||
|
[% elif spl.concentration %]%
|
||||||
|
(\textit{concentration})%
|
||||||
|
[% endif %]%
|
||||||
|
|
||||||
\noindent
|
\noindent
|
||||||
\textbf{Casting Time:} [[ spl.casting_time ]] \\
|
\textbf{Casting Time:} [[ spl.casting_time ]] \\
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
\textbf{Components:} [[ spl.component_string() ]] \\
|
\textbf{Components:} [[ spl.component_string() ]] \\
|
||||||
\textbf{Duration:} [[ spl.duration ]]
|
\textbf{Duration:} [[ spl.duration ]]
|
||||||
|
|
||||||
[[ spl.__doc__ ]]
|
[[ spl.__doc__|rst_to_latex ]]
|
||||||
|
|
||||||
} %\color
|
} %\color
|
||||||
[% endfor %]
|
[% endfor %]
|
||||||
|
|||||||
+107
-9
@@ -1540,6 +1540,35 @@ class Counterspell(Spell):
|
|||||||
classes = ()
|
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):
|
class CreateUndead(Spell):
|
||||||
"""You can cast this spell only at night. Choose up to three corpses
|
"""You can cast this spell only at night. Choose up to three corpses
|
||||||
of Medium or Small humanoids within range. Each corpse becomes a
|
of Medium or Small humanoids within range. Each corpse becomes a
|
||||||
@@ -1585,11 +1614,11 @@ class CreateUndead(Spell):
|
|||||||
|
|
||||||
|
|
||||||
class CureWounds(Spell):
|
class CureWounds(Spell):
|
||||||
"""A creature you touch regains a number of hit points equal to 1d8 +
|
"""A creature you touch regains a number of hit points equal to
|
||||||
your spellcasting ability modifier. This spell has no effect on
|
``1d8`` + your spellcasting ability modifier. This spell has no
|
||||||
undead or constructs. At Higher Levels. When you cast this spell
|
effect on undead or constructs. At Higher Levels. When you cast
|
||||||
using a spell slot of 2nd level or higher, the healing increases
|
this spell using a spell slot of 2nd level or higher, the healing
|
||||||
by 1d8 for each slot level above 1st.
|
increases by ``1d8`` for each slot level above 1st.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = "Cure Wounds"
|
name = "Cure Wounds"
|
||||||
@@ -1725,7 +1754,6 @@ class DetectMagic(Spell):
|
|||||||
classes = ('Bard', 'Cleric', 'Druid', 'Paladin', 'Ranger', 'Sorceror', 'Wizard', )
|
classes = ('Bard', 'Cleric', 'Druid', 'Paladin', 'Ranger', 'Sorceror', 'Wizard', )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DimensionDoor(Spell):
|
class DimensionDoor(Spell):
|
||||||
"""You teleport yourself from your current location to any other spot
|
"""You teleport yourself from your current location to any other spot
|
||||||
within range. You arrive at exactly the spot desired. It can be a
|
within range. You arrive at exactly the spot desired. It can be a
|
||||||
@@ -2061,6 +2089,31 @@ class ElementalWeapon(Spell):
|
|||||||
classes = ('Paladin', )
|
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):
|
class Etherealness(Spell):
|
||||||
"""You step into the border regions of the Ethereal Plane, in the area
|
"""You step into the border regions of the Ethereal Plane, in the area
|
||||||
where it overlaps with your current plane. You remain in the
|
where it overlaps with your current plane. You remain in the
|
||||||
@@ -3529,9 +3582,10 @@ class PhantasmalForce(Spell):
|
|||||||
class PoisonSpray(Spell):
|
class PoisonSpray(Spell):
|
||||||
"""You extend your hand toward a creature you can see within range and
|
"""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
|
project a puff of noxious gas from your palm. The creature must
|
||||||
succeed on a Constitution saving throw or take 1d12 poison
|
succeed on a Constitution saving throw or take ``1d12`` poison
|
||||||
damage. This spell’s damage increases by 1d12 when you reach 5th
|
damage. This spell’s damage increases by ``1d12`` when you reach
|
||||||
level (2d12), 11th level (3d12), and 17th level (4d12).
|
5th level (``2d12``), 11th level (``3d12``), and 17th level
|
||||||
|
(``4d12``).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = "Poison Spray"
|
name = "Poison Spray"
|
||||||
@@ -3888,6 +3942,29 @@ class Sanctuary(Spell):
|
|||||||
classes = ()
|
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):
|
class Shatter(Spell):
|
||||||
"""A sudden loud ringing noise, painfully intense, erupts from a point
|
"""A sudden loud ringing noise, painfully intense, erupts from a point
|
||||||
of your choice within range. Each creature in a 10-foot-radius
|
of your choice within range. Each creature in a 10-foot-radius
|
||||||
@@ -4064,6 +4141,27 @@ class SpareTheDying(Spell):
|
|||||||
classes = ()
|
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):
|
class SpeakWithDead(Spell):
|
||||||
"""You grant the semblance of life and intelligence to a corpse of
|
"""You grant the semblance of life and intelligence to a corpse of
|
||||||
your choice within range, allowing it to answer the questions you
|
your choice within range, allowing it to answer the questions you
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class Ability():
|
|||||||
modifier = math.floor((score - 10) / 2)
|
modifier = math.floor((score - 10) / 2)
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
saving_throw = modifier
|
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)
|
is_proficient = (self.ability_name in character.saving_throw_proficiencies)
|
||||||
if is_proficient:
|
if is_proficient:
|
||||||
saving_throw += character.proficiency_bonus
|
saving_throw += character.proficiency_bonus
|
||||||
|
|||||||
Binary file not shown.
@@ -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
@@ -36,7 +36,7 @@ ep = 50
|
|||||||
gp = 120
|
gp = 120
|
||||||
pp = 0
|
pp = 0
|
||||||
weapons = ('shortsword', 'shortbow')
|
weapons = ('shortsword', 'shortbow')
|
||||||
armor = 'light leather armor'
|
armor = 'leather armor'
|
||||||
shield = 'shield'
|
shield = 'shield'
|
||||||
equipment = (
|
equipment = (
|
||||||
"""Shortsword, shortbow, 20 arrows, leather armor, thieves’ tools,
|
"""Shortsword, shortbow, 20 arrows, leather armor, thieves’ tools,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+64
-6
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from dungeonsheets import race
|
from dungeonsheets import race, monsters, exceptions
|
||||||
from dungeonsheets.character import Character, Wizard
|
from dungeonsheets.character import Character, Wizard, Druid
|
||||||
from dungeonsheets.weapons import Weapon, Shortsword
|
from dungeonsheets.weapons import Weapon, Shortsword
|
||||||
from dungeonsheets.armor import Armor, LightLeatherArmor, Shield
|
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
||||||
|
|
||||||
|
|
||||||
class TestCharacter(TestCase):
|
class TestCharacter(TestCase):
|
||||||
@@ -30,7 +30,7 @@ class TestCharacter(TestCase):
|
|||||||
self.assertEqual(len(char.weapons), 1)
|
self.assertEqual(len(char.weapons), 1)
|
||||||
self.assertTrue(isinstance(char.weapons[0], Shortsword))
|
self.assertTrue(isinstance(char.weapons[0], Shortsword))
|
||||||
# Check that armor and shield gets set_attrs
|
# 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.armor, str))
|
||||||
self.assertFalse(isinstance(char.shield, str))
|
self.assertFalse(isinstance(char.shield, str))
|
||||||
# Check that race gets set to an object
|
# 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=1), 3)
|
||||||
self.assertEqual(char.spell_slots(spell_level=2), 0)
|
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):
|
def test_equip_armor(self):
|
||||||
char = Character(dexterity=16)
|
char = Character(dexterity=16)
|
||||||
char.wear_armor('light leather armor')
|
char.wear_armor('leather armor')
|
||||||
self.assertTrue(isinstance(char.armor, Armor))
|
self.assertTrue(isinstance(char.armor, Armor))
|
||||||
# Now make sure the armor class is correct
|
# Now make sure the armor class is correct
|
||||||
self.assertEqual(char.armor_class, 14)
|
self.assertEqual(char.armor_class, 14)
|
||||||
# Try passing an Armor object directly
|
# Try passing an Armor object directly
|
||||||
char.wear_armor(LightLeatherArmor)
|
char.wear_armor(LeatherArmor())
|
||||||
self.assertEqual(char.armor_class, 14)
|
self.assertEqual(char.armor_class, 14)
|
||||||
# Test equipped armor with max dexterity mod_str
|
# Test equipped armor with max dexterity mod_str
|
||||||
char.armor.dexterity_mod_max = 1
|
char.armor.dexterity_mod_max = 1
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user