mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 05:03: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
|
||||
|
||||
|
||||
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
@@ -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):
|
||||
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""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 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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
\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
@@ -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 spell’s 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 spell’s 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
|
||||
|
||||
@@ -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.
@@ -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
|
||||
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
@@ -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
|
||||
|
||||
@@ -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