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, TYPE_DICE_TUPLE
[docs]
class Monk(Character):
"""
Monk 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("monk weapons")
proficiencies.add("strength")
proficiencies.add("dexterity")
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.UnarmoredDefenseMonk]})
super().__init__(**kwargs)
if self.get_level() > 1:
if self.get_level() < 6:
self._unarmored_movement = 10
elif self.get_level() < 10:
self._unarmored_movement = 15
elif self.get_level() < 14:
self._unarmored_movement = 20
elif self.get_level() < 18:
self._unarmored_movement = 25
else:
self._unarmored_movement = 30
if self.get_level() < 5:
self._martial_arts_dice = (1, 4)
elif self.get_level() < 11:
self._martial_arts_dice = (1, 6)
elif self.get_level() < 17:
self._martial_arts_dice = (1, 8)
else:
self._martial_arts_dice = (1, 10)
self.add_feature("unarmored defense")
if self.get_level() > 1:
self._ki_points = self.get_level()
self._ki_save_dc = 8 + self.get_proficiency_mod() + self.get_wisdom()
self.add_feature("flurry of blows")
self.add_feature("patient defense")
self.add_feature("step of the wind")
if self.get_level() > 2:
self.add_feature("deflect missiles")
if self.get_level() > 3:
self.add_feature("slow fall")
if self.get_level() > 4:
self.add_feature("extra attack")
self.add_feature("stunning strike")
if self.get_level() > 5:
self.add_feature("ki-empowered strikes")
if self.get_level() > 6:
self.add_feature("stillness of mind")
self.add_feature("evasion")
if self.get_level() > 9:
self.add_feature("purity of body")
if self.get_level() > 12:
self.add_feature("tongue of the sun and moon")
if self.get_level() > 13:
self.add_feature("diamond soul")
# proficiency in all Saving Throws
self._proficiencies.add("constitution")
self._proficiencies.add("intelligence")
self._proficiencies.add("wisdom")
self._proficiencies.add("charisma")
self._saving_throws = {"strength": self.get_strength() + self.get_proficiency_mod(),
"dexterity": self.get_dexterity() + self.get_proficiency_mod(),
"constitution": self.get_constitution() + self.get_proficiency_mod(),
"intelligence": self.get_intelligence() + self.get_proficiency_mod(),
"wisdom": self.get_wisdom() + self.get_proficiency_mod(),
"charisma": self.get_charisma() + self.get_proficiency_mod()}
if self.get_level() > 17:
self.add_feature("empty body")
if self.get_level() == 20:
self.add_feature("perfect soul")
def __eq__(self, other) -> bool:
"""
Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes:
ki save dc
:param other: the Monk to compare
:type other: Monk
:return: True if *self* equals *other*, False otherwise
:rtype: bool
"""
return super().__eq__(other) \
and self.get_ki_save_dc() == other.get_ki_save_dc()
[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: ki points
:param other: the Monk to compare
:type other: Monk
:return: True if *self* is identical to *other*, False otherwise
:rtype: bool
"""
return super().current_eq(other) \
and self.get_ki_points() == other.get_ki_points()
[docs]
def get_martial_arts_dice(self) -> TYPE_DICE_TUPLE:
"""
:return: martial arts dice
:rtype: TYPE_DICE_TUPLE
"""
return self._martial_arts_dice
[docs]
def get_ki_points(self) -> int:
"""
:return: ki points
:rtype: non-negative integer
"""
try:
return self._ki_points
except AttributeError:
return 0
[docs]
def get_ki_save_dc(self) -> int:
"""
:return: ki save dc
:rtype: positive integer
"""
try:
return self._ki_save_dc
except AttributeError:
self.get_logger().error("Cannot use a ki saving throw attack", stack_info=True)
raise ValueError("Cannot use a ki saving throw attack")
[docs]
def spend_ki_points(self, num: int) -> int:
"""
Spend *num* number of ki points
:param num: the number of ki points to spend
:return: None
:raise: ValueError if *self* doesn't have enough ki points or *num* is invalid
"""
if not isinstance(num, int) or num < 1:
self.get_logger().error("Num of ki points must be a positive integer", stack_info=True)
raise ValueError("Num of ki points must be a positive integer")
if num > self.get_ki_points():
self.get_logger().error("You don't have enough ki points left to spend", stack_info=True)
raise ValueError("You don't have enough ki points left to spend")
self._ki_points -= num
return num
[docs]
def add_attack(self, attack):
"""
Add the given attack. If the attack is related to a monk weapon and dexterity mod is less than strength mod,
change strength mod for attack and damage to dexterity mod.
:param attack: the Attack to add
:type attack: Attack
:return: None
"""
if armory.is_monk_weapon(attack.get_weapon()) and self.get_dexterity() > self.get_strength():
# change from str to dex
attack.set_attack_mod(attack.get_attack_mod() - self.get_strength() + self.get_dexterity())
attack.set_damage_mod(attack.get_damage_mod() - self.get_strength() + self.get_dexterity())
super().add_attack(attack)