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)