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)