Source code for DnD_5e.character_classes.rogue

from typing import Optional

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


[docs] class Rogue(Character): """ Rogue 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, 8) 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 = 8 + constitution_mod if level > 1: max_hp += (5 + 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("simple weapons") proficiencies.add("hand crossbow") proficiencies.add("longsword") proficiencies.add("rapier") proficiencies.add("shortsword") proficiencies.add("dexterity") proficiencies.add("intelligence") proficiency_mod = proficiency_bonus_per_level(level) kwargs.update({"max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod, "hit_dice": hit_dice}) super().__init__(**kwargs) self.add_feature("sneak attack") sneak_dice_num = (self.get_level() - 1) // 2 + 1 self._sneak_attack_dice = (sneak_dice_num, 6) if self.get_level() > 1: self.add_feature("cunning action") if self.get_level() > 4: self.add_feature("uncanny dodge") if self.get_level() > 6: self.add_feature("evasion") if self.get_level() > 10: self.add_feature("reliable talent") if self.get_level() > 13: self.add_feature("blindsense") if self.get_level() > 14: self.add_feature("slippery mind") self._proficiencies.add("wisdom") self._saving_throws = {"strength": self.get_strength(), "dexterity": self.get_dexterity(), "constitution": self.get_constitution(), "intelligence": self.get_intelligence(), "wisdom": self.get_wisdom(), "charisma": self.get_charisma()} for ability in self._saving_throws: if ability in self._proficiencies: self._saving_throws[ability] += self._proficiency_mod if self.get_level() > 17: self.add_feature("elusive") if self.get_level() > 19: self.add_feature("stroke of luck") self._stroke_of_luck_slots = 1 def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes: sneak attack dice :param other: the Rogue to compare :type other: Rogue :return: True if *self* equals *other*, False otherwise :rtype: bool """ return super().__eq__(other) \ and self.get_sneak_attack_dice() == other.get_sneak_attack_dice()
[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: stroke of luck slots :param other: the Rogue to compare :type other: Rogue :return: True if *self* is identical to *other*, False otherwise :rtype: bool """ return super().current_eq(other) \ and self.get_stroke_of_luck_slots() == other.get_stroke_of_luck_slots()
[docs] def can_see(self, light_src: str) -> bool: """ Determine whether *self* can see a given light source. If *self* can't see according to superclass method, look at blindsense feature. :param light_src: a kind of light :type light_src: one of these strings: "normal", "dark", "magic" :return: True if *self* can see *light_src*, False otherwise :rtype: bool """ result = super().can_see(light_src) if not result: result = self.has_feature("blindsense") and not self.has_condition("deafened") return result
[docs] def get_adv_to_be_hit(self) -> int: """ The sum of advantage (+1) and disadvantage (-1) circumstances affecting *self* is stored in *self._adv_to_be_hit*. Look at this number and return an integer indicating whether an attack against *self* has advantage, disadvantage, or neither. Look at the "elusive" feature. :return: positive if attacks against *self* have advantage, negative if they have disadvantage, and 0 otherwise :rtype: one of these integers: -1, 0, 1 """ super_adv = super().get_adv_to_be_hit() if self.has_feature("elusive") and not self.has_condition("incapacitated") and super_adv > 0: return 0 return super_adv
[docs] def get_stroke_of_luck_slots(self) -> int: """ :return: stroke of luck slots :rtype: non-negative integer """ try: return self._stroke_of_luck_slots except AttributeError: return 0
[docs] def get_sneak_attack_dice(self) -> TYPE_DICE_TUPLE: """ :return: sneak attack dice :rtype: TYPE_DICE_TUPLE """ return self._sneak_attack_dice
[docs] def can_make_sneak_attack(self, weapon, target, adv) -> bool: # pylint: disable=unused-argument """ Determine if *self* can make a sneak attack against *target*. NOT IMPLEMENTED YET :param weapon: the Weapon used for the attack :type weapon: Weapon :param target: the Combatant being attacked :type target: Combatant :param adv: advantage (positive), disadvantage (negative), or neither (0) :type adv: int :return: True if *self* can make a sneak attack against *target*, False otherwise :rtype: bool """ # Note: this assumes the attack hit try: if not (weapon.has_prop("finesse") or isinstance(weapon, armory.RangedWeapon)): return False except NameError: return False if adv > 0: return True return False
[docs] def roll_sneak_attack_dice(self) -> int: """ Roll sneak attack dice :return: the number rolled by the sneak attack dice :rtype: non-negative integer """ return roll_dice(num=self.get_sneak_attack_dice()[0], dice_type=self.get_sneak_attack_dice()[1])[0]
[docs] def take_stroke_of_luck(self): """ Use the Stroke of Luck feature. NOT IMPLEMENTED YET. :return: None :raise: ValueError if *self* has no stroke of luck slots """ if not self.get_stroke_of_luck_slots(): self.get_logger().error("You have no slots left for stroke of luck", stack_info=True) raise ValueError("You have no slots left for stroke of luck") self._stroke_of_luck_slots -= 1
[docs] def send_attack(self, target, attack, adv=0) -> Optional[int]: """ Attack a given target using a given attack. Roll and add sneak attack damage if applicable :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 """ weapon = attack.get_weapon() adv_calc = adv + target.get_adv_to_be_hit() damage = super().send_attack(attack=attack, target=target) if damage is not None and self.can_make_sneak_attack(weapon, target, adv_calc): # TODO: sending the damage in two calls like this (see super().send_attack()) means 2 failed death saves. # Refactor so take_damage is only called once damage += target.take_damage(self.roll_sneak_attack_dice(), weapon.get_damage_type()) return damage