Source code for DnD_5e.attack_class.spell_attacks

from DnD_5e.attack_class import Attack
from DnD_5e.attack_class.saving_throw_attacks import SavingThrowAttack
from DnD_5e.utility_methods_dnd import time_to_rounds


[docs] class Spell(Attack): """ This class represents spells """ def __init__(self, **kwargs): """ Validate the input and set the instance variables :param kwargs: keyword arguments. Valid arguments are as follows: :param level: the level of the spell :type level: integer between 1 and 9 (inclusive) :param school: the school of magic that *self* belongs to (currently not typechecked) :param casting_time: the time it takes to cast *self* :type casting_time: a positive integer for the number of rounds, or one of these strings: "1 action", "1 bonus action", "1 reaction" :param components: the components ("v" for verbal, "s" for somatic, "m" for material) needed to cast *self* :type components: list or tuple containing one or more of the aforementioned components :param duration: how long the spell lasts (in rounds) :type duration: non-negative integer :param ritual: whether or not the spell can be cast as a ritual :type ritual: bool :param name: what *self* is called. A unique name is recommended but not required :type name: str :param damage_dice: the dice to determine damage points :type damage_dice: TYPE_DICE_TUPLE :param attack_mod: the number to be added to attack rolls :type attack_mod: int :param damage_mod: the number to be added to damage rolls :type damage_mod: int :param range_val: the range of the ranged attack :type range_val: usually an integer, but other types are allowed currently (a warning is issued) :param melee_range: the range of the melee attack :param adv: indicates whether the attack rolls should be made normally, with advantage, or with disadvanatage :type adv: one of these integers: -1, 0, 1 :param weapon: the Weapon this attack comes from, if any :type weapon: Weapon or None :raise: ValueError if input is invalid """ super().__init__(**kwargs) level = kwargs.get("level") if isinstance(level, int) and 0 <= level < 10: self._level = level else: raise ValueError("Level must be an integer between 0 and 9") school = kwargs.get("school") self._school = school casting_time = kwargs.get("casting_time") if casting_time in ["1 action", "1 bonus action", "1 reaction"]: self._casting_time = casting_time else: self._casting_time = time_to_rounds(casting_time) components = kwargs.get("components") if not isinstance(components, (list, tuple)): raise ValueError("Components must be a list or tuple") self._components = [] if "v" in components: self._components.append("v") if "s" in components: self._components.append("s") if "m" in components: self._components.append("m") duration = kwargs.get("duration") self._duration = time_to_rounds(duration) if kwargs.get("ritual"): self._ritual = True else: self._ritual = False def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on what is checked in the superclass method as well as level, school, casting time, components, and duration :param other: the Spell to be compared :type other: Spell :return: True if *self* __eq__ *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_level() == other.get_level() \ and self.get_school() == other.get_school() \ and self.get_casting_time() == other.get_casting_time() \ and self.get_components() == other.get_components() \ and self.get_duration() == other.get_duration() \ and self.get_ritual() == other.get_ritual()
[docs] def get_level(self) -> int: """ :return: level :rtype: integer between 1 and 9 (inclusive) """ return self._level
[docs] def get_school(self): """ :return: school of magic """ return self._school
[docs] def get_casting_time(self) -> int: """ :return: amount of time it takes cast *self*, in rounds :rtype: a positive integer for the number of rounds, or one of these strings: "1 action", "1 bonus action", "1 reaction" """ return self._casting_time
[docs] def get_components(self) -> list: """ :return: the components needed to cast *self* :rtype: list of strings """ return self._components
[docs] def has_component(self, component: str) -> bool: """ :param component: the component in question :type component: str :return: True if *self* has that component, False otherwise :rtype: bool """ return component in self.get_components()
[docs] def get_duration(self) -> int: """ :return: duration in rounds :rtype: non-negative integer """ return self._duration
[docs] def get_ritual(self) -> bool: """ :return: whether spell can be cast as a ritual """ return self._ritual
[docs] def make_attack(self, source, target, adv: int = 0, level: int = None) -> int: # pylint: disable=arguments-differ """ First, try to spend the appropriate spell slot. Then, call the superclass method. :param source: the Combatant that is making the attack :type source: Combatant :param target: the Combatant that *source* is attacking :type target: Combatant :param adv: advantage, disadvantage, or neither on the attack roll :type adv: one of these integers: -1, 0, 1 :param level: the level the spell is being cast at :type level: integer between 1 and 9 (inclusive) :return: the damage taken (if the attack misses, damage taken is None) :rtype: non-negative integer or None :raise: ValueError if *source* can't send attacks or *target* can't take attacks """ if level is None: level = self.get_level() elif level < self.get_level(): raise ValueError("Cannot cast a spell at a lower level than the spell's original level") if not source.can_cast(self): raise ValueError("Source tried to cast a spell that they cannot cast right now") try: source.spend_spell_slot(level, self) except ValueError: source.get_logger().error(f"You tried to cast a level {level} spell even though you have no slots left for it", stack_info=True) raise ValueError(f"Source tried to cast a level {level} spell even though they have no slots left for it") except NameError: raise ValueError("Tried to cast a spell using a source that can't cast spells") return super().make_attack(source=source, target=target, adv=adv) # To be overridden in subclasses
[docs] class SavingThrowSpell(Spell, SavingThrowAttack): """ Spells that require the target to make a saving throw """ def __init__(self, **kwargs): """ See Spell and SavingThrowAttack methods """ super().__init__(**kwargs)
[docs] class HealingSpell(Spell): """ Spells that heal (i.e., add hit points) instead of dealing damage; damage is used as healing """ def __init__(self, **kwargs): """ Validate the input and set the instance variables :param kwargs: keyword arguments. Valid arguments are as follows: :param level: the level of the spell :type level: integer between 1 and 9 (inclusive) :param school: the school of magic that *self* belongs to (currently not typechecked) :param casting_time: the time it takes to cast *self* :type casting_time: a positive integer for the number of rounds, or one of these strings: "1 action", "1 bonus action", "1 reaction" :param components: the components ("v" for verbal, "s" for somatic, "m" for material) needed to cast *self* :type components: list or tuple containing one or more of the aforementioned components :param duration: how long the spell lasts (in rounds) :type duration: non-negative integer :param name: what *self* is called. A unique name is recommended but not required :type name: str :param damage_dice: the dice to determine damage points :type damage_dice: TYPE_DICE_TUPLE :param attack_mod: the number to be added to attack rolls :type attack_mod: int :param damage_mod: the number to be added to damage rolls :type damage_mod: int :param range_val: the range of the ranged attack :type range_val: usually an integer, but other types are allowed currently (a warning is issued) :param melee_range: the range of the melee attack :param adv: indicates whether the attack rolls should be made normally, with advantage, or with disadvanatage :type adv: one of these integers: -1, 0, 1 :param weapon: the Weapon this attack comes from, if any :type weapon: Weapon or None :raise: ValueError if input is invalid """ super().__init__(**kwargs)
[docs] def make_attack(self, source, target, adv: int = 0, level=None) -> int: """ Spend the appropriate spell slot, then roll damage and heal the target for that amount :param source: the Combatant that is sending the healing :type source: Combatant :param target: the Combatant that *source* is healing :type target: Combatant :param adv: advantage, disadvantage, or neither on the attack roll (NOT USED FOR THIS METHOD) :type adv: one of these integers: -1, 0, 1 :param level: the level the spell is being cast at :type level: integer between 1 and 9 (inclusive) :return: the points the target is healed for :rtype: non-negative integer :raise: ValueError if input is invalid """ if level is None: level = self._level elif level < self.get_level(): source.get_logger.error("You cannot cast a spell at a lower level than the spell's original level", stack_info=True) raise ValueError(f"{source.get_name()} cannot cast a spell at a lower level than the spell's original level") try: source.spend_spell_slot(level, self) except ValueError: source.get_logger.error("You tried to cast a level {level} spell even though they have no slots left for it") raise ValueError(f"{source.get_name()} tried to cast a level ${level} spell " f"even though they have no slots left for it") source.get_logger().info(f"{source.get_name()} heals {target.get_name()}") healing = self.roll_damage().roll_number return target.take_healing(healing)