from DnD_5e.combatant import Combatant
from DnD_5e.combatant.character import Character
from DnD_5e.combatant.spellcaster import SpellCaster
from DnD_5e.utility_methods_dnd import ability_to_mod, proficiency_bonus_per_level, roll_dice
[docs]
class Paladin(SpellCaster, Character):
"""
Paladin 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)
:param fighting_style: the fighting style *self* knows
:type fighting_style: str
: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, 10)
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 = 10 + constitution_mod
if level > 1:
max_hp += (6 + 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("martial weapons")
proficiencies.add("wisdom")
proficiencies.add("charisma")
proficiency_mod = proficiency_bonus_per_level(level)
spell_slots = {1: 0}
if level > 1:
spell_slots = {1: 2}
if level > 2:
spell_slots.update({1: 3})
if level > 4:
spell_slots.update({1: 4, 2: 2})
if level > 6:
spell_slots.update({2: 3})
if level > 8:
spell_slots.update({3: 2})
if level > 9:
spell_slots.update({3: 3})
if level > 12:
spell_slots.update({4: 1})
if level > 14:
spell_slots.update({4: 2})
if level > 16:
spell_slots.update({4: 3, 5: 1})
if level > 18:
spell_slots.update({5: 2})
kwargs.update({"max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod,
"hit_dice": hit_dice, "spell_ability": "charisma", "spell_slots": spell_slots})
# include empty spell slots so that SpellCaster init doesn't throw a fit
super().__init__(**kwargs)
self.add_feature("divine sense")
self._divine_sense_slots = 1 + self.get_charisma()
self.add_feature("lay on hands")
self._lay_on_hands_pool = self.get_level() * 5
if self.get_level() > 1:
self.add_feature("fighting style")
fighting_style = kwargs.get("fighting_style")
self.add_fighting_style(fighting_style)
self.add_feature("divine smite")
if level > 2:
self.add_feature("divine health")
self.add_feature("sacred oath")
if level > 4:
self.add_feature("extra attack")
if level > 5:
self.add_feature("aura of protection")
self._aura = 10
if level > 9:
self.add_feature("aura of courage")
if level > 10:
self.add_feature("improved divine smite")
if level > 13:
self.add_feature("cleansing touch")
self._cleansing_touch_slots = max(self.get_charisma(), 1)
if level > 17:
self._aura = 30
def __eq__(self, other) -> bool:
"""
Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes:
aura
:param other: the Paladin to compare
:type other: Paladin
:return: True if *self* equals *other*, False otherwise
:rtype: bool
"""
return super().__eq__(other) \
and self.get_aura() == other.get_aura()
[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: divine sense slots, lay on hands pool, cleansing touch slots
:param other: the Paladin to compare
:type other: Paladin
:return: True if *self* is identical to *other*, False otherwise
:rtype: bool
"""
return super().current_eq(other) \
and self.get_divine_sense_slots() == other.get_divine_sense_slots() \
and self.get_lay_on_hands_pool() == other.get_lay_on_hands_pool() \
and self.get_cleansing_touch_slots() == other.get_cleansing_touch_slots()
[docs]
def get_divine_sense_slots(self) -> int:
"""
:return: divine sense slots
:rtype: non-negative integer
"""
return self._divine_sense_slots
[docs]
def get_lay_on_hands_pool(self) -> int:
"""
:return: the hit points in the lay on hands pool
:rtype: non-negative integer
"""
return self._lay_on_hands_pool
[docs]
def get_aura(self) -> int:
"""
:return: the number of feet for the aura features
:rtype: non-negative integer
"""
try:
return self._aura
except AttributeError:
return 0
[docs]
def get_cleansing_touch_slots(self) -> int:
"""
:return: cleansing touch slots
:rtype: non-negative integer
"""
try:
return self._cleansing_touch_slots
except AttributeError:
return 0
[docs]
def spend_divine_sense_slot(self):
"""
Spend a divine sense slot
:return: None
:raise: ValueError if *self* has no divine sense slots left
"""
if self.get_divine_sense_slots():
self._divine_sense_slots -= 1
else:
self.get_logger().error("You have no slots left to cast divine sense", stack_info=True)
raise ValueError("You have no slots left to cast divine sense")
[docs]
def spend_cleansing_touch_slot(self):
"""
Spend a cleansing touch slot
:return: None
:raise: ValueError if *self* has no cleansing touch slots left
"""
if not self.get_cleansing_touch_slots():
self.get_logger().error("You don't have any cleansing touch slots left", stack_info=True)
raise ValueError("You don't have any cleansing touch slots left")
self._cleansing_touch_slots -= 1
[docs]
def send_lay_on_hands(self, hp: int, target=None, use="healing"): # pylint: disable=inconsistent-return-statements
"""
Use the Lay on Hands feature
:param hp: the number of hit points to take from the Lay on Hands pool
:type hp: non-negative integer
:param target: the Combatant to use Lay on Hands on
:type target: Combatant
:param use: what to use Lay on Hands for
:type use: str
:return: the hit points healed for, if *use* is "healing"
:raise: ValueError if *self* doesn't have enough Lay on Hands points
"""
if self.get_lay_on_hands_pool() >= hp:
self._lay_on_hands_pool -= hp
if use == "healing" and isinstance(target, Combatant):
self.get_logger().info("%s uses Lay on Hands to heal %s", self.get_name(), target.get_name())
return target.take_healing(hp)
else:
self.get_logger().error("You don't have enough lay on hands points left for that", stack_info=True)
raise ValueError("You don't have enough lay on hands points left for that")
[docs]
def send_divine_smite(self, target: Combatant, level=1) -> int:
"""
Use Divine Smite on a specified target
:param target: the Combatant to use Divine Smite on
:type target: Combatant
:param level: the level to cast Divine Smite at
:type level: integer between 1 and 9 (inclusive)
:return: the damage taken
:rtype: non-negative integer
:raise: ValueError if *self* doesn't have the Divine Smite feature
"""
if not self.has_feature("divine smite"):
self.get_logger().error("You don't have the divine smite feature", stack_info=True)
raise ValueError("You don't have the divine smite feature")
self.spend_spell_slot(level)
dice_num = max(2 + level-1, 5)
dice_type = 8
damage = roll_dice(dice_type, num=dice_num)[0] # TODO: use Dice
self.get_logger().info("%s uses Divine Smite on %s", self.get_name(), target.get_name())
return target.take_damage(damage, damage_type="radiant")