Source code for DnD_5e.character_classes.paladin

from DnD_5e.combatant import Combatant
from DnD_5e.combatant.character import Character
from DnD_5e.combatant.spellcaster import SpellCaster
from DnD_5e.utility_methods_dnd import ability_to_mod, proficiency_bonus_per_level, roll_dice


[docs] class Paladin(SpellCaster, Character): """ Paladin character class """ def __init__(self, **kwargs): """ Validate the input and set the instance variables :param kwargs: keyword arguments. Some of the keyword arguments are overridden by this class. :param name: what *self* is called. A unique name is recommended but not required :type name: str :param vulnerabilities: all the damage types that *self* is vulnerable to :type vulnerabilities: set, list, or tuple of strings (will be converted to set of strings) :param resistances: all the damage types that *self* is resistant to :type resistances: set, list, or tuple of strings (will be converted to set of strings) :param immunities: all the damage types that *self* is immune to :type immunities: set, list, or tuple of strings (will be converted to set of strings) :param ac: *self's* armor class :type ac: positive integer :param temp_hp: temporary hit points :type temp_hp: non-negative integer :param conditions: all conditions currently affecting *self* :type conditions: list of strings :param strength: strength score. Will be converted to modifier and stored as such. :type strength: integer between 1 and 30 (inclusive) :param strength_mod: dexterity modifier :type strength_mod: int :param dexterity: dexterity score. Will be converted to modifier and stored as such. :type dexterity: integer between 1 and 30 (inclusive) :param dexterity_mod: dexterity modifier :type dexterity_mod: int :param constitution: constitution score. Will be converted to modifier and stored as such. :type constitution: integer between 1 and 30 (inclusive) :param constitution_mod: constitution modifier :type constitution_mod: int :param intelligence: intelligence score. Will be converted to modifier and stored as such. :type intelligence: integer between 1 and 30 (inclusive) :param intelligence_mod: intelligence modifier :type intelligence_mod: int :param wisdom: wisdom score. Will be converted to modifier and stored as such. :type wisdom: integer between 1 and 30 (inclusive) :param wisdom_mod: wisdom modifier :type wisdom_mod: int :param charisma: charisma score. Will be converted to modifier and stored as such. :type charisma: integer between 1 and 30 (inclusive) :param charisma_mod: charisma modifier :type charisma_mod: int :param death_saves: NOT IMPLEMENTED YET :param attacks: NOT IMPLEMENTED YET :param weapons: Weapons (see weapons module) that *self* has available to use :type weapons: list of Weapons :param size: size :type size: one of these strings: "tiny", "small", "medium", "large", "huge", "gargantuan" :param items: NOT IMPLEMENTED YET :param level: character level :type level: integer between 1 and 20 (inclusive) :param fighting_style: the fighting style *self* knows :type fighting_style: str :raise: ValueError if input is invalid """ level = kwargs.get("level") if not level: raise ValueError("No level provided or level is 0") if not isinstance(level, int) or level < 1 or level > 20: raise ValueError("Level must be an integer between 1 and 20") hit_dice = (1 * level, 10) constitution = kwargs.get('constitution') if not constitution: constitution_mod = kwargs.get("constitution_mod") if constitution_mod is not None: if isinstance(constitution_mod, int): self._constitution = constitution_mod else: raise ValueError("Constitution mod must be an integer") else: raise ValueError("Must provide constitution score or modifier") else: # pragma: no cover constitution_mod = ability_to_mod(constitution) max_hp = kwargs.get("max_hp") if max_hp and (not isinstance(max_hp, int) or max_hp <= 0): raise ValueError("Must provide positive integer max hp") max_hp = 10 + constitution_mod if level > 1: max_hp += (6 + constitution_mod) * (level - 1) proficiencies = kwargs.get('proficiencies') if isinstance(proficiencies, (tuple, list, set)): proficiencies = set(proficiencies) elif proficiencies is None: # pragma: no cover proficiencies = set() else: # pragma: no cover raise ValueError("Proficiencies must be provided as a set, list, or tuple") proficiencies.add("simple weapons") proficiencies.add("martial weapons") proficiencies.add("wisdom") proficiencies.add("charisma") proficiency_mod = proficiency_bonus_per_level(level) spell_slots = {1: 0} if level > 1: spell_slots = {1: 2} if level > 2: spell_slots.update({1: 3}) if level > 4: spell_slots.update({1: 4, 2: 2}) if level > 6: spell_slots.update({2: 3}) if level > 8: spell_slots.update({3: 2}) if level > 9: spell_slots.update({3: 3}) if level > 12: spell_slots.update({4: 1}) if level > 14: spell_slots.update({4: 2}) if level > 16: spell_slots.update({4: 3, 5: 1}) if level > 18: spell_slots.update({5: 2}) kwargs.update({"max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod, "hit_dice": hit_dice, "spell_ability": "charisma", "spell_slots": spell_slots}) # include empty spell slots so that SpellCaster init doesn't throw a fit super().__init__(**kwargs) self.add_feature("divine sense") self._divine_sense_slots = 1 + self.get_charisma() self.add_feature("lay on hands") self._lay_on_hands_pool = self.get_level() * 5 if self.get_level() > 1: self.add_feature("fighting style") fighting_style = kwargs.get("fighting_style") self.add_fighting_style(fighting_style) self.add_feature("divine smite") if level > 2: self.add_feature("divine health") self.add_feature("sacred oath") if level > 4: self.add_feature("extra attack") if level > 5: self.add_feature("aura of protection") self._aura = 10 if level > 9: self.add_feature("aura of courage") if level > 10: self.add_feature("improved divine smite") if level > 13: self.add_feature("cleansing touch") self._cleansing_touch_slots = max(self.get_charisma(), 1) if level > 17: self._aura = 30 def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes: aura :param other: the Paladin to compare :type other: Paladin :return: True if *self* equals *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_aura() == other.get_aura()
[docs] def current_eq(self, other) -> bool: """ Compare *self* and *other* to determine if they are identical based on the attributes checked in *equals* and also these attributes: divine sense slots, lay on hands pool, cleansing touch slots :param other: the Paladin to compare :type other: Paladin :return: True if *self* is identical to *other*, False otherwise :rtype: bool """ return super().current_eq(other) \ and self.get_divine_sense_slots() == other.get_divine_sense_slots() \ and self.get_lay_on_hands_pool() == other.get_lay_on_hands_pool() \ and self.get_cleansing_touch_slots() == other.get_cleansing_touch_slots()
[docs] def get_divine_sense_slots(self) -> int: """ :return: divine sense slots :rtype: non-negative integer """ return self._divine_sense_slots
[docs] def get_lay_on_hands_pool(self) -> int: """ :return: the hit points in the lay on hands pool :rtype: non-negative integer """ return self._lay_on_hands_pool
[docs] def get_aura(self) -> int: """ :return: the number of feet for the aura features :rtype: non-negative integer """ try: return self._aura except AttributeError: return 0
[docs] def get_cleansing_touch_slots(self) -> int: """ :return: cleansing touch slots :rtype: non-negative integer """ try: return self._cleansing_touch_slots except AttributeError: return 0
[docs] def spend_divine_sense_slot(self): """ Spend a divine sense slot :return: None :raise: ValueError if *self* has no divine sense slots left """ if self.get_divine_sense_slots(): self._divine_sense_slots -= 1 else: self.get_logger().error("You have no slots left to cast divine sense", stack_info=True) raise ValueError("You have no slots left to cast divine sense")
[docs] def spend_cleansing_touch_slot(self): """ Spend a cleansing touch slot :return: None :raise: ValueError if *self* has no cleansing touch slots left """ if not self.get_cleansing_touch_slots(): self.get_logger().error("You don't have any cleansing touch slots left", stack_info=True) raise ValueError("You don't have any cleansing touch slots left") self._cleansing_touch_slots -= 1
[docs] def send_lay_on_hands(self, hp: int, target=None, use="healing"): # pylint: disable=inconsistent-return-statements """ Use the Lay on Hands feature :param hp: the number of hit points to take from the Lay on Hands pool :type hp: non-negative integer :param target: the Combatant to use Lay on Hands on :type target: Combatant :param use: what to use Lay on Hands for :type use: str :return: the hit points healed for, if *use* is "healing" :raise: ValueError if *self* doesn't have enough Lay on Hands points """ if self.get_lay_on_hands_pool() >= hp: self._lay_on_hands_pool -= hp if use == "healing" and isinstance(target, Combatant): self.get_logger().info("%s uses Lay on Hands to heal %s", self.get_name(), target.get_name()) return target.take_healing(hp) else: self.get_logger().error("You don't have enough lay on hands points left for that", stack_info=True) raise ValueError("You don't have enough lay on hands points left for that")
[docs] def send_divine_smite(self, target: Combatant, level=1) -> int: """ Use Divine Smite on a specified target :param target: the Combatant to use Divine Smite on :type target: Combatant :param level: the level to cast Divine Smite at :type level: integer between 1 and 9 (inclusive) :return: the damage taken :rtype: non-negative integer :raise: ValueError if *self* doesn't have the Divine Smite feature """ if not self.has_feature("divine smite"): self.get_logger().error("You don't have the divine smite feature", stack_info=True) raise ValueError("You don't have the divine smite feature") self.spend_spell_slot(level) dice_num = max(2 + level-1, 5) dice_type = 8 damage = roll_dice(dice_type, num=dice_num)[0] # TODO: use Dice self.get_logger().info("%s uses Divine Smite on %s", self.get_name(), target.get_name()) return target.take_damage(damage, damage_type="radiant")