Source code for DnD_5e.character_classes.sorcerer

import warnings

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


[docs] class Sorcerer(SpellCaster, Character): """ Sorcerer 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) Class specific parameters: :param metamagic: the names of the metamagic that *self* can do :type metamagic: set, list, or tuple of strings (will be converted to set) :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, 6) 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 = 6 + constitution_mod if level > 1: max_hp += (4 + 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("dagger") proficiencies.add("dart") proficiencies.add("sling") proficiencies.add("quarterstaff") proficiencies.add("light crossbow") proficiencies.add("constitution") proficiencies.add("charisma") proficiency_mod = proficiency_bonus_per_level(level) spell_slots = {1: 2} if level > 1: spell_slots.update({1: 3}) if level > 2: spell_slots.update({1:4, 2: 2}) if level > 3: spell_slots.update({2: 3}) if level > 4: spell_slots.update({3: 2}) if level > 5: spell_slots.update({3: 3}) if level > 6: spell_slots.update({4: 1}) if level > 7: spell_slots.update({4: 2}) if level > 8: spell_slots.update({4: 3, 5: 1}) if level > 9: spell_slots.update({5: 2}) if level > 10: spell_slots.update({6: 1}) if level > 12: spell_slots.update({7: 1}) if level > 14: spell_slots.update({8: 1}) if level > 16: spell_slots.update({9: 1}) if level > 17: spell_slots.update({5: 3}) if level > 18: spell_slots.update({6: 2}) if level > 19: spell_slots.update({7: 2}) kwargs.update({"max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod, "hit_dice": hit_dice, "spell_ability": "charisma", "spell_slots": spell_slots}) super().__init__(**kwargs) if self.get_level() > 1: self.add_feature("font of magic") self._sorcery_points = self.get_level() self._full_sorcery_points = self._sorcery_points if self.get_level() > 2: self.add_feature("metamagic") self._metamagic = set() metamagic = kwargs.get("metamagic", ()) for item in metamagic: self.add_metamagic(item) def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes: full sorcery points, metamagic :param other: the Sorcerer to compare :type other: Sorcerer :return: True if *self* equals *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_full_sorcery_points() == other.get_full_sorcery_points() \ and self.get_metamagic() == other.get_metamagic()
[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: sorcery points :param other: the Sorcerer to compare :type other: Sorcerer :return: True if *self* is identical to *other*, False otherwise :rtype: bool """ return super().current_eq(other) \ and self.get_sorcery_points() == other.get_sorcery_points()
[docs] def get_sorcery_points(self) -> int: """ :return: sorcery points :rtype: non-negative integer """ if self.has_feature("font of magic"): return self._sorcery_points return 0
[docs] def get_full_sorcery_points(self) -> int: """ :return: full sorcery points :rtype: non-negative integer """ if self.has_feature("font of magic"): return self._full_sorcery_points return 0
[docs] def get_metamagic(self) -> set: """ :return: metamagic :rtype: set of strings """ try: return self._metamagic except AttributeError: return set()
[docs] def has_metamagic(self, item: str) -> bool: """ Determine whether *self* has a given metamagic :param item: the metamagic to check for :type item: str :return: True if *self* has the metamagic *item*, False otherwise :rtype: bool """ if not isinstance(item, str): self.get_logger().error("Metamagic name must be a string", stack_info=False) raise ValueError("Metamagic name must be a string") return item in self.get_metamagic()
[docs] def spend_sorcery_points(self, points: int) -> int: """ Spend a given number of sorcery points :param points: the number of sorcery points to spend :type points: non-negative integer :return: the number of sorcery points spent :rtype: positive integer :raise: ValueError if *points* is invalid or *self* doesn't have enough sorcery points """ if not self.has_feature("font of magic"): self.get_logger().error("You can't spend sorcery points because you can't store sorcery points yet", stack_info=True) raise ValueError("You can't spend sorcery points because you can't store sorcery points yet") if not isinstance(points, int) or points < 1: self.get_logger().error("Sorcery points must be a positive integer", stack_info=True) raise ValueError("Sorcery points must be a positive integer") if points > self.get_sorcery_points(): self.get_logger().error("You doesn't have enough sorcery points to spend", stack_info=True) raise ValueError("You doesn't have enough sorcery points to spend") self._sorcery_points -= points return points
[docs] def reset_sorcery_points(self): """ Reset sorcery points :return: None """ if not self.has_feature("font of magic"): self.get_logger().error("You can't reset sorcery points because you can't store sorcery points yet", stack_info=True) raise ValueError("You can't reset sorcery points because you can't store sorcery points yet") self._sorcery_points = self.get_full_sorcery_points() self.get_logger().info("%s resets sorcery points", self.get_name())
[docs] def spell_slot_to_sorcery_points(self, level: int): """ Convert a spell slot to sorcery points :param level: the level of the spell slot being used :type level: integer between 1 and 9 (inclusive) :return: None :raise: ValueError if *level* is invalid """ if not self.has_feature("font of magic"): self.get_logger().error("You can't convert a spell slot to sorcery points because you can't store sorcery points yet", stack_info=True) raise ValueError("You can't convert a spell slot to sorcery points because you can't store sorcery points yet") if not isinstance(level, int) or not (0 < level < 10): # pylint: disable=superfluous-parens self.get_logger().error("Spell slot level must be an integer between 1 and 9", stack_info=True) raise ValueError("Spell slot level must be an integer between 1 and 9") self.spend_spell_slot(level) self._sorcery_points += level self.get_logger().info("%s converts a %dth level spell slot to sorcery points", self.get_name(), level)
[docs] def sorcery_points_to_spell_slot(self, level: int) -> int: """ Convert sorcery points to a spell slot :param level: the level of spell slot to gain :type level: integer between 1 and 5 (inclusive) :return: the number of sorcery points spent :rtype: positive integer :raise ValueError if *level* is invalid """ self.get_logger().info("%s (tries to) convert sorcery points to a level %d spell slot", self.get_name(), level) if level == 1: result = self.spend_sorcery_points(2) self.get_spell_slots()[1] += 1 return result if level == 2: result = self.spend_sorcery_points(3) self.get_spell_slots()[2] += 1 return result if level == 3: result = self.spend_sorcery_points(5) self.get_spell_slots()[3] += 1 return result if level == 4: result = self.spend_sorcery_points(6) self.get_spell_slots()[4] += 1 return result if level == 5: result = self.spend_sorcery_points(7) self.get_spell_slots()[5] += 1 return result self.get_logger().error("Spell slot level must be an integer between 1 and 5", stack_info=True) raise ValueError("Spell slot level must be an integer between 1 and 5")
[docs] def add_metamagic(self, item: str): """ Add the given metamagic :param item: the metamagic to add :type item: one of these strings: "careful", "distant", "empowered", "extended", "heightened", "quickened", "subtle", "twinned" :return: None :raise: ValueError if *item* is invalid """ if not self.has_feature("metamagic"): self.get_logger().error("You can't do metamagic yet", stack_info=True) raise ValueError("You can't do metamagic yet") if len(self.get_metamagic()) >= 2 + (self.get_level() - 3) // 7: self.get_logger().error("You can't add another metamagic option; you aren't a high enough level", stack_info=True) raise ValueError("You can't add another metamagic option; you aren't a high enough level") if item in ["careful", "distant", "empowered", "extended", "heightened", "quickened", "subtle", "twinned"]: if self.has_metamagic(item): self.get_logger().warning("You already have that kind of metamagic (%s). It cannot be added again.", item, stack_info=True) warnings.warn("You already have that kind of metamagic (%s). It cannot be added again." % item) return self._metamagic.add(item) self.get_logger().info("%s adds %s metamagic", self.get_name(), item) else: self.get_logger().error('Metamagic must be one of these kinds: "careful", "distant", "empowered", ' '"extended", "heightened", "quickened", "subtle", "twinned"', stack_info=True) raise ValueError('Metamagic must be one of these kinds: "careful", "distant", "empowered", "extended", ' '"heightened", "quickened", "subtle", "twinned"')