diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index ca96930..c9702e8 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -62,6 +62,8 @@ class Character(): alignment = "Neutral" dungeonsheets_version = __version__ class_list = [] + _level = 1 # Keep internal check of total level + _hit_dice_faces = 2 race = None background = None xp = 0 @@ -74,6 +76,7 @@ class Character(): intelligence = Ability() wisdom = Ability() charisma = Ability() + _saving_throw_proficiencies = [] other_weapon_proficiencies = tuple() skill_proficiencies = tuple() skill_expertise = tuple() @@ -82,24 +85,24 @@ class Character(): proficiencies_extra = tuple() languages = "" # Skills - acrobatics = Skill(ability='dexterity', name='acrobatics') - animal_handling = Skill(ability='wisdom', name='animal handling') - arcana = Skill(ability='intelligence', name='arcana') - athletics = Skill(ability='strength', name='athletics') - deception = Skill(ability='charisma', name='deception') - history = Skill(ability='intelligence', name='history') - insight = Skill(ability='wisdom', name='insight') - intimidation = Skill(ability='charisma', name='intimidation') - investigation = Skill(ability='intelligence', name='investigation') - medicine = Skill(ability='wisdom', name='medicine') - nature = Skill(ability='intelligence', name='nature') - perception = Skill(ability='wisdom', name='perception') - performance = Skill(ability='charisma', name='performance') - persuasion = Skill(ability='charisma', name='persuasion') - religion = Skill(ability='intelligence', name='religion') - sleight_of_hand = Skill(ability='dexterity', name='sleight of hand') - stealth = Skill(ability='dexterity', name='stealth') - survival = Skill(ability='wisdom', name='survival') + acrobatics = Skill(ability='dexterity') + animal_handling = Skill(ability='wisdom') + arcana = Skill(ability='intelligence') + athletics = Skill(ability='strength') + deception = Skill(ability='charisma') + history = Skill(ability='intelligence') + insight = Skill(ability='wisdom') + intimidation = Skill(ability='charisma') + investigation = Skill(ability='intelligence') + medicine = Skill(ability='wisdom') + nature = Skill(ability='intelligence') + perception = Skill(ability='wisdom') + performance = Skill(ability='charisma') + persuasion = Skill(ability='charisma') + religion = Skill(ability='intelligence') + sleight_of_hand = Skill(ability='dexterity') + stealth = Skill(ability='dexterity') + survival = Skill(ability='wisdom') # Characteristics attacks_and_spellcasting = "" personality_traits = "" @@ -146,7 +149,7 @@ class Character(): @property def class_name(self): - return ' / '.join([f'{c.class_name} {c.class_level}' + return ' / '.join([f'{c.class_name}' for c in self.class_list]) @property @@ -159,8 +162,21 @@ class Character(): @property def level(self): - return sum(c.class_level for c in self.class_list) + if self.num_classes == 0: + return self._level + else: + return sum(c.class_level for c in self.class_list) + @level.setter + def level(self, new_level): + if self.num_classes == 0: + self._level = new_level + else: + self.primary_class.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.class_name)) + @property def num_classes(self): return len(self.class_list) @@ -185,9 +201,8 @@ class Character(): @property def weapon_proficiencies(self): wp = set(self.other_weapon_proficiencies) - if not self.class_initialized: - return wp - wp |= set(self.primary_class.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) @@ -196,6 +211,10 @@ class Character(): 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 features(self): @@ -220,10 +239,15 @@ class Character(): @property def saving_throw_proficiencies(self): - if self.primary_class is not None: - return self.primary_class.saving_throw_proficiencies + if self.primary_class is None: + return self._saving_throw_proficiencies else: - return () + 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): @@ -304,14 +328,6 @@ class Character(): self.wear_armor(val) elif attr == 'shield': self.wield_shield(val) - elif attr == 'wild_shapes': - for c in self.class_list: - if isinstance(c, classes.Druid): - c.wild_shapes = val - self.all_wild_shapes = c.all_wild_shapes - self.wild_shapes = c.wild_shapes - self.can_assume_shape = c.can_assume_shape - break elif attr == 'circle': for c in self.class_list: if isinstance(c, classes.Druid) and (c.circle == ''): @@ -393,17 +409,17 @@ class Character(): @property def proficiencies_text(self): final_text = "" - all_proficiencies = set(self._proficiencies_text) + all_proficiencies = tuple(self._proficiencies_text) if self.class_initialized: - all_proficiencies |= set(self.primary_class._proficiencies_text) + all_proficiencies += tuple(self.primary_class._proficiencies_text) if self.num_classes > 1: for c in self.class_list[1:]: - all_proficiencies |= set(c._multiclass_proficiencies_text) + all_proficiencies += tuple(c._multiclass_proficiencies_text) if self.race is not None: - all_proficiencies |= set(self.race.proficiencies_text) + all_proficiencies += tuple(self.race.proficiencies_text) if self.background is not None: - all_proficiencies |= set(self.background.proficiencies_text) - all_proficiencies |= set(self.proficiencies_extra) + all_proficiencies += tuple(self.background.proficiencies_text) + all_proficiencies += tuple(self.proficiencies_extra) # Create a single string out of all the proficiencies for txt in all_proficiencies: if not final_text: @@ -499,8 +515,24 @@ class Character(): """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.class_level}d{c.hit_dice_faces}' - for c in self.class_list]) + if self.num_classes == 0: + return '{:d}d{:d}'.format(self.level, self._hit_dice_faces) + else: + return ' + '.join([f'{c.class_level}d{c.hit_dice_faces}' + for c in self.class_list]) + + @property + def hit_dice_faces(self): + if self.num_classes == 0: + return self._hit_dice_faces + else: # 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._hit_dice_faces = faces @property def proficiency_bonus(self): @@ -540,6 +572,33 @@ class Character(): ac += [f.AC_func(self) for f in self.features] return max(ac) + def can_assume_shape(self, shape: monsters.Monster): + for c in self.class_list: + if isinstance(c, classes.Druid): + return c.can_assume_shape(shape) + return False + + @property + def all_wild_shapes(self): + for c in self.class_list: + if isinstance(c, classes.Druid): + return c.all_wild_shapes + return () + + @property + def wild_shapes(self): + for c in self.class_list: + if isinstance(c, classes.Druid): + return c.wild_shapes + return () + + @wild_shapes.setter + def wild_shapes(self, new_shapes): + for c in self.class_list: + if isinstance(c, classes.Druid): + c.wild_shapes = new_shapes + + @classmethod def load(cls, character_file): # Create a character from the character definition @@ -651,3 +710,21 @@ def read_character_file(filename): if prop_name[0:2] != '__': char_props[prop_name] = getattr(module, prop_name) return char_props + + +# Add backwards compatability for tests +class Druid(Character): + + def __init__(self, level=1, **attrs): + MyDruid = classes.Druid(level=level) + + self.class_list = [MyDruid] + super().__init__(**attrs) + + +class Wizard(Character): + + def __init__(self, level=1, **attrs): + self.class_list = [classes.Wizard(level=level)] + super().__init__(**attrs) + diff --git a/dungeonsheets/classes/druid.py b/dungeonsheets/classes/druid.py index f8d5173..acf59f8 100644 --- a/dungeonsheets/classes/druid.py +++ b/dungeonsheets/classes/druid.py @@ -84,6 +84,7 @@ class ShepherdCircle(SubClass): class Druid(CharClass): class_name = 'Druid' _wild_shapes = () + _circle = '' hit_dice_faces = 8 saving_throw_proficiencies = ('intelligence', 'wisdom') languages = 'Druidic' @@ -144,8 +145,15 @@ class Druid(CharClass): if isinstance(self.subclass, SubClass): return self.subclass.circle.lower() else: - return '' - + return self._circle + + @circle.setter + def circle(self, circle_str): + if isinstance(self.subclass, SubClass): + self.subclass = self.select_subclass(circle_str) + else: + self._circle = circle_str + @property def all_wild_shapes(self): """Return all wild shapes, regardless of validity.""" diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index fb2246c..043d252 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -69,23 +69,23 @@ def text_box(string): return new_string -def create_druid_shapes_pdf(char, basename): +def create_druid_shapes_pdf(character, basename): template = jinja_env.get_template('druid_shapes_template.tex') - return create_latex_pdf(char, basename, template) + return create_latex_pdf(character, basename, template) -def create_spellbook_pdf(char, basename): +def create_spellbook_pdf(character, basename): template = jinja_env.get_template('spellbook_template.tex') - return create_latex_pdf(char, basename, template) + return create_latex_pdf(character, basename, template) -def create_features_pdf(char, basename): +def create_features_pdf(character, basename): template = jinja_env.get_template('features_template.tex') - return create_latex_pdf(char, basename, template) + return create_latex_pdf(character, basename, template) -def create_latex_pdf(char, basename, template): - tex = template.render(character=char) +def create_latex_pdf(character, basename, template): + tex = template.render(character=character) # Create tex document tex_file = f'{basename}.tex' with open(tex_file, mode='w') as f: @@ -116,15 +116,15 @@ def create_latex_pdf(char, basename, template): raise exceptions.LatexError(f'Processing of {basename}.tex failed.') -def create_spells_pdf(char, basename, flatten=False): +def create_spells_pdf(character, basename, flatten=False): class_level = ' / '.join([c.class_name + ' ' + str(c.class_level) - for c in char.spellcasting_classes]) + for c in character.spellcasting_classes]) abilities = ' / '.join([c.spellcasting_ability.upper()[:3] - for c in char.spellcasting_classes]) - DCs = ' / '.join([str(char.spell_save_dc(c)) - for c in char.spellcasting_classes]) - bonuses = ' / '.join([mod_str(char.spell_attack_bonus(c)) - for c in char.spellcasting_classes]) + for c in character.spellcasting_classes]) + DCs = ' / '.join([str(character.spell_save_dc(c)) + for c in character.spellcasting_classes]) + bonuses = ' / '.join([mod_str(character.spell_attack_bonus(c)) + for c in character.spellcasting_classes]) spell_level = lambda x : (x or 0) fields = { 'Spellcasting Class 2': class_level, @@ -132,19 +132,19 @@ def create_spells_pdf(char, basename, flatten=False): 'SpellSaveDC 2': DCs, 'SpellAtkBonus 2': bonuses, # Number of spell slots - 'SlotsTotal 19': spell_level(char.spell_slots(1)), - 'SlotsTotal 20': spell_level(char.spell_slots(2)), - 'SlotsTotal 21': spell_level(char.spell_slots(3)), - 'SlotsTotal 22': spell_level(char.spell_slots(4)), - 'SlotsTotal 23': spell_level(char.spell_slots(5)), - 'SlotsTotal 24': spell_level(char.spell_slots(6)), - 'SlotsTotal 25': spell_level(char.spell_slots(7)), - 'SlotsTotal 26': spell_level(char.spell_slots(8)), - 'SlotsTotal 27': spell_level(char.spell_slots(9)), + 'SlotsTotal 19': spell_level(character.spell_slots(1)), + 'SlotsTotal 20': spell_level(character.spell_slots(2)), + 'SlotsTotal 21': spell_level(character.spell_slots(3)), + 'SlotsTotal 22': spell_level(character.spell_slots(4)), + 'SlotsTotal 23': spell_level(character.spell_slots(5)), + 'SlotsTotal 24': spell_level(character.spell_slots(6)), + 'SlotsTotal 25': spell_level(character.spell_slots(7)), + 'SlotsTotal 26': spell_level(character.spell_slots(8)), + 'SlotsTotal 27': spell_level(character.spell_slots(9)), } # Cantrips cantrip_fields = (f'Spells 10{i}' for i in (14, 16, 17, 18, 19, 20, 21, 22)) - cantrips = (spl for spl in char.spells if spl.level == 0) + cantrips = (spl for spl in character.spells if spl.level == 0) for spell, field_name in zip(cantrips, cantrip_fields): fields[field_name] = str(spell) # Spells for each level @@ -171,13 +171,13 @@ def create_spells_pdf(char, basename, flatten=False): 9: (327, 326, 3079, 3080, 3081, 3082, 3083, ), } for level in field_numbers.keys(): - spells = tuple(spl for spl in char.spells if spl.level == level) + spells = tuple(spl for spl in character.spells if spl.level == level) field_names = tuple(f'Spells {i}' for i in field_numbers[level]) prep_names = tuple(f'Check Box {i}' for i in prep_numbers[level]) for spell, field, chk_field in zip(spells, field_names, prep_names): fields[field] = str(spell) is_prepared = any([spell == Spl - for Spl in char.spells_prepared]) + for Spl in character.spells_prepared]) fields[chk_field] = CHECKBOX_ON if is_prepared else CHECKBOX_OFF # # Uncomment to post field names instead: # for field in field_names: @@ -188,77 +188,77 @@ def create_spells_pdf(char, basename, flatten=False): make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) -def create_character_pdf(char, basename, flatten=False): +def create_character_pdf(character, basename, flatten=False): # Prepare the list of fields fields = { # Character description - 'CharacterName': char.name, - 'ClassLevel': char.class_name, - 'Background': str(char.background), - 'PlayerName': char.player_name, - 'Race ': str(char.race), - 'Alignment': char.alignment, - 'XP': str(char.xp), + 'CharacterName': character.name, + 'ClassLevel': character.class_name, + 'Background': str(character.background), + 'PlayerName': character.player_name, + 'Race ': str(character.race), + 'Alignment': character.alignment, + 'XP': str(character.xp), # Abilities - 'ProfBonus': mod_str(char.proficiency_bonus), - 'STRmod': str(char.strength.value), - 'STR': mod_str(char.strength.modifier), - 'DEXmod ': str(char.dexterity.value), - 'DEX': mod_str(char.dexterity.modifier), - 'CONmod': str(char.constitution.value), - 'CON': mod_str(char.constitution.modifier), - 'INTmod': str(char.intelligence.value), - 'INT': mod_str(char.intelligence.modifier), - 'WISmod': str(char.wisdom.value), - 'WIS': mod_str(char.wisdom.modifier), - 'CHamod': str(char.charisma.value), - 'CHA': mod_str(char.charisma.modifier), - 'AC': str(char.armor_class), - 'Initiative': mod_str(char.dexterity.modifier), - 'Speed': str(char.speed), - 'Passive': 10 + char.perception, + 'ProfBonus': mod_str(character.proficiency_bonus), + 'STRmod': str(character.strength.value), + 'STR': mod_str(character.strength.modifier), + 'DEXmod ': str(character.dexterity.value), + 'DEX': mod_str(character.dexterity.modifier), + 'CONmod': str(character.constitution.value), + 'CON': mod_str(character.constitution.modifier), + 'INTmod': str(character.intelligence.value), + 'INT': mod_str(character.intelligence.modifier), + 'WISmod': str(character.wisdom.value), + 'WIS': mod_str(character.wisdom.modifier), + 'CHamod': str(character.charisma.value), + 'CHA': mod_str(character.charisma.modifier), + 'AC': str(character.armor_class), + 'Initiative': mod_str(character.dexterity.modifier), + 'Speed': str(character.speed), + 'Passive': 10 + character.perception, # Saving throws (proficiencies handled later) - 'ST Strength': mod_str(char.strength.saving_throw), - 'ST Dexterity': mod_str(char.dexterity.saving_throw), - 'ST Constitution': mod_str(char.constitution.saving_throw), - 'ST Intelligence': mod_str(char.intelligence.saving_throw), - 'ST Wisdom': mod_str(char.wisdom.saving_throw), - 'ST Charisma': mod_str(char.charisma.saving_throw), + 'ST Strength': mod_str(character.strength.saving_throw), + 'ST Dexterity': mod_str(character.dexterity.saving_throw), + 'ST Constitution': mod_str(character.constitution.saving_throw), + 'ST Intelligence': mod_str(character.intelligence.saving_throw), + 'ST Wisdom': mod_str(character.wisdom.saving_throw), + 'ST Charisma': mod_str(character.charisma.saving_throw), # Skills (proficiencies handled below) - 'Acrobatics': mod_str(char.acrobatics), - 'Animal': mod_str(char.animal_handling), - 'Arcana': mod_str(char.arcana), - 'Athletics': mod_str(char.athletics), - 'Deception ': mod_str(char.deception), - 'History ': mod_str(char.history), - 'Insight': mod_str(char.insight), - 'Intimidation': mod_str(char.intimidation), - 'Investigation ': mod_str(char.investigation), - 'Medicine': mod_str(char.medicine), - 'Nature': mod_str(char.nature), - 'Perception ': mod_str(char.perception), - 'Performance': mod_str(char.performance), - 'Persuasion': mod_str(char.persuasion), - 'Religion': mod_str(char.religion), - 'SleightofHand': mod_str(char.sleight_of_hand), - 'Stealth ': mod_str(char.stealth), - 'Survival': mod_str(char.survival), + 'Acrobatics': mod_str(character.acrobatics), + 'Animal': mod_str(character.animal_handling), + 'Arcana': mod_str(character.arcana), + 'Athletics': mod_str(character.athletics), + 'Deception ': mod_str(character.deception), + 'History ': mod_str(character.history), + 'Insight': mod_str(character.insight), + 'Intimidation': mod_str(character.intimidation), + 'Investigation ': mod_str(character.investigation), + 'Medicine': mod_str(character.medicine), + 'Nature': mod_str(character.nature), + 'Perception ': mod_str(character.perception), + 'Performance': mod_str(character.performance), + 'Persuasion': mod_str(character.persuasion), + 'Religion': mod_str(character.religion), + 'SleightofHand': mod_str(character.sleight_of_hand), + 'Stealth ': mod_str(character.stealth), + 'Survival': mod_str(character.survival), # Hit points - 'HDTotal': char.hit_dice, - 'HPMax': str(char.hp_max), + 'HDTotal': character.hit_dice, + 'HPMax': str(character.hp_max), # Personality traits and other features - 'PersonalityTraits ': text_box(char.personality_traits), - 'Ideals': text_box(char.ideals), - 'Bonds': text_box(char.bonds), - 'Flaws': text_box(char.flaws), - 'Features and Traits': text_box(char.features_text + char.features_and_traits), + 'PersonalityTraits ': text_box(character.personality_traits), + 'Ideals': text_box(character.ideals), + 'Bonds': text_box(character.bonds), + 'Flaws': text_box(character.flaws), + 'Features and Traits': text_box(character.features_text + character.features_and_traits), # Inventory - 'CP': char.cp, - 'SP': char.sp, - 'EP': char.ep, - 'GP': char.gp, - 'PP': char.pp, - 'Equipment': text_box(char.equipment), + 'CP': character.cp, + 'SP': character.sp, + 'EP': character.ep, + 'GP': character.gp, + 'PP': character.pp, + 'Equipment': text_box(character.equipment), } # Check boxes for proficiencies ST_boxes = { @@ -269,7 +269,7 @@ def create_character_pdf(char, basename, flatten=False): 'wisdom': 'Check Box 21', 'charisma': 'Check Box 22', } - for ability in char.saving_throw_proficiencies: + for ability in character.saving_throw_proficiencies: fields[ST_boxes[ability]] = CHECKBOX_ON # Add skill proficiencies skill_boxes = { @@ -292,7 +292,7 @@ def create_character_pdf(char, basename, flatten=False): 'stealth': 'Check Box 39', 'survival': 'Check Box 40', } - for skill in char.skill_proficiencies: + for skill in character.skill_proficiencies: try: fields[skill_boxes[skill.replace(' ', '_').lower()]] = CHECKBOX_ON except KeyError: @@ -301,21 +301,21 @@ def create_character_pdf(char, basename, flatten=False): weapon_fields = [('Wpn Name', 'Wpn1 AtkBonus', 'Wpn1 Damage'), ('Wpn Name 2', 'Wpn2 AtkBonus ', 'Wpn2 Damage '), ('Wpn Name 3', 'Wpn3 AtkBonus ', 'Wpn3 Damage '),] - for _fields, weapon in zip(weapon_fields, char.weapons): + for _fields, weapon in zip(weapon_fields, character.weapons): name_field, atk_field, dmg_field = _fields fields[name_field] = weapon.name fields[atk_field] = '{:+d}'.format(weapon.attack_bonus) fields[dmg_field] = f'{weapon.damage}/{weapon.damage_type}' # Other attack information - attack_str = f'Armor: {char.armor}' + attack_str = f'Armor: {character.armor}' attack_str += '\n \n' - attack_str += f'Shield: {char.shield}' + attack_str += f'Shield: {character.shield}' attack_str += '\n \n' - attack_str += char.attacks_and_spellcasting + attack_str += character.attacks_and_spellcasting fields['AttacksSpellcasting'] = text_box(attack_str) # Other proficiencies and languages - prof_text = "Proficiencies:\n" + text_box(char.proficiencies_text) - prof_text += "\n\nLanguages:\n" + text_box(char.languages) + prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text) + prof_text += "\n\nLanguages:\n" + text_box(character.languages) fields['ProficienciesLang'] = prof_text # Prepare the actual PDF dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'forms/') @@ -420,63 +420,63 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): os.remove(fdfname) -def make_sheet(character_file, char=None, flatten=False): +def make_sheet(character_file, character=None, flatten=False): """Prepare a PDF character sheet from the given character file. Parameters ---------- character_file : str File (.py) to load character from. Will save PDF using same name - char : Character, optional + character : Character, optional If provided, will not load from the character file, just use file for PDF name flatten : bool, optional If true, the resulting PDF will look better and won't be fillable form. """ - if char is None: - char = character.Character.load(character_file) + if character is None: + character = character.Character.load(character_file) # Set the fields in the FDF char_base = os.path.splitext(character_file)[0] + '_char' sheets = [char_base + '.pdf'] pages = [] - char_pdf = create_character_pdf(char=char, basename=char_base, + char_pdf = create_character_pdf(character=character, basename=char_base, flatten=flatten) pages.append(char_pdf) - if char.is_spellcaster: + if character.is_spellcaster: # Create spell sheet spell_base = '{:s}_spells'.format( os.path.splitext(character_file)[0]) - create_spells_pdf(char=char, basename=spell_base, flatten=flatten) + create_spells_pdf(character=character, basename=spell_base, flatten=flatten) sheets.append(spell_base + '.pdf') - if len(char.features) > 0: + if len(character.features) > 0: feat_base = '{:s}_feats'.format( os.path.splitext(character_file)[0]) try: - create_features_pdf(char=char, basename=feat_base) + create_features_pdf(character=character, basename=feat_base) except exceptions.LatexNotFoundError as e: log.warning('``pdflatex`` not available. Skipping features book ' - f'for {char.name}') + f'for {character.name}') else: sheets.append(feat_base + '.pdf') - if char.is_spellcaster: + if character.is_spellcaster: # Create spell book spellbook_base = os.path.splitext(character_file)[0] + '_spellbook' try: - create_spellbook_pdf(char=char, basename=spellbook_base) + create_spellbook_pdf(character=character, basename=spellbook_base) except exceptions.LatexNotFoundError as e: log.warning('``pdflatex`` not available. Skipping spellbook ' - f'for {char.name}') + f'for {character.name}') else: sheets.append(spellbook_base + '.pdf') # Create a list of Druid wild_shapes - wild_shapes = getattr(char, 'wild_shapes', []) + wild_shapes = getattr(character, 'wild_shapes', []) if len(wild_shapes) > 0: shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes' try: - create_druid_shapes_pdf(char=char, basename=shapes_base) + create_druid_shapes_pdf(character=character, basename=shapes_base) except exceptions.LatexNotFoundError as e: log.warning('``pdflatex`` not available. Skipping wild shapes list ' - f'for {char.name}') + f'for {character.name}') else: sheets.append(shapes_base + '.pdf') # Combine sheets into final pdf @@ -510,6 +510,9 @@ def merge_pdfs(src_filenames, dest_filename, clean_up=False): os.remove(sheet) +load_character_file = character.read_character_file + + def main(): # Prepare an argument parser parser = argparse.ArgumentParser( diff --git a/dungeonsheets/spells/spells.py b/dungeonsheets/spells/spells.py index 33933bf..b9116b5 100644 --- a/dungeonsheets/spells/spells.py +++ b/dungeonsheets/spells/spells.py @@ -30,11 +30,15 @@ class Spell(): materials = "" duration = "instantaneous" ritual = False + _concentration = False magic_school = "" classes = () def __str__(self): - s = self.name + ' ({:s}) '.format(','.join(self.components)) + if len(self.components) == 0: + s = self.name + else: + s = self.name + ' ({:s}) '.format(','.join(self.components)) # Indicate if this is a ritual or a concentration indicators = [('R', self.ritual), ('C', self.concentration), ('$', self.special_material)] indicators = tuple(letter for letter, is_active in indicators if is_active) @@ -60,7 +64,11 @@ class Spell(): @property def concentration(self): - return ('concentration' in self.duration.lower()) + return ('concentration' in self.duration.lower()) or self._concentration + + @concentration.setter + def concentration(self, val: bool): + self._concentration = val @property def special_material(self): diff --git a/dungeonsheets/stats.py b/dungeonsheets/stats.py index d957233..9e854b3 100644 --- a/dungeonsheets/stats.py +++ b/dungeonsheets/stats.py @@ -74,13 +74,12 @@ class Ability(): class Skill(): """An ability-based skill, such as athletics.""" - def __init__(self, ability, name): + def __init__(self, ability): self.ability_name = ability - self.skill_name = name - # def __set_name__(self, character, name): - # self.skill_name = name - # self.character = character + def __set_name__(self, character, name): + self.skill_name = name.lower().replace('_', ' ') + self.character = character def __get__(self, character, owner): ability = getattr(character, self.ability_name)