from DnD_5e.combatant import Combatant
from DnD_5e.utility_methods_dnd import validate_dice, TYPE_DICE_TUPLE
[docs]
class Character(Combatant):
"""
This is for characters (PC, NPC, whatever)
"""
def __init__(self, **kwargs):
"""
Validate the input and set the instance variables
:param kwargs: keyword arguments. Uses all arguments in superclass constructor, plus these:
:param level: the character level
:type level: an integer between 1 and 20 (inclusive)
:param death_saves: a dictionary to track how number of death save fails and successes. Defaults to {0: 0, 1: 0}
:raise: ValueError if input is invalid
"""
super().__init__(**kwargs)
self._level = kwargs.get("level")
if not self._level:
self.get_logger().error("No level provided or level is 0", stack_info=True)
raise ValueError("No level provided or level is 0")
if not isinstance(self._level, int) or self._level < 1 or self._level > 20:
self.get_logger().error("Level must be an integer between 1 and 20", stack_info=True)
raise ValueError("Level must be an integer between 1 and 20")
self._hit_dice = validate_dice(kwargs.get("hit_dice"))
self._death_saves = self.validate_death_saves(kwargs.get('death_saves', {0: 0, 1: 0}))
[docs]
def validate_death_saves(self, death_saves):
try:
# TODO: better check to make sure death saves are valid
death_saves[0] # pylint:disable=pointless-statement
death_saves[1] # pylint:disable=pointless-statement
except KeyError:
self.get_logger().error(
"Death saves must be a dictionary mapping 0 to number of failures and 1 to number of successes",
stack_info=True)
raise ValueError(
"Death saves must be a dictionary mapping 0 to number of failures and 1 to number of successes")
return death_saves
def __eq__(self, other) -> bool:
"""
Compare *self* and *other* to determine if they are equal
based on what is checked in the superclass method as well as :py:attr:`level`
:param other: the Character to compare
:type other: Character
:return: True if *self* equals *other*, False otherwise
"""
return super().__eq__(other) and self.get_level() == other.get_level() \
and self.get_hit_dice() == other.get_hit_dice()
[docs]
def get_level(self) -> int:
"""
:return: level
:rtype: int
"""
return self._level
[docs]
def get_hit_dice(self) -> TYPE_DICE_TUPLE:
"""
:return: hit dice
:rtype: TYPE_DICE_TUPLE
"""
return self._hit_dice
[docs]
def get_death_saves(self):
"""
:return: death saves
"""
return self._death_saves
[docs]
def reset_death_saves(self):
"""
Reset death saves to 0 failures and 0 successes.
:return: None
"""
self._death_saves[0] = 0
self._death_saves[1] = 0
[docs]
def take_damage(self, damage: int, damage_type: str = None, is_critical: bool = False) -> int:
if self.has_condition("unstable"):
self.fail_death_save()
if is_critical:
self.fail_death_save()
return super().take_damage(damage, damage_type, is_critical)
[docs]
def should_die_from_damage(self, damage_taken: int, damage_type: str = None):
return damage_taken >= self.get_max_hp() * 2
[docs]
def become_unconscious(self):
super().become_unconscious()
if self.get_current_hp() == 0:
self.remove_condition("stable")
self.add_condition("unstable")
[docs]
def become_conscious(self):
"""
Become conscious (removing unconcsious, unstable, and stable conditions)
:return: None
"""
super().become_conscious()
self.remove_condition("unstable")
self.remove_condition("stable")
self.reset_death_saves()
[docs]
def fail_death_save(self):
"""
Record that *self* failed a death save, die if this is the third failed death save
:return: None
"""
self.get_logger().info("%s failed a death save.", self.get_name())
self._death_saves[0] += 1
if self.get_death_saves()[0] > 2:
self.get_logger().info("%s failed 3 death saves and will now die.", self.get_name())
self.die()
[docs]
def succeed_death_save(self):
"""
Record that *self* succeeded a death save, stabilize if this is the third successful death save
:return: None
"""
self.get_logger().info("%s succeeded a death save.", self.get_name())
self._death_saves[1] += 1
if self.get_death_saves()[1] > 2:
self.get_logger().info("%s succeeded 3 death saves and is now stable.", self.get_name())
self.remove_condition("unstable")
self.add_condition("stable")
[docs]
def take_turn_unconscious(self):
"""
If unstable, make a death saving throw. If stable, do nothing.
:return: None (no damage was dealt)
"""
if self.take_saving_throw("death", 10):
self.succeed_death_save()
else:
self.fail_death_save()
return super().take_turn_unconscious()
[docs]
def reset(self):
super().reset()
self.reset_death_saves()