Source code for DnD_5e.character_classes.barbarian

import warnings

from DnD_5e import features, armory
from DnD_5e.combatant.character import Character
from DnD_5e.utility_methods_dnd import ability_to_mod, proficiency_bonus_per_level


[docs] class Barbarian(Character): """ Barbarian 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) :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, 12) 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 = 12 + constitution_mod if level > 1: max_hp += (7 + constitution_mod) * (level - 1) proficiencies = kwargs.get('proficiencies', set()) if isinstance(proficiencies, (tuple, list, set)): proficiencies = set(proficiencies) elif isinstance(proficiencies, set): # pragma: no cover pass elif proficiencies is None: # pragma: no cover proficiencies = set() else: raise ValueError("Proficiencies must be provided as a set, list, or tuple") proficiencies.add("simple weapons") proficiencies.add("martial weapons") proficiencies.add("strength") proficiencies.add("constitution") proficiency_mod = proficiency_bonus_per_level(level) kwargs.update({"hit_dice": hit_dice, "max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod, "feature_classes": [features.UnarmoredDefenseBarbarian, features.FastMovementBarbarian]}) super().__init__(**kwargs) if self.get_level() < 3: self._rage_slots = 2 elif self.get_level() < 6: self._rage_slots = 3 elif self.get_level() < 12: self._rage_slots = 4 elif self.get_level() < 17: self._rage_slots = 5 else: self._rage_slots = 6 if self.get_level() < 9: self._rage_damage_bonus = 2 elif self.get_level() < 16: self._rage_damage_bonus = 3 else: self._rage_damage_bonus = 4 self._rage_state = False self.add_feature("rage") self.add_feature("unarmored defense") if self.get_level() > 1: self.add_feature("reckless attack") self._reckless_state = False self.add_feature("danger sense") if self.get_level() > 2: self._specialization = kwargs.get("specialization") if self.get_level() > 4: self.add_feature("extra attack") self.add_feature_class(features.FastMovementBarbarian) if self.get_level() > 6: self.add_feature("feral instinct") if self.get_level() > 8: self.add_feature("brutal critical") if self.get_level() > 10: self.add_feature("relentless rage") self._relentless_rage_dc = 10 if self.get_level() > 14: self.add_feature("persistent rage") if self.get_level() > 17: self.add_feature("indomitable might") 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 rage damage bonus :param other: the Barbarian to be compared :type other: Barbarian :return: True if *self* equals *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_rage_damage_bonus() == other.get_rage_damage_bonus()
[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: rage slots, rage state :param other: the Barbarian to compare :return: True if *self* is identical to *other*, False otherwise :rtype: bool """ return super().current_eq(other) \ and self.get_rage_slots() == other.get_rage_slots() \ and self.is_raging() == other.is_raging()
[docs] def get_rage_slots(self) -> int: """ :return: rage slots :rtype: non-negative integer """ return self._rage_slots
[docs] def get_rage_damage_bonus(self) -> int: """ :return: rage damage bonus :rtype: int """ return self._rage_damage_bonus
[docs] def is_raging(self) -> bool: """ :return: True if *self* is raging, False otherwise :rtype: bool """ return self._rage_state
[docs] def start_rage(self): """ Go into a rage state :return: None """ if self._rage_state: self.get_logger().warning("You cannot start raging because you are already raging", stack_info=True) warnings.warn("You cannot start raging because you are already raging") return self.get_logger().info("%s would like to RAGE!", self.get_name()) self._rage_state = True for attack in self._attacks: if attack.get_melee_range(): weapon = attack.get_weapon() if not weapon or isinstance(weapon, armory.MeleeWeapon): attack.set_damage_mod(attack.get_damage_mod() + self._rage_damage_bonus) self.add_resistance("bludgeoning") self.add_resistance("piercing") self.add_resistance("slashing")
[docs] def stop_rage(self): """ Stop the rage state :return: None """ if not self._rage_state: self.get_logger().warning("You cannot stop raging because you are not raging", stack_info=True) warnings.warn("You cannot stop raging because you are not raging") return self.get_logger().info("%s has finished raging.", self.get_name()) self._rage_state = False for attack in self._attacks: if attack.get_melee_range(): attack.set_damage_mod(attack.get_damage_mod() - self._rage_damage_bonus) self.remove_resistance("bludgeoning") self.remove_resistance("piercing") self.remove_resistance("slashing")
[docs] def start_reckless(self): """ Enter this state when attacking recklessly. It lasts for one round. :return: None """ if self._reckless_state: self.get_logger().warning("You cannot start recklessly attacking because you are already doing so", stack_info=True) warnings.warn("You cannot start recklessly attacking because you are already doing so") return self.get_logger().info("%s attacks recklessly!", self.get_name()) self.modify_adv_to_be_hit(1) self._reckless_state = True
[docs] def stop_reckless(self): """ End the reckless state. This happens after one round of recklessly attacking. :return: None """ if not self._reckless_state: self.get_logger().warning("You cannot stop recklessly attacking because you are not currently recklessly attacking", stack_info=True) warnings.warn("You cannot stop recklessly attacking because you are not currently recklessly attacking") return self.modify_adv_to_be_hit(-1) self._reckless_state = False
[docs] def make_saving_throw(self, save_type: str, adv=0): """ Roll a saving throw of the given type. This is different from the superclass method in that it is affected by raging and the "danger sense" feature. :param save_type: the kind of saving throw to make :type save_type: one of these strings: "strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma" :param adv: indicates advantage (positive), disadvantage (negative), or neither (0) :type adv: int :return: the number rolled for the saving throw :rtype: int """ if save_type == "strength" and self.is_raging(): local_adv = 1 elif self.has_feature("danger sense") and save_type == "dexterity" and not self.has_condition("blinded") \ and not self.has_condition("deafened") and not self.has_condition("incapacitated"): local_adv = 1 else: local_adv = 0 adv += local_adv return super().make_saving_throw(save_type=save_type, adv=local_adv)
[docs] def become_unconscious(self): """ Go unconscious. This differs from the superclass method in that *self* may drop to 1 hit point instead due to the "relentless rage" feature. If *self* saves, then *self* drops to 1 hit point. Otherwise, *self* is unconscious and has current hp of 0 :return: None """ if self.is_raging(): if self.has_feature("relentless rage"): if self.take_saving_throw("constitution", self._relentless_rage_dc): self._current_hp = 1 self.get_logger().info("Fueled by rage, %s staves off unconsciousness and drops to 1 hit point instead.", self.get_name()) return self._relentless_rage_dc += 5 self.stop_rage() super().become_unconscious()
[docs] def send_attack(self, target, attack, adv=0): """ Attack a given target using a given attack. This differs from the superclass method in that if *self* is recklessly attacking, the attack has advantage :param target: the Combatant to attack :type target: Combatant :param attack: the Attack being made :type attack: Attack :param adv: indicates whether *self* has advantage for this attack :type adv: int :return: the damage *target* took from *attack*, or None if the attack failed to hit """ if attack.get_melee_range() and self.has_feature("reckless attack") and self._reckless_state: adv += 1 super().send_attack(target, attack, adv)