Added feature enhancements for Druid's

- Circle now properly reflects the druid's available wild_shapes
- Unavaiable wild_shapes are not listed on the sheet but ghosted.
- ``spells`` is now longer relevant, only use ``spells_prepared`` in
  the character file.
This commit is contained in:
Mark Wolfman
2018-10-31 18:16:35 -05:00
parent d2c24cfb2a
commit f10867719d
13 changed files with 370 additions and 128 deletions
+96 -78
View File
@@ -2,6 +2,7 @@
import re
import warnings
import math
from .stats import Ability, Skill, findattr
from .dice import read_dice_str
@@ -86,8 +87,6 @@ class Character():
spellcasting_ability = None
spells = tuple()
spells_prepared = tuple()
# Druid wilf shape transofmration options
_wild_shapes = ()
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
@@ -100,82 +99,6 @@ class Character():
def __repr__(self):
return f"<{self.class_name}: {self.name}>"
@property
def all_wild_shapes(self):
"""Return all wild shapes, regardless of validity."""
return self._wild_shapes
@property
def wild_shapes(self):
"""Return a list of valid wild shapes for this Druid."""
valid_shapes = []
for shape in self._wild_shapes:
# Check if shape can be transformed into
if self.can_assume_shape(shape):
valid_shapes.append(shape)
return valid_shapes
@wild_shapes.setter
def wild_shapes(self, new_shapes):
actual_shapes = []
# Retrieve the actual monster classes if possible
for shape in new_shapes:
if isinstance(shape, monsters.Monster):
# Already a monster shape so just add it as is
new_shape = shape
else:
# Not already a monster so see if we can find one
try:
NewMonster = findattr(monsters, shape)
new_shape = NewMonster()
except AttributeError:
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``'
raise exceptions.MonsterError(msg)
actual_shapes.append(new_shape)
# Save the updated list for later
self._wild_shapes = actual_shapes
def can_assume_shape(self, shape: monsters.Monster)-> bool:
"""Determine if a given shape meets the requirements for transforming.
See Pg 66 of player's handbook.
Parameters
==========
shape
A monster that the Druid wishes to transform into.
Returns
=======
can_assume
True if the monster meets the C/R, swim and flying speed
restrictions.
"""
# Determine acceptable states based on druid level
if self.level < 2:
max_cr = -1
max_swim = 0
max_fly = 0
elif self.level < 4:
max_cr = 1/4
max_swim = 0
max_fly = 0
elif self.level < 8:
max_cr = 1/2
max_swim = None
max_fly = 0
else:
max_cr = None
max_swim = None
max_fly = None
# Check if the beast shape can be assumed
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr)
valid_swim = (max_swim is None or shape.swim_speed <= max_swim)
valid_fly = (max_fly is None or shape.fly_speed <= max_fly)
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly
return can_assume
@property
def speed(self):
return getattr(self.race, 'speed', 30)
@@ -424,6 +347,8 @@ class Cleric(Character):
class Druid(Character):
class_name = 'Druid'
circle = "" # Moon, land
_wild_shapes = ()
hit_dice_faces = 8
saving_throw_proficiencies = ('intelligence', 'wisdom')
spellcasting_ability = 'wisdom'
@@ -460,6 +385,99 @@ class Druid(Character):
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
@property
def all_wild_shapes(self):
"""Return all wild shapes, regardless of validity."""
return self._wild_shapes
@property
def wild_shapes(self):
"""Return a list of valid wild shapes for this Druid."""
valid_shapes = []
for shape in self._wild_shapes:
# Check if shape can be transformed into
if self.can_assume_shape(shape):
valid_shapes.append(shape)
return valid_shapes
@wild_shapes.setter
def wild_shapes(self, new_shapes):
actual_shapes = []
# Retrieve the actual monster classes if possible
for shape in new_shapes:
if isinstance(shape, monsters.Monster):
# Already a monster shape so just add it as is
new_shape = shape
else:
# Not already a monster so see if we can find one
try:
NewMonster = findattr(monsters, shape)
new_shape = NewMonster()
except AttributeError:
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``'
raise exceptions.MonsterError(msg)
actual_shapes.append(new_shape)
# Save the updated list for later
self._wild_shapes = actual_shapes
def can_assume_shape(self, shape: monsters.Monster)-> bool:
"""Determine if a given shape meets the requirements for transforming.
See Pg 66 of player's handbook.
Parameters
==========
shape
A monster that the Druid wishes to transform into.
Returns
=======
can_assume
True if the monster meets the C/R, swim and flying speed
restrictions.
"""
# Determine acceptable states based on druid level
if self.level < 2:
max_cr = -1
max_swim = 0
max_fly = 0
elif self.level < 4:
max_cr = 1/4
max_swim = 0
max_fly = 0
elif self.level < 8:
max_cr = 1/2
max_swim = None
max_fly = 0
else:
max_cr = 1
max_swim = None
max_fly = None
# Make adjustments for moon cirlce druids
if self.circle.lower() == "moon":
if 2 <= self.level < 6:
max_cr = 1
elif self.level >= 6:
max_cr = math.floor(self.level / 3)
# Check if the beast shape can be assumed
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr)
valid_swim = (max_swim is None or shape.swim_speed <= max_swim)
valid_fly = (max_fly is None or shape.fly_speed <= max_fly)
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly
return can_assume
@property
def spells(self):
return tuple(S() for S in self.spells_prepared)
@spells.setter
def spells(self, val):
if len(val) > 0:
warnings.warn("Druids cannot learn spells, "
"use ``spells_prepared`` instead.",
RuntimeWarning)
class Fighter(Character):
+12 -2
View File
@@ -26,9 +26,17 @@
]
[% for shape in character.wild_shapes|sort(attribute='challenge_rating') %]
[% for shape in character.all_wild_shapes|sort(attribute='challenge_rating') %]
[% if not character.can_assume_shape(shape) %]
{\color{mygrey}
[% else %]
{
[% endif %]
\section*{[[ shape.name ]]}
[% if shape.description %]
\subsection*{[[ shape.description ]]}
[% endif %]
\begin{tabular}{c | c | c}
Armor Class & Hit Points & Speed \\
@@ -57,15 +65,17 @@
\vspace{0.2cm}
\begin{tabular}{l l}
\begin{tabular}{p{0.1\textwidth} p{0.32\textwidth}}
\textbf{Skills:} & [[ shape.skills ]] \\
\textbf{Senses:} & [[ shape.senses ]] \\
\textbf{Languages:} & [[ shape.languages ]] \\
\end{tabular}
\vspace{0.2cm}
[[ shape.__doc__ | rst_to_latex ]]
} %\color
[% endfor %]
\end{document}
+12 -6
View File
@@ -24,13 +24,19 @@ character sheet."""
bold_re = re.compile(r'\*\*([^*]+)\*\*')
it_re = re.compile(r'\*([^*]+)\*')
tt_re = re.compile(r'``([^`]+)``')
# A dice string, with optinal backticks: ``1d6 + 3``
dice_re = re.compile(r'`*(\d+d\d+(?:\s*\+\s*\d+)?)`*')
def rst_to_latex(rst):
"""Basic markup of RST to LaTeX code."""
tex = rst
tex = bold_re.sub(r'\\textbf{\1}', tex)
tex = it_re.sub(r'\\textit{\1}', tex)
tex = tt_re.sub(r'\\texttt{\1}', tex)
if rst is None:
tex = ""
else:
tex = rst
tex = bold_re.sub(r'\\textbf{\1}', tex)
tex = it_re.sub(r'\\textit{\1}', tex)
tex = dice_re.sub(r'\\texttt{\1}', tex)
tex = tt_re.sub(r'\\texttt{\1}', tex)
return tex
@@ -437,7 +443,6 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
def make_sheet(character_file, flatten=False):
"""Prepare a PDF character sheet from the given character file.
Parameters
@@ -472,7 +477,8 @@ def make_sheet(character_file, flatten=False):
else:
sheets.append(spellbook_base + '.pdf')
# Create a list of Druid wild_shapes
if len(char.wild_shapes) > 0:
wild_shapes = getattr(char, 'wild_shapes', [])
if len(wild_shapes) > 0:
shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes'
try:
create_druid_shapes_pdf(character=char, basename=shapes_base)
+122 -3
View File
@@ -12,6 +12,8 @@ class Monster():
challenge_rating = 0
armor_class = 0
skills = "Perception +3, Stealth +4"
senses = ""
languages = ""
strength = Ability()
dexterity = Ability()
constitution = Ability()
@@ -30,12 +32,129 @@ class Monster():
return is_beast
class Crocodile(Monster):
name = "Crocodile"
class Ankylosaurus(Monster):
"""Thick armor plating covers the body of the plant-eating dinosaur
ankylosaurus, which defends itself against predators with a
knobbed tail that delivers a devastating strike.
**Tail:** *Melee Weapon Attack:* +7 to hit, reach 10 ft., one
target. *Hit:* 18 (4d6+4) bludgeoning damage. If the target is a
creature, it must succeed on a DC 14 Strength saving throw or be
knocked prone.
"""
name = "Ankylosaurus"
description = "Huge beast, unaligned"
challenge_rating = 3
armor_class = 15
skills = ""
senses = "Passive perception 11"
strength = Ability(19)
dexterity = Ability(11)
constitution = Ability(15)
intelligence = Ability(2)
wisdom = Ability(12)
charisma = Ability(5)
speed = 30
swim_speed = 0
fly_speed = 0
hp_max = 68
hit_dice = '8d12+16'
class Ape(Monster):
"""**Multiattack:** The ape makes two fist attacks.
**Fist:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
target. *Hit:* 6 (1d6+3) bludgeoning damage.
**Rock:** *Ranged Weapon Attack:* +5 to hit, range 25/50 ft., one
target. *Hit:* 6 (1d6+3) bludgeoning damage.
"""
name = "Ape"
description = "Medium beast, unaligned"
challenge_rating = 1 / 2
armor_class = 12
skills = "Athletics +5, Perception +3"
senses = "Passive perception 13"
strength = Ability(16)
dexterity = Ability(14)
constitution = Ability(14)
intelligence = Ability(6)
wisdom = Ability(12)
charisma = Ability(7)
speed = 30
swim_speed = 0
fly_speed = 0
hp_max = 19
hit_dice = '3d8+6'
class Crocodile(Monster):
"""**Hold Breath:** The crocodile can hold its breath for 15 minutes.
**Bite:** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one
creature. *Hit:* 7 (1d10+2) piercing damage, and the target is
Grappled (escape DC 12). Until this grapple ends, the target is
Restrained, and the crocodile can't bite another target.
"""
name = "Crocodile"
description = "Large beast, unaligned"
challenge_rating = 1/2
armor_class = 12
skills = "Stealth +2"
senses = "Passive perception 10"
strength = Ability(15)
dexterity = Ability(10)
constitution = Ability(13)
intelligence = Ability(2)
wisdom = Ability(10)
charisma = Ability(5)
speed = 30
swim_speed = 30
fly_speed = 0
hp_max = 19
hit_dice = '3d10+3'
class GiantEagle(Monster):
"""A giant eagle is a noble creature that speaks its own language and
understands Speech in the Common tongue. A mated pair of giant
eagles typically has up to four eggs or young in their nest (treat
the young as normal eagles).
**Keen Sight:** The eagle has advantage on Wisdom (Perception)
checks that rely on sight.
**Multiattack:** The eagle makes two attacks: one with its beak
and one with its talons.
**Beak:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
target. *Hit:* 6 (1d6 + 3) piercing damage.
**Talons:** *Melee Weapon Attack:* +5 to hit, reach 5 ft., one
target. *Hit:* 10 (2d6 + 3) slashing damage.
"""
name = "Giant eagle"
description = "Large beast, neutral good"
challenge_rating = 1
armor_class = 13
skills = "Perception +4"
senses = "Passive perception 14"
languages = "Giant Eagle, understands common and Auran but can't speak."
strength = Ability(16)
dexterity = Ability(17)
constitution = Ability(13)
intelligence = Ability(8)
wisdom = Ability(14)
charisma = Ability(10)
speed = 10
swim_speed = 0
fly_speed = 80
hp_max = 26
hit_dice = '4d10+4'
class Spider(Monster):
@@ -105,7 +224,7 @@ class Wolf(Monster):
the creature and the ally isn't incapacitated. Actions
**Bite.** *Melee Weapon Attack:* +4 to hit, reach 5 ft., one
target. *Hit:* (2d4 + 2) piercing damage. If the target is a
target. *Hit:* (2d4+2) piercing damage. If the target is a
creature, it must succeed on a DC 11 Strength saving throw or be
knocked prone