Source code for DnD_5e.weapons

import warnings
from copy import copy
from typing import Tuple, Optional
from DnD_5e import dice
from DnD_5e.utility_methods_dnd import validate_dice, TYPE_DICE_TUPLE

TYPE_RANGE_TUPLE = Tuple[int, int]  # pylint: disable=invalid-name

[docs] class Weapon: """ This class represents weapons """ def __init__(self, **kwargs): """ Validate the input and set the instance variables :param kwargs: keyword arguments. Valid arguments are as follows: :param name: what *self* is called. A unique name is recommended but not required :type name: str :param finesse: if this evaluates to True, *self* has the finesse property :param light: if this evaluates to True, *self* has the light property :param heavy: if this evaluates to True, *self* has the heavy property. Only read in if *light* doesn't evaluate to true :param loading: if this evaluates to True, *self* has the loading property :param range: range for ranged attack :type range: a tuple of two non-negative integers or a string with two non-negative integers separated by '/'. First number is range for a normal attack, second number is range for a disadvantage attack. :param melee_range: range for melee attack :type melee_range: non-negative integer :param reach: if this evaluates to True, *self* has the reach property :param two-handed: if this evaluates to True, *self* has the two-handed property :param versatile: damage dice for using this weapon two-handed. If *versatile* is not a valid dice value, then *self._versatile* is 0 and *self* does not have the *versatile* property. :type versatile: see *utility_methods_dnd.validate_dice* :param damage_dice: damage dice for attacks this weapon makes :type damage_dice: see constructor in dice module :param owner: whatever owns this weapon. Only one thing can own *self* at a time. Can be a free weapon that doesn't need no owner. :type owner: NO TYPECHECKING IS PERFORMED (allows more freedom, prevents circular import with *combatant* module). Usually will be a Combatant or None, but could be anything. :param attack_mod: the number added to attack rolls :type attack_mod: int :param damage_mod: the number added to damage rolls :type damage_mod: int :raise: ValueError if input is invalid """ self._name = kwargs.get('name') # needed for verbose output if not self._name: raise ValueError("Must provide name") finesse = kwargs.get('finesse', 0) light = kwargs.get('light', 0) heavy = kwargs.get('heavy', 0) loading = kwargs.get('loading', 0) self._range = kwargs.get('range', 0) # range is 0 or a tuple. For our purposes it's the same property as thrown if isinstance(self._range, tuple): # lists are mutable and that could be a problem if len(self._range) != 2: raise ValueError(f"{self._name} must provide exactly two non-negative integer values: " f"normal range and disadvantage range") if not isinstance(self._range[0], int) or self._range[0] < 0 \ or not isinstance(self._range[1], int) or self._range[1] < 0: raise ValueError(f"{self._name}: must provide exactly two non-negative integer values: " "normal range and disadvantage range") elif isinstance(self._range, str): try: self._range = tuple(int(x) for x in self._range.split("/")) except ValueError: raise ValueError(f"{self.get_name()} must provide range in tuple of two non-negative integers (20, 60) " "or string format 20/60") if len(self._range) != 2: raise ValueError(f"{self.get_name()} must provide range in tuple of two non-negative integers (20, 60) " "or string format 20/60") elif self._range != 0: raise ValueError(f"{self.get_name()} must provide range in tuple of two non-negative integers (1, 6) " f"or string format 1d6, or 0 if this is melee weapon") self._melee_range = kwargs.get('melee_range', 0) if not self._melee_range: # assume this is a melee weapon if not otherwise specified if not self._range: self._melee_range = 5 else: if not isinstance(self._melee_range, int) or self._melee_range < 0: raise ValueError(f"{self.get_name()} melee range must be a non-negative integer") self._reach = kwargs.get('reach', 0) if self._reach: if self._melee_range: self._melee_range += 5 else: raise ValueError(f"{self.get_name()} must have melee range to have the reach property") two_handed = kwargs.get('two_handed', 0) self._versatile = kwargs.get('versatile', 0) try: self._versatile = validate_dice(self._versatile) except ValueError: self._versatile = None self._props = set() if finesse: self._props.add("finesse") if light: self._props.add("light") elif heavy: self._props.add("heavy") if loading: self._props.add("loading") if self._range: self._props.add("range") if self._melee_range: self._props.add("melee") if self._reach: self._props.add("reach") if two_handed: self._props.add("two_handed") elif self._versatile: self._props.add("versatile") damage_dice = kwargs.get("damage_dice") if isinstance(damage_dice, dice.DamageDice): self._damage_dice = damage_dice else: kwargs.update(modifier=kwargs.get("damage_mod", 0)) if isinstance(damage_dice, tuple): kwargs.update(dice_tuple=damage_dice) elif isinstance(damage_dice, str): kwargs.update(str_val=damage_dice) self._damage_dice = dice.DamageDice(**kwargs) self._owner = kwargs.get('owner', None) self._attack_mod = kwargs.get('attack_mod', 0) if not isinstance(self._attack_mod, int): raise ValueError(f"{self.get_name()} attack bonus must be an integer") def __eq__(self, other) -> bool: """ Compare *self* and *other* to determine if they are equal based on the following characteristics: range, melee range, properties, damage dice, damage type :param other: the Weapon to be compared :type other: Weapon :return: True if *self* __eq__ *other*, False otherwise :rtype: bool """ if other is self: return True if type(other) != type(self): # pylint: disable=unidiomatic-typecheck return False return self.get_range() == other.get_range() \ and self.get_melee_range() == other.get_melee_range() \ and self.get_properties() == other.get_properties() \ and self.get_damage_dice() == other.get_damage_dice() \ and self.get_damage_type() == other.get_damage_type() \ and self.get_attack_mod() == other.get_attack_mod() \ and self.get_damage_mod() == other.get_damage_mod()
[docs] def get_properties(self) -> set: """ :return: properties :rtype: set of strings """ return self._props
[docs] def has_prop(self, prop) -> bool: """ Determine whether *self* has a given property :param prop: the property to check for :return: True if *self* has property *prop*, False otherwise :rtype: bool """ return prop in self._props
[docs] def get_range(self) -> TYPE_RANGE_TUPLE: """ :return: range for ranged attacks :rtype: TYPE_RANGE_TUPLE """ return self._range
[docs] def get_melee_range(self) -> int: """ :return: melee range :rtype: non-negative integer """ return self._melee_range
[docs] def get_damage_dice(self) -> dice.DamageDice: """ :return: damage dice :rtype: dice.DamageDice """ return self._damage_dice
[docs] def get_damage_type(self): """ :return: damage type """ return self.get_damage_dice().get_damage_type()
[docs] def has_damage_type(self, damage_type: str) -> bool: """ :param damage_type: the damage type to look for :type damage_type: str :return: True if damage dice has the specified damage type, False otherwise :rtype: bool """ return self.get_damage_dice().has_damage_type(damage_type)
[docs] def get_versatile(self) -> Optional[TYPE_DICE_TUPLE]: # damage dice for versatile weapon """ :return: dice for two-handed attack with versatile weapon :rtype: TYPE_DICE_TUPLE or None """ return self._versatile
[docs] def get_name(self) -> str: """ :return: name :rtype: str """ return self._name
[docs] def get_owner(self): """ :return: owner """ return self._owner
[docs] def get_attack_mod(self) -> int: """ :return: attack mod :rtype: int """ return self._attack_mod
[docs] def get_damage_mod(self) -> int: """ :return: damage mod :rtype: int """ try: return self.get_damage_dice().get_modifier() except NotImplementedError: warnings.warn("Tried to get damage mod on a DamageDiceBag. Returning 0 instead") return 0
[docs] def set_name(self, name: str): """ Set *self._name* :param name: the name to change to :type name: str :return: None :raise: ValueError if *name* is not a string """ if name == self._name: return if not isinstance(name, str): raise ValueError("Name must be a string") self._name = name
[docs] def set_owner(self, owner): """ Set *self._owner* to a given owner (not typechecked) :param owner: the new owner of *self* :return: None """ self._owner = owner
[docs] def set_attack_mod(self, attack_mod: int): """ Set *self._attack_mod* :param attack_mod: the new attack mod :type attack_mod: int :return: None :raise: ValueError if *attack_mod* is not an integer """ if not isinstance(attack_mod, int): raise ValueError(f"{self.get_name()} attack mod must be an integer") self._attack_mod = attack_mod
[docs] def set_damage_mod(self, damage_mod): """ Set *self._damage_mod* :param damage_mod: the new attack mod :type damage_mod: int :return: None :raise: ValueError if *damage_mod* is not an integer """ self.get_damage_dice().set_modifier(damage_mod)
[docs] def get_attack_kwargs(self): """ Get the kwargs dictionaries for each :py:class:`Attack` that can be made using this weapon. I would use this to build the attack themselves, but I need to avoid a circular import. :return: a list of dicts, with one dictionary for each attack :rtype: dict of dicts: key is the attack name, value is the attack kwargs """ attacks = {} damage_dice = self.get_damage_dice() if self.get_range(): name_range = f"{self.get_name()}_range" name_range_disadv = f"{self.get_name()}_range_disadvantage" attacks["range"] = {"damage_dice": damage_dice, "name": name_range, "weapon": self, "range": self.get_range()[0]} attacks["range_disadv"] = {"damage_dice": damage_dice, "name": name_range_disadv, "weapon": self, "range": self.get_range()[1], "adv": -1} if self.get_melee_range(): melee_name = f"{self.get_name()}_melee" attacks["melee"] = {"damage_dice": damage_dice, "name": melee_name, "weapon": self, "melee_range": self.get_melee_range()} if self.get_versatile(): versatile_name = f"{self.get_name()}_versatile" attacks["versatile"] = {"damage_dice": self.get_versatile(), "name": versatile_name, "weapon": self, "melee_range": self.get_melee_range()} return attacks
[docs] def weapon_list_equals(list1: list, list2: list) -> bool: """ Function to compare two lists of :py:class:`Weapon` s . Order matters for equality; [a, b, c] is not equal to [b, a, c] :param list1: list of Weapons :type list1: list of :py:class:`Weapon` s :param list2: list of Weapons :type list2: list of :py:class:`Weapon` s :return: True if the lists are equal, False otherwise """ try: if len(list1) != len(list2): return False for i in range(len(list1)): # pylint: disable=consider-using-enumerate if list1[i] != list2[i]: return False return True except (AttributeError, ValueError, TypeError): raise ValueError("Input should be two lists of Weapons")