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")