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