"""Tools for describing a player character.""" import os import re import warnings import math from types import ModuleType from typing import Sequence, Union, MutableMapping import jinja2 from dungeonsheets import ( armor, background, classes, features, infusions, magic_items, monsters, race, spells, weapons, ) from dungeonsheets.content_registry import find_content from dungeonsheets.weapons import Weapon from dungeonsheets.entity import Entity dice_re = re.compile(r"(\d+)d(\d+)") __all__ = ( "Artificer", "Barbarian", "Bard", "Cleric", "Character", "Druid", "Fighter", "Monk", "Paladin", "Ranger", "Rogue", "Sorceror", "Warlock", "Wizard", ) multiclass_spellslots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) 1: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0), 2: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0), 3: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0), 4: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0), 5: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0), 6: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0), 7: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0), 8: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0), 9: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0), 10: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0), 11: (0, 4, 3, 3, 3, 2, 1, 0, 0, 0), 12: (0, 4, 3, 3, 3, 2, 1, 0, 0, 0), 13: (0, 4, 3, 3, 3, 2, 1, 1, 0, 0), 14: (0, 4, 3, 3, 3, 2, 1, 1, 0, 0), 15: (0, 4, 3, 3, 3, 2, 1, 1, 1, 0), 16: (0, 4, 3, 3, 3, 2, 1, 1, 1, 0), 17: (0, 4, 3, 3, 3, 2, 1, 1, 1, 1), 18: (0, 4, 3, 3, 3, 3, 1, 1, 1, 1), 19: (0, 4, 3, 3, 3, 3, 2, 1, 1, 1), 20: (0, 4, 3, 3, 3, 3, 2, 2, 1, 1), } def _resolve_mechanic(mechanic, SuperClass, warning_message=None): """Take a raw entry in a character sheet and turn it into a usable object. Eg: spells can be defined in many ways. This function accepts all of those options and returns an actual *Spell* class that can be used by a character:: >>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell) >>> _resolve_mechanic("mage_hand", SuperClass=None) >>> from dungeonsheets import spells >>> class MySpell(spells.Spell): pass >>> _resolve_mechanic(MySpell, SuperClass=spells.Spell) >>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell) The acceptable entries for *mechanic*, in priority order, are: 1. A subclass of *SuperClass* 2. A string with the name of defined content 3. The name of an unknown spell (creates generic object using *factory*) *SuperClass* can be ``None`` to match any class, however this will raise an exception if more than one content type has this name. For example, "shield" can refer to both the armor or the spell, so ``_resolve_mechanic("shield")`` will raise an exception. Parameters ========== mechanic : str, type The thing to be resolved, either a string with the name of the mechanic, or a subclass of *ParentClass* describing the mechanic. SuperClass : type Class to determine whether *mechanic* should just be allowed through as is. error_message : str, optional A string whose ``str.format()`` method (receiving one positional argument *mechanic*) will be used for displaying a warning when an unknown mechanic is resolved. If omitted, no warning will be displayed. Returns ======= Mechanic A class representing the resolved game mechanic. This will likely be a subclass of *SuperClass* if the other parameters are well behaved, but this is not enforced. """ is_already_resolved = isinstance(mechanic, type) and issubclass( mechanic, SuperClass ) if is_already_resolved: Mechanic = mechanic else: try: # Retrieve pre-defined mechanic valid_classes = [SuperClass] if SuperClass is not None else [] Mechanic = find_content(mechanic, valid_classes=valid_classes) except AttributeError: # No pre-defined mechanic available if warning_message is not None: # Emit the warning msg = warning_message.format(mechanic) warnings.warn(msg) else: # Create a generic message so we can make a docstring later. msg = f'Mechanic "{mechanic}" not defined. Please add it.' # Create generic mechanic from the factory class_name = "".join([s.title() for s in mechanic.split("_")]) mechanic_name = mechanic.replace("_", " ").title() attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"} Mechanic = type(class_name, (SuperClass,), attrs) return Mechanic class Character(Entity): """A generic player character.""" # Character-specific player_name = "" xp = 0 # Extra hit points info, for characters only hp_current = None hp_temp = 0 # Base stats (ability scores) inspiration = False attacks_and_spellcasting = "" class_list = list() _background = None # Characteristics personality_traits = ( "TODO: Describe how your character behaves, interacts with others" ) ideals = "TODO: Describe what values your character believes in." bonds = "TODO: Describe your character's commitments or ongoing quests." flaws = "TODO: Describe your character's interesting flaws." features_and_traits = "Describe any other features and abilities." _proficiencies_text = list() # Appearance # portrait = placeholder not sure how to implement age = 0 height = "" weight = "" eyes = "" skin = "" hair = "" # Background allies = "" faction_name = "" # faction_symbol = placeholder not sure how to implement backstory = "" other_feats_traits = "" treasure = "" def __init__( self, classes: Sequence = [], levels: Sequence[int] = [], subclasses: Sequence = [], **attrs, ): """Create a new character from attributes *attrs*. **Multiclassing** can be accomplished by a list of class names *classes*, and a list of class levels *levels*. Parameters ========== classes Strings with class names, or character class definitions representing the characters various D&D classes. levels The class levels for each corresponding class entry in *classes*. subclasses Subclasses that apply for this character. **attrs Additional keyword parameters to set as attributes for this character. """ super(Character, self).__init__() self.clear() # make sure class, race, background are set first my_classes = classes my_levels = levels my_subclasses = subclasses # backwards compatability if len(my_classes) == 0: if "class" in attrs: my_classes = [attrs.pop("class")] my_levels = [attrs.pop("level", 1)] my_subclasses = [attrs.pop("subclass", None)] else: # if no classes or levels given, default to Lvl 1 Fighter my_classes = ["Fighter"] my_levels = [1] my_subclasses = [None] # Generate the list of class objects self.add_classes( my_classes, my_levels, my_subclasses, feature_choices=attrs.get("feature_choices", []), ) # parse race and background self.race = attrs.pop("race", None) self.background = attrs.pop("background", None) # parse all other attributes self.set_attrs(**attrs) self.__set_max_hp(attrs.get("hp_max", None)) def clear(self): # reset class-defined items self.class_list = list() self.weapons = list() self.magic_items = list() self._saving_throw_proficiencies = tuple() self.other_weapon_proficiencies = tuple() self.skill_proficiencies = list() self.skill_expertise = list() self._proficiencies_text = list() self._spells = list() self._spells_prepared = list() self.infusions = list() self.custom_features = list() self.feature_choices = list() def __str__(self): return self.name def __repr__(self): return f"<{self.class_name}: {self.name}>" def add_class( self, cls: (classes.CharClass, type, str), level: (int, str), subclass=None, feature_choices: Sequence = [], ): """Add a class, level, and subclass the character has attained.""" if isinstance(cls, str): cls = cls.strip().title().replace(" ", "") try: cls = getattr(classes, cls) except AttributeError: raise AttributeError( "class was not recognized from classes.py: {:s}".format(cls) ) if isinstance(level, str): level = int(level) self.class_list.append( cls(level, owner=self, subclass=subclass, feature_choices=feature_choices) ) def add_classes( self, classes_list: Sequence[Union[str, classes.CharClass]] = [], levels: Sequence[Union[int, float, str]] = [], subclasses: Sequence = [], feature_choices: Sequence = [], ): """Add several classes, levels, etc. The lists can also be single values for a single class character. """ if isinstance(classes_list, str): classes_list = [classes_list] if ( isinstance(levels, int) or isinstance(levels, float) or isinstance(levels, str) ): levels = [levels] if len(levels) == 0: levels = [1] * len(classes_list) if isinstance(subclasses, str): subclasses = [subclasses] if len(subclasses) == 0: subclasses = [None] * len(classes_list) assert len(classes_list) == len( levels ), "the length of classes {:d} does not match length of levels {:d}".format( len(classes), len(levels) ) assert len(classes_list) == len( subclasses ), "the length of classes {:d} does not match length of subclasses {:d}".format( len(classes_list), len(subclasses) ) for cls, lvl, sub in zip(classes_list, levels, subclasses): params = {} params["feature_choices"] = feature_choices self.add_class(cls=cls, level=lvl, subclass=sub, **params) @property def race(self): return self._race @race.setter def race(self, newrace): if isinstance(newrace, race.Race): self._race = newrace self._race.owner = self elif isinstance(newrace, type) and issubclass(newrace, race.Race): self._race = newrace(owner=self) elif isinstance(newrace, str): try: self._race = find_content(newrace, valid_classes=[race.Race])( owner=self ) except AttributeError: msg = f'Race "{newrace}" not defined. Please add it to ``race.py``' self._race = race.Race(owner=self) warnings.warn(msg) elif newrace is None: self._race = race.Race(owner=self) @property def background(self): return self._background @background.setter def background(self, bg): if isinstance(bg, background.Background): self._background = bg self._background.owner = self elif isinstance(bg, type) and issubclass(bg, background.Background): self._background = bg(owner=self) elif isinstance(bg, str): try: self._background = find_content( bg, valid_classes=[background.Background] )(owner=self) except AttributeError: msg = ( f'Background "{bg}" not defined. Please add it to ``background.py``' ) self._background = background.Background(owner=self) warnings.warn(msg) @property def class_name(self): if self.num_classes >= 1: return self.primary_class.name else: return "" @property def classes_and_levels(self): return " / ".join([f"{c.name} {c.level}" for c in self.class_list]) @property def class_names(self): return [c.name for c in self.class_list] @property def levels(self): return [c.level for c in self.class_list] @property def subclasses(self): return [c.subclass for c in self.class_list if c.subclass is not None] @property def level(self): return sum(c.level for c in self.class_list) @level.setter def level(self, new_level): self.primary_class.level = new_level if self.num_classes > 1: warnings.warn( "Unable to tell which level to set. Updating " "level of primary class {:s}".format(self.primary_class.name) ) @property def num_classes(self): return len(self.class_list) @property def has_class(self): return self.num_classes > 0 @property def primary_class(self): # for now, assume first class given must be primary class if self.has_class: return self.class_list[0] else: return None def __set_max_hp(self, hp_max): """ Set maximum HP based on value in charlist py or calc from classes """ if hp_max: assert isinstance(hp_max, int), hp_max.__class__ self.hp_max = hp_max else: const_mod = self.constitution.modifier level_one_hp = self.primary_class.hit_dice_faces + const_mod self.hp_max = level_one_hp for char_cls in self.class_list: hp_per_lvl = char_cls.hit_dice_faces / 2 + 1 + const_mod levels = char_cls.level if char_cls == self.primary_class: levels -= 1 assert levels >= 0 self.hp_max += int(hp_per_lvl * levels) @property def weapon_proficiencies(self): wp = set(self.other_weapon_proficiencies) if self.num_classes > 0: wp |= set(self.primary_class.weapon_proficiencies) if self.num_classes > 1: for c in self.class_list[1:]: wp |= set(c.multiclass_weapon_proficiencies) if self.race is not None: wp |= set(getattr(self.race, "weapon_proficiencies", ())) if self.background is not None: wp |= set(getattr(self.background, "weapon_proficiencies", ())) return tuple(wp) @weapon_proficiencies.setter def weapon_proficiencies(self, new_weapons): self.other_weapon_proficiencies = tuple(new_weapons) @property def other_weapon_proficiencies_text(self): return tuple(w.name for w in self.other_weapon_proficiencies) @property def features(self): fts = set(self.custom_features) fighting_style_defined = False set_of_fighting_styles = { "Fighting Style (Archery)", "Fighting Style (Defense)", "Fighting Style (Dueling)", "Fighting Style (Great Weapon Fighting)", "Fighting Style (Protection)", "Fighting Style (Two-Weapon Fighting)", } for temp_feature in fts: fighting_style_defined = temp_feature.name in set_of_fighting_styles if fighting_style_defined: break if not self.has_class: return fts for c in self.class_list: fts |= set(c.features) for feature in fts: if ( fighting_style_defined and feature.name == "Fighting Style (Select One)" ): temp_feature = feature fts.remove(temp_feature) break if self.race is not None: fts |= set(getattr(self.race, "features", ())) # some races have level-based features (Ex: Aasimar) if hasattr(self.race, "features_by_level"): for lvl in range(1, self.level + 1): fts |= set(self.race.features_by_level[lvl]) if self.background is not None: fts |= set(getattr(self.background, "features", ())) return sorted(tuple(fts), key=(lambda x: x.name)) @property def custom_features_text(self): return tuple([f.name for f in self.custom_features]) def has_feature(self, feat): return any([isinstance(f, feat) for f in self.features]) @property def saving_throw_proficiencies(self): if self.primary_class is None: return self._saving_throw_proficiencies else: return ( self._saving_throw_proficiencies or self.primary_class.saving_throw_proficiencies ) @saving_throw_proficiencies.setter def saving_throw_proficiencies(self, vals): self._saving_throw_proficiencies = vals @property def spellcasting_classes(self): return [c for c in self.class_list if c.is_spellcaster] @property def spellcasting_classes_excluding_warlock(self): return [c for c in self.spellcasting_classes if not type(c) == classes.Warlock] @property def is_spellcaster(self): return len(self.spellcasting_classes) > 0 def spell_slots(self, spell_level): warlock_slots = 0 for c in self.spellcasting_classes: if type(c) is classes.Warlock: warlock_slots = c.spell_slots(spell_level) if len(self.spellcasting_classes_excluding_warlock) == 0: return warlock_slots if len(self.spellcasting_classes_excluding_warlock) == 1: return ( self.spellcasting_classes_excluding_warlock[0].spell_slots(spell_level) + warlock_slots ) else: if spell_level == 0: return sum([c.spell_slots(0) for c in self.spellcasting_classes]) else: # compute effective level from PHB pg 164 eff_level = 0 for c in self.spellcasting_classes_excluding_warlock: if type(c) in [ classes.Bard, classes.Cleric, classes.Druid, classes.Sorceror, classes.Wizard, ]: eff_level += c.level elif type(c) in [classes.Paladin, classes.Ranger]: eff_level += c.level // 2 elif type(c) in [classes.Fighter, classes.Rogue]: eff_level += c.level // 3 elif type(c) is classes.Artificer: eff_level += math.ceil(c.level / 2) if eff_level == 0: return warlock_slots else: return ( multiclass_spellslots_by_level[eff_level][spell_level] + warlock_slots ) @property def spells(self): spells = set(self._spells) | set(self._spells_prepared) for f in self.features: spells |= set(f.spells_known) | set(f.spells_prepared) for c in self.spellcasting_classes: spells |= set(c.spells_known) | set(c.spells_prepared) if self.race is not None: spells |= set(self.race.spells_known) | set(self.race.spells_prepared) return sorted(tuple(spells), key=(lambda x: x.name)) @property def spells_prepared(self): spells = set(self._spells_prepared) for f in self.features: spells |= set(f.spells_prepared) for c in self.spellcasting_classes: spells |= set(c.spells_prepared) if self.race is not None: spells |= set(self.race.spells_prepared) return sorted(tuple(spells), key=(lambda x: x.name)) def set_attrs(self, **attrs): """ Bulk setting of attributes Useful for loading a character from a dictionary """ for attr, val in attrs.items(): if attr == "dungeonsheets_version": pass # Maybe we'll verify this later? elif attr == "weapons": if isinstance(val, str): val = [val] # Treat weapons specially for weap in val: self.wield_weapon(weap) elif attr == "magic_items": if isinstance(val, str): val = [val] for mitem in val: msg = ( f'Magic Item "{mitem}" not defined. ' "Please add it to ``magic_items.py``" ) ThisMagicItem = _resolve_mechanic( mechanic=mitem, SuperClass=magic_items.MagicItem, warning_message=msg, ) self.magic_items.append(ThisMagicItem(owner=self)) elif attr == "weapon_proficiencies": self.other_weapon_proficiencies = () msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``' wps = set( [ _resolve_mechanic( w, SuperClass=weapons.Weapon, warning_message=msg ) for w in val ] ) wps -= set(self.weapon_proficiencies) self.other_weapon_proficiencies = list(wps) elif attr == "armor": self.wear_armor(val) elif attr == "shield": self.wield_shield(val) elif attr == "circle": if hasattr(self, "Druid"): self.Druid.circle = val elif attr == "features": if isinstance(val, str): val = [val] _features = [] for f in val: msg = 'Feature "{}" not defined. Please add it to ``features.py``' ThisFeature = _resolve_mechanic( mechanic=f, SuperClass=features.Feature, warning_message=msg, ) _features.append(ThisFeature) self.custom_features += tuple(F(owner=self) for F in _features) elif (attr == "spells") or (attr == "spells_prepared"): # Create a list of actual spell objects _spells = [] for spell_name in val: msg = 'Spell "{}" not defined. Please add it to ``spells.py``' ThisSpell = _resolve_mechanic( mechanic=spell_name, SuperClass=spells.Spell, warning_message=msg, ) _spells.append(ThisSpell) # Sort by name _spells.sort(key=lambda spell: spell.name) # Save list of spells to character atribute if attr == "spells": # Instantiate them all for the spells list self._spells = tuple(S() for S in _spells) else: # Instantiate them all for the spells list self._spells_prepared = tuple(S() for S in _spells) elif attr == "infusions": if hasattr(self, "Artificer"): _infusions = [] for infusion_name in val: msg = ( "Infusion '{}' not defined. Please add it to" " ``infusions.py``" ) ThisInfusion = _resolve_mechanic( mechanic=infusion_name, SuperClass=infusions.Infusion, warning_message=msg, ) _infusions.append(ThisInfusion) _infusions.sort(key=lambda infusion: infusion.name) self.infusions = tuple(i() for i in _infusions) elif type(val) not in (type, ModuleType): # Some other generic attribute is_unknown = not hasattr(self, attr) and not attr.startswith("_") if is_unknown: warnings.warn( f"Setting unknown character attribute {attr}", RuntimeWarning ) # Lookup general attributes setattr(self, attr, val) def spell_save_dc(self, class_type): ability_mod = getattr(self, class_type.spellcasting_ability).modifier return 8 + self.proficiency_bonus + ability_mod def spell_attack_bonus(self, class_type): ability_mod = getattr(self, class_type.spellcasting_ability).modifier return self.proficiency_bonus + ability_mod def is_proficient(self, weapon: Weapon): """Is the character proficient with this item? Considers class proficiencies and race proficiencies. Parameters ---------- weapon The weapon to be tested for proficiency. Returns ------- Boolean: is this character proficient with this weapon? """ all_proficiencies = self.weapon_proficiencies is_proficient = any((isinstance(weapon, W) for W in all_proficiencies)) return is_proficient @property def proficiencies_text(self): final_text = "" all_proficiencies = tuple(self._proficiencies_text) if self.has_class: all_proficiencies += tuple(self.primary_class._proficiencies_text) if self.num_classes > 1: for c in self.class_list[1:]: all_proficiencies += tuple(c._multiclass_proficiencies_text) if self.race is not None: all_proficiencies += tuple(self.race.proficiencies_text) if self.background is not None: all_proficiencies += tuple(self.background.proficiencies_text) # Create a single string out of all the proficiencies for txt in all_proficiencies: if not final_text: # Capitalize the first entry txt = txt.capitalize() else: # Put a comma first txt = ", " + txt # Add this item to the list text final_text += txt # Add a period at the end final_text += "." return final_text @property def features_text(self): s = "\n\n--".join( [f.name + ("**" if f.needs_implementation else "") for f in self.features] ) if s != "": s = "(See Features Page)\n\n--" + s s += "\n\n=================\n\n" return s @property def magic_items_text(self): s = ", ".join( [ f.name + ("**" if f.needs_implementation else "") for f in sorted(self.magic_items, key=(lambda x: x.name)) ] ) if s: s += ", " return s def wear_armor(self, new_armor): """Accepts a string or Armor class and replaces the current armor. If a string is given, then a subclass of :py:class:`~dungeonsheets.armor.Armor` is retrived from the ``armor.py`` file. Otherwise, an subclass of :py:class:`~dungeonsheets.armor.Armor` can be provided directly. """ if new_armor not in ("", "None", None): if isinstance(new_armor, armor.Armor): new_armor = new_armor else: msg = 'Unnown armor "{}". Please add it to ``armor.py``.' NewArmor = _resolve_mechanic( mechanic=new_armor, SuperClass=armor.Armor, warning_message=msg, ) new_armor = NewArmor() self.armor = new_armor def wield_shield(self, shield): """Accepts a string or Shield class and replaces the current armor. If a string is given, then a subclass of :py:class:`~dungeonsheets.armor.Shield` is retrived from the ``armor.py`` file. Otherwise, an subclass of :py:class:`~dungeonsheets.armor.Shield` can be provided directly. """ if shield not in ("", "None", None): try: NewShield = find_content(shield, valid_classes=[armor.Shield]) except AttributeError: # Not a string, so just treat it as Armor NewShield = shield self.shield = NewShield() def wield_weapon(self, weapon): """Accepts a string and adds it to the list of wielded weapons. Parameters ---------- weapon : str Case-insensitive string with a name of the weapon. """ # Retrieve the weapon class from the weapons module if isinstance(weapon, weapons.Weapon): ThisWeapon = type(weapon) else: msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.' ThisWeapon = _resolve_mechanic( mechanic=weapon, SuperClass=weapons.Weapon, warning_message=msg, ) # Save it to the array self.weapons.append(ThisWeapon(wielder=self)) @property def hit_dice(self): """What type and how many dice to use for re-gaining hit points. To change, set hit_dice_num and hit_dice_faces.""" return " + ".join([f"{c.level}d{c.hit_dice_faces}" for c in self.class_list]) @property def hit_dice_faces(self): # Not a valid function if multiclass if self.num_classes > 1: warnings.warn("hit_dice_faces is not valid for multiclass characters") return self.primary_class.hit_dice_faces @hit_dice_faces.setter def hit_dice_faces(self, faces): self.primary_class.hit_dice_faces = faces @property def proficiency_bonus(self): if self.level < 5: prof = 2 elif 5 <= self.level < 9: prof = 3 elif 9 <= self.level < 13: prof = 4 elif 13 <= self.level < 17: prof = 5 elif 17 <= self.level: prof = 6 return prof def can_assume_shape(self, shape: monsters.Monster): return hasattr(self, "Druid") and self.Druid.can_assume_shape(shape) @property def all_wild_shapes(self): if hasattr(self, "Druid"): return self.Druid.all_wild_shapes else: return () @property def wild_shapes(self): if hasattr(self, "Druid"): return self.Druid.wild_shapes else: return () @wild_shapes.setter def wild_shapes(self, new_shapes): if hasattr(self, "Druid"): self.Druid.wild_shapes = new_shapes @property def infusions_text(self): if hasattr(self, "Artificer"): return tuple([i.name for i in self.infusions]) else: return () @classmethod def load(Cls, char_props: MutableMapping): """Factory Creates a character from the character definition. Parameters ========== char_props Keys and values holding all the attributes of the character. E.g. ``char_props['strength'] = 16`` Returns ======= char The initialized ``Character`` object with associated parameters. """ # Parse the sheet type char_props.pop("sheet_type", "") # Load classes classes = char_props.get("classes", []) # backwards compatability if (len(classes) == 0) and ("character_class" in char_props): char_props["classes"] = [ char_props.pop("character_class").lower().capitalize() ] char_props["levels"] = [str(char_props.pop("level"))] # Create the character with loaded properties char = Cls(**char_props) return char def save(self, filename, template_file="character_template.txt"): # Create the template context context = dict( char=self, ) # Render the template src_path = os.path.join(os.path.dirname(__file__), "forms/") text = ( jinja2.Environment(loader=jinja2.FileSystemLoader(src_path or "./")) .get_template(template_file) .render(context) ) # Save the file with open(filename, mode="w") as f: f.write(text) def to_pdf(self, filename, **kwargs): from dungeonsheets.make_sheets import make_sheet if filename.endswith(".pdf"): filename = filename.replace("pdf", "py") make_sheet(filename, character=self, flatten=kwargs.get("flatten", True)) # Add backwards compatibility for tests class Artificer(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Artificer"] attrs["levels"] = [level] super().__init__(**attrs) class Barbarian(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Barbarian"] attrs["levels"] = [level] super().__init__(**attrs) class Bard(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Bard"] attrs["levels"] = [level] super().__init__(**attrs) class Cleric(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Cleric"] attrs["levels"] = [level] super().__init__(**attrs) class Druid(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Druid"] attrs["levels"] = [level] super().__init__(**attrs) class Fighter(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Fighter"] attrs["levels"] = [level] super().__init__(**attrs) class Monk(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Monk"] attrs["levels"] = [level] super().__init__(**attrs) class Paladin(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Paladin"] attrs["levels"] = [level] super().__init__(**attrs) class Ranger(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Ranger"] attrs["levels"] = [level] super().__init__(**attrs) class Rogue(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Rogue"] attrs["levels"] = [level] super().__init__(**attrs) class Sorceror(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Sorceror"] attrs["levels"] = [level] super().__init__(**attrs) class Warlock(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Warlock"] attrs["levels"] = [level] super().__init__(**attrs) class Wizard(Character): def __init__(self, level=1, **attrs): attrs["classes"] = ["Wizard"] attrs["levels"] = [level] super().__init__(**attrs)