Source code for DnD_5e.combatant.spellcaster

import DnD_5e.attack_class.spell_attacks
from DnD_5e.combatant import Combatant


[docs] class SpellCaster(Combatant): """ A Combatant that can cast spells """ def __init__(self, **kwargs): """ Validate the input and set the instance variables :param kwargs: keyword arguments. Uses all keyword arguments from superclass, plus these: :param spell_ability: the ability used for spellcasting :type spell_ability: str :param spell_slots: how many slots *self* has for each spell level :type spell_slots: dict that maps int (spell level) to int (spell slots) :param spells: the Spells that *self* knows how to cast :type spells: list or tuple of Spells :raise: ValueError if input is invalid """ super().__init__(**kwargs) spell_ability = kwargs.get("spell_ability", "wisdom") try: self._spell_ability_mod = self.get_ability(spell_ability) self._spell_ability = spell_ability except ValueError: self.get_logger().error("Spell ability must be strength, dexterity, constitution, " "intelligence, wisdom, or charisma", stack_info=True) raise ValueError( "Spell ability must be strength, dexterity, constitution, intelligence, wisdom, or charisma") self._spell_save_dc = 8 + self._proficiency_mod + self._spell_ability_mod self._spell_attack_mod = self._proficiency_mod + self._spell_ability_mod spell_slots = kwargs.get("spell_slots") if isinstance(spell_slots, dict): for key in spell_slots: if key not in list(range(1, 10)) or not (isinstance(spell_slots[key], int) and spell_slots[key] > -1): self.get_logger().error("Spell slots must be a dictionary mapping " "spell levels (1-20) to slots (0 or more)", stack_info=True) raise ValueError( "Spell slots must be a dictionary mapping spell levels (1-20) to slots (0 or more)") elif not spell_slots: for i in range(1, 10): value = kwargs.get(f"level_{i}") if value: if isinstance(value, int): spell_slots[i] = value else: self.get_logger().error("Spell slot number for level %d must be an integer", i, stack_info=True) raise ValueError(f"Spell slot number for level {i} must be an integer") else: self.get_logger().error("Spell slots must be a dictionary. {1: 3, 2:1} " "would mean 3 1st level spells and 1 2nd level spell", stack_info=True) raise ValueError( "Spell slots must be a dictionary. {1: 3, 2:1} would mean 3 1st level spells and 1 2nd level spell") self._spell_slots = spell_slots self._full_spell_slots = {} self._full_spell_slots.update(self._spell_slots) self._spells = [] spells = kwargs.get("spells") if not spells: self.get_logger().warning("Created a SpellCaster with no spells") elif not isinstance(spells, (list, tuple)): self.get_logger().error("Spells must be a list or tuple", stack_info=True) raise ValueError("Spells must be a list or tuple") else: for spell in spells: try: self.add_spell(spell) except ValueError: self.get_logger().error("spells must contain only Spells", stack_info=True) raise ValueError("spells must contain only Spells") def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes: :py:attr:`spell_ability`, :py:attr:`spell_ability_mod`, :py:attr:`spell_save_dc`, :py:attr:`spell_attack_mod`, :py:attr:`spells`, and :py:attr:`full_spell_slots` :param other: the SpellCaster to compare :type other: SpellCaster :return: True if *self* equals *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_spell_ability() == other.get_spell_ability() \ and self.get_spell_ability_mod() == other.get_spell_ability_mod() \ and self.get_spell_save_dc() == other.get_spell_save_dc() \ and self.get_spell_attack_mod() == other.get_spell_attack_mod() \ and self.get_spells() == other.get_spells() \ and self.get_full_spell_slots() == other.get_full_spell_slots()
[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: :py:attr:`spell_slots` :param other: the SpellCaster to compare :type other: SpellCaster :return: True if *self* is identical to *other*, False otherwise :rtype: bool """ return super().current_eq(other) and self.get_spell_slots() == other.get_spell_slots()
[docs] def get_spell_ability(self) -> str: """ :return: spell ability :rtype: str """ return self._spell_ability
[docs] def get_spell_ability_mod(self) -> int: """ :return: spell ability mod :rtype: int """ return self._spell_ability_mod
[docs] def get_spell_save_dc(self) -> int: """ :return: spell save dc :rtype: int """ return self._spell_save_dc
[docs] def get_spell_attack_mod(self) -> int: """ :return: spell attack mod :rtype: int """ return self._spell_attack_mod
[docs] def get_spell_slots(self) -> dict: """ :return: spell slots :rtype: dict mapping int to int """ return self._spell_slots
[docs] def get_full_spell_slots(self) -> dict: """ :return: full/maximum spell slots :rtype: dict mapping int to int """ return self._full_spell_slots
[docs] def get_level_spell_slots(self, level: int) -> int: """ Determine the number of spell slots available for a given level :param level: the spell level to look at :type level: integer from 1 to 9 (inclusive) :return: the number of available spell slots :rtype: int :raise: ValueError if *level* is invalid """ if not isinstance(level, int): raise ValueError("Level provided must be an integer") try: return self._spell_slots[level] except KeyError: return 0
[docs] def get_spells(self) -> list: """ :return: spells :rtype: list of Spells """ return self._spells
[docs] def can_cast(self, spell: DnD_5e.attack_class.spell_attacks.Spell) -> bool: """ Determine if *self* can cast the given Spell. Note: does not check spell slots. :param spell: the Spell *self* is trying to cast :type spell: py:class:`Spell` :return: True if *self* can cast *spell*, False otherwise :rtype: bool """ # TODO: check conditions that would impair verbal, somatic, or material components result = self.get_armor() is None or self.has_armor_proficiency(self.get_armor()) if self.has_feature_method("can_cast"): result = result and self.get_feature_dict()["can_cast"](self, spell) return result
[docs] def add_spell(self, spell: DnD_5e.attack_class.spell_attacks.Spell): """ Add a given spell :param spell: the Spell to add :type spell: Spell :return: None :raise: ValueError if *spell* is not a Spell """ if isinstance(spell, DnD_5e.attack_class.spell_attacks.Spell): if isinstance(spell, DnD_5e.attack_class.spell_attacks.HealingSpell): spell.set_damage_mod(self._spell_ability_mod) spell.set_attack_mod(self._spell_attack_mod) self._spells.append(spell) self.get_logger().info("%s adds spell %s", self.get_name(), spell.get_name()) else: self.get_logger().error("Cannot add a non-Spell object as an attack.", stack_info=True) raise ValueError("Cannot add a non-Spell object as an attack.")
[docs] def spend_spell_slot(self, level: int, spell=None): # pylint: disable=unused-argument """ Spend a spell slot of the given level :param level: the spell level :type level: an integer from 0 to 9 (inclusive) :param spell: the spell that is being used :type spell: Spell :return: None :raise: ValueError if *self* doesn't have any spell slots of *level* level """ if level == 0: return # don't spend a slot if not isinstance(level, int) or not (0 < level < 10): # pylint: disable=superfluous-parens self.get_logger().error("level must be an integer between 1 and 9", stack_info=True) raise ValueError("level must be an integer between 1 and 9") if self.get_level_spell_slots(level) > 0: self._spell_slots[level] -= 1 else: self.get_logger().error("Tried to use a level %d spell slot that you don't have", level, stack_info=True) raise ValueError("Tried to use a level %d spell slot that you don't have" % level)
[docs] def reset_spell_slots(self): """ Set spell slots to the maximum/full amount :return: None """ self.get_logger().info("%s resetting spell slots", self.get_name()) spell_slots = {} spell_slots.update(self.get_full_spell_slots()) self._spell_slots = spell_slots # pylint: disable=protected-access self.get_logger().info("%s resets spell slots", self.get_name())