Files
dungeon-sheets/dungeonsheets/classes.py
T
2018-12-19 09:34:03 -05:00

477 lines
18 KiB
Python

__all__ = ('Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk',
'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', 'Wizard', )
from .stats import findattr
from . import (weapons, monsters, exceptions)
import math
import warnings
class CharClass():
"""
A generic Character Class (not to be confused with builtin class)
"""
class_name = ""
class_level = 1
hit_dice_faces = None
weapon_proficiencies = ()
_proficiencies_text = ()
multiclass_weapon_proficiencies = ()
_multiclass_proficiencies_text = ()
languages = ()
class_skill_choices = ()
num_skill_choices = 2
spellcasting_ability = None
spell_slots_by_level = None
subclass = None
class_features_by_level = {lvl: () for lvl in range(1, 21)}
def __init__(self, level, subclass=None, **params):
self.class_level = level
self.subclass = subclass
for k, v in params:
setattr(self, k, v)
@property
def class_features(self):
features = ()
for lvl in range(1, self.class_level+1):
features += tuple(self.class_features_by_level[lvl])
if self.subclass is not None:
features += tuple(self.subclass.features_by_level[lvl])
return features
@property
def is_spellcaster(self):
result = (self.spellcasting_ability is not None)
return result
def spell_slots(self, spell_level):
"""How many spells slots are available for this spell level."""
if self.spell_slots_by_level is None:
return 0
else:
return self.spell_slots_by_level[self.class_level][spell_level]
class Barbarian(CharClass):
class_name = 'Barbarian'
hit_dice_faces = 12
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'simple weapons', 'martial weapons')
weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons)
class_skill_choices = ('Animal Handling', 'Athletics',
'Intimidation', 'Nature', 'Perception', 'Survival')
class Bard(CharClass):
class_name = 'Bard'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'charisma')
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
class_skill_choices = ('Acrobatics', 'Animal Handling', 'Arcana',
'Athletics', 'Deception', 'History', 'Insight',
'Intimidation', 'Investigation', 'Medicine',
'Nature', 'Perception', 'Performance', 'Persuasion',
'Religion', 'Sleight of Hand', 'Stealth',
'Survival')
num_skill_choices = 3
spellcasting_ability = 'charisma'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
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: (4, 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 Cleric(CharClass):
class_name = 'Cleric'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficiencies = weapons.simple_weapons
class_skill_choices = ('History', 'Insight', 'Medicine',
'Persuasion', 'Religion')
spellcasting_ability = 'wisdom'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (3, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (4, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (4, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (5, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (5, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (5, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (5, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (5, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
class Druid(CharClass):
class_name = 'Druid'
circle = "" # Moon, land
_wild_shapes = ()
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)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficiencies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace,
weapons.Quarterstaff, 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),
}
@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.class_level < 2:
max_cr = -1
max_swim = 0
max_fly = 0
elif self.class_level < 4:
max_cr = 1/4
max_swim = 0
max_fly = 0
elif self.class_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 circle druids
if self.circle.lower() == "moon":
if 2 <= self.class_level < 6:
max_cr = 1
elif self.class_level >= 6:
max_cr = math.floor(self.class_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(CharClass):
class_name = 'Fighter'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Acrobatics', 'Animal Handling',
'Athletics', 'History', 'Insight', 'Intimidation',
'Perception', 'Survival')
class Monk(CharClass):
class_name = 'Monk'
hit_dice_faces = 8
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficiencies = (weapons.Shortsword,) + weapons.simple_weapons
class_skill_choices = ('Acrobatics', 'Athletics', 'History', 'Insight',
'Religion', 'Stealth')
class Paladin(CharClass):
class_name = 'Paladin'
hit_dice_faces = 10
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ("Athletics", 'Insight', 'Intimidation',
'Medicine', 'Persuasion', 'Religion')
spellcasting_ability = 'charisma'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
2: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
4: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
5: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
6: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
7: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
8: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
9: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
10: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
11: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
13: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
14: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
15: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
16: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
17: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
18: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
19: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
}
class Ranger(CharClass):
class_name = 'Ranger'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Animal Handling', 'Athletics', 'Insight',
'Investigation', 'Nature', 'Perception', 'Stealth',
'Survival')
num_skill_choices = 3
spellcasting_ability = 'wisdom'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
2: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
4: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
5: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
6: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
7: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
8: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
9: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
10: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
11: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
13: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
14: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
15: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
16: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
17: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
18: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
19: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
}
class Rogue(CharClass):
class_name = 'Rogue'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'intelligence')
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficiencies = weapons.simple_weapons + (
weapons.HandCrossbow, weapons.Longsword, weapons.Rapier,
weapons.Shortsword)
class_skill_choices = ('Acrobatics', 'Athletics', 'Deception',
'Insight', 'Intimidation', 'Investigation',
'Perception', 'Performance', 'Persuasion',
'Sleight of Hand', 'Stealth')
class Sorceror(CharClass):
class_name = 'Sorceror'
hit_dice_faces = 6
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'Deception', 'Insight',
'Intimidation', 'Persuasion', 'Religion')
class Warlock(CharClass):
class_name = 'Warlock'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
class_skill_choices = ('Arcana', 'Deception', 'History',
'Intimidation', 'Investigation', 'Nature',
'Religion')
weapon_proficiencies = weapons.simple_weapons
spellcasting_ability = 'charisma'
spell_slots_by_level = {
1: (2, 1, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 0, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 0, 2, 0, 0, 0, 0, 0, 0, 0),
5: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
6: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
7: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
8: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
9: (3, 0, 0, 0, 0, 2, 0, 0, 0, 0),
10: (4, 0, 0, 0, 0, 2, 0, 0, 0, 0),
11: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
12: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
13: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
14: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
15: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
16: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
17: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
18: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
19: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
20: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
}
class Wizard(CharClass):
class_name = 'Wizard'
hit_dice_faces = 6
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'History', 'Investigation',
'Medicine', 'Religion')
spellcasting_ability = 'intelligence'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (3, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (4, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (4, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (5, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (5, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (5, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (5, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (5, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}