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