__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 _proficiencies_text = () weapon_proficiencies = () multiclass_weapon_proficiencies = () languages = () class_skill_choices = () num_skill_choices = 2 spellcasing_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 cirlce 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), }