from DnD_5e.attack_class.spell_attacks import Spell
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
[docs]
class Wizard(SpellCaster, Character):
"""
Wizard 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)
Class specific parameters:
:param spell_mastery: the Spells *self* has mastered
:type spell_mastery: set, list, or tuple of strings or Spells (will be converted to set of strings)
:param signature_spells: *self's* Signature Spells
:type signature_spells: set, list, or tuple of strings or Spells (will be converted to set of strings)
: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, 6)
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 = 6 + constitution_mod
if level > 1:
max_hp += (4 + 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("dagger")
proficiencies.add("dart")
proficiencies.add("sling")
proficiencies.add("quarterstaff")
proficiencies.add("light crossbow")
proficiencies.add("intelligence")
proficiencies.add("wisdom")
proficiency_mod = proficiency_bonus_per_level(level)
spell_slots = {1: 2}
if level > 1:
spell_slots.update({1: 3})
if level > 2:
spell_slots.update({1:4, 2: 2})
if level > 3:
spell_slots.update({2: 3})
if level > 4:
spell_slots.update({3: 2})
if level > 5:
spell_slots.update({3: 3})
if level > 6:
spell_slots.update({4: 1})
if level > 7:
spell_slots.update({4: 2})
if level > 8:
spell_slots.update({4: 3, 5: 1})
if level > 9:
spell_slots.update({5: 2})
if level > 10:
spell_slots.update({6: 1})
if level > 12:
spell_slots.update({7: 1})
if level > 14:
spell_slots.update({8: 1})
if level > 16:
spell_slots.update({9: 1})
if level > 17:
spell_slots.update({5: 3})
if level > 18:
spell_slots.update({6: 2})
if level > 19:
spell_slots.update({7: 2})
kwargs.update({"max_hp": max_hp, "proficiencies": proficiencies, "proficiency_mod": proficiency_mod,
"hit_dice": hit_dice, "spell_ability": "intelligence", "spell_slots": spell_slots})
super().__init__(**kwargs)
# TODO: warn user that spell mastery and signature spells will be ignored if level is too low
if self.get_level() > 17:
self.add_feature("spell mastery")
self._spell_mastery_names = set()
spell_names = kwargs.get("spell_mastery")
if not isinstance(spell_names, (set, list, tuple)):
raise ValueError("Spell mastery names must be provided in a set, list, or tuple")
for name in spell_names:
if not isinstance(name, str):
if isinstance(name, Spell):
name = name.get_name()
else:
raise ValueError("Spell mastery must be names of spells (or actual spells to get names from)")
if len(self._spell_mastery_names) < 2:
self._spell_mastery_names.add(name)
else:
raise ValueError("Too many spell mastery spells/names provided")
if self.get_level() > 19:
self.add_feature("signature spells")
self._signature_spell_slots = {}
signature_spell_names = kwargs.get("signature_spells")
if not isinstance(signature_spell_names, (set, list, tuple)):
raise ValueError("Signature spell names must be provided in a set, list, or tuple")
for name in signature_spell_names:
if not isinstance(name, str):
if isinstance(name, Spell):
name = name.get_name()
else:
raise ValueError("Signature spell must be names of spells (or actual spells to get names from)")
if len(self._signature_spell_slots) < 2:
self._signature_spell_slots[name] = 1
else:
raise ValueError("Too many spell mastery spells/names provided")
def __eq__(self, other) -> bool:
"""
Compare *self* and *other* to determine if they are equal based on the superclass method and these attributes:
spell mastery, signature spells
:param other: the Wizard to compare
:type other: Wizard
:return: True if *self* equals *other*, False otherwise
:rtype: bool
"""
return super().__eq__(other) \
and self.get_spell_mastery_names() == other.get_spell_mastery_names() \
and self.get_signature_spells() == other.get_signature_spells()
[docs]
def current_eq(self, other):
"""
Compare *self* and *other* to determine if they are identical based on the attributes checked in *equals*
and also these attributes: signature spell slots
:param other: the Wizard to compare
:type other: Wizard
:return: True if *self* is identical to *other*, False otherwise
:rtype: bool
"""
return super().current_eq(other) \
and self.get_signature_spell_slots() == other.get_signature_spell_slots()
[docs]
def get_spell_mastery_names(self) -> set:
"""
:return: name of Spells that *self* has Spell Mastery for
:rtype: set of strings
"""
return self._spell_mastery_names
[docs]
def has_spell_mastery(self, item) -> bool:
"""
Determine if *self* has mastery of a given spell
:param item: a Spell or name of a Spell
:type item: Spell or str
:return: True if *self* has mastery of the spell *item*, False otherwise
:rtype: bool
"""
if not self.has_feature("spell mastery"):
return False
if isinstance(item, str):
name = item
elif isinstance(item, Spell):
name = item.get_name()
else:
self.get_logger().error("Must pass a string or a Spell to has_spell_mastery", stack_info=True)
raise ValueError("Must pass a string or a Spell to has_spell_mastery")
return name in self.get_spell_mastery_names()
[docs]
def get_signature_spell_slots(self) -> dict:
"""
:return: signature spell slots
:rtype: dict mapping spell names to the number of slots left to use that as a signature spell
"""
return self._signature_spell_slots
[docs]
def get_signature_spells(self) -> list:
"""
:return: singature spell names
:rtype: list of strings
"""
return sorted(self.get_signature_spell_slots().keys())
[docs]
def has_signature_spell(self, item) -> bool:
"""
Determine whether a given spell is a signature spell
:param item: a Spell or the name of a Spell
:type item: Spell or str
:return: True if *item* is a signature spell, False otherwise
:rtype: bool
"""
if not self.has_feature("signature spells"):
return False
if isinstance(item, str):
name = item
elif isinstance(item, Spell):
name = item.get_name()
else:
self.get_logger().error("Must pass a string or a Spell to has_signature_spell", stack_info=True)
raise ValueError("Must pass a string or a Spell to has_signature_spell")
return name in self.get_signature_spell_slots()
[docs]
def spend_spell_slot(self, level: int, spell=None):
"""
Spend a spell slot of the given level. Deal with spell mastery and signature spells appropriately.
:param level: the spell level
:type level: an integer from 0 to 9 (inclusive)
:param spell: the spell that is being used
:type spell: Spell
:return: None
:raise: ValueError if *self* doesn't have any spell slots of *level* level
"""
if self.has_spell_mastery(spell) and level == spell.get_level():
return
if self.has_signature_spell(spell) and level == spell.get_level():
if self._signature_spell_slots[spell.get_name()]:
self._signature_spell_slots[spell.get_name()] -= 1
return
super().spend_spell_slot(level)