diff --git a/VERSION b/VERSION index 7ceb040..7deb86f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 \ No newline at end of file +0.7.1 \ No newline at end of file diff --git a/dungeonsheets/ToDoNotes.txt b/dungeonsheets/ToDoNotes.txt index aebc5db..0a9d32d 100644 --- a/dungeonsheets/ToDoNotes.txt +++ b/dungeonsheets/ToDoNotes.txt @@ -1,2 +1,3 @@ -Add multiclassing hit dice Add Character.save() option to save to text file +Add Inspiration points +Test Multiclass diff --git a/dungeonsheets/__init__.py b/dungeonsheets/__init__.py index b7ef393..9acc344 100644 --- a/dungeonsheets/__init__.py +++ b/dungeonsheets/__init__.py @@ -1,3 +1,2 @@ from . import weapons, character -__VERSION__ = "0.7.0" diff --git a/dungeonsheets/background.py b/dungeonsheets/background.py index 23153e7..d30abc5 100644 --- a/dungeonsheets/background.py +++ b/dungeonsheets/background.py @@ -1,6 +1,8 @@ class Background(): name = "Generic background" skill_proficiencies = () + weapon_proficiencies = () + proficiencies_text = () languages = () def __str__(self): diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 005dc90..10b0d12 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -38,11 +38,9 @@ class Character(): intelligence = Ability() wisdom = Ability() charisma = Ability() - saving_throw_proficiencies = [] skill_proficiencies = tuple() class_skill_choices = tuple() num_skill_choices = 2 - weapon_proficiencies = tuple() proficiencies_extra = tuple() languages = "" # Skills @@ -90,8 +88,23 @@ class Character(): def __init__(self, **attrs): """Takes a bunch of attrs and passes them to ``set_attrs``""" self.weapons = [] + # make sure class, race, background are set first + class_list = attrs.pop('class_list', self.class_list) + race = attrs.pop('race', self.race) + background = attrs.pop('background', self.background) + self.set_attrs(**{'class_list': class_list, + 'race': race, + 'background': background}) self.set_attrs(**attrs) - + for c in self.class_list: + if isinstance(c, classes.Druid): + ws = self.wild_shapes + c.wild_shapes = ws + c.circle = self.circle + self.all_wild_shapes = c.all_wild_shapes + self.wild_shapes = c.wild_shapes + self.can_assume_shape = c.can_assume_shape + def __str__(self): return self.name @@ -107,13 +120,41 @@ class Character(): return sum(c.class_level for c in self.class_list) @property - def primary_class(self): - # for now, assume first class given must be primary class - return self.class_list[0] + def num_classes(self): + return len(self.class_list) + @property + def class_initialized(self): + return (self.num_classes > 0) + + @property + def primary_class(self): + # for now, assume first class given must be primary class + if self.class_initialized: + return self.class_list[0] + else: + return None + + @property + def weapon_proficiencies(self): + if not self.class_initialized: + return () + 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 wp + @property def saving_throw_proficiencies(self): - return self.primary_class.saving_throw_proficiencies + if self.primary_class is not None: + return self.primary_class.saving_throw_proficiencies + else: + return () @property def spellcasting_classes(self): @@ -194,19 +235,24 @@ class Character(): The weapon to be tested for proficiency. """ - all_proficiencies = tuple(self.weapon_proficiencies) - all_proficiencies += tuple(getattr(self.race, 'weapon_proficiencies', - tuple())) + 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 = self._proficiencies_text + all_proficiencies = set(self._proficiencies_text) + if self.class_initialized: + all_proficiencies |= set(self.primary_class._proficiencies_text) + if self.num_classes > 1: + for c in self.class_list[1:]: + all_proficiencies |= set(c._multiclass_proficiencies_text) if self.race is not None: - all_proficiencies += self.race.proficiencies_text - all_proficiencies += self.proficiencies_extra + all_proficiencies |= set(self.race.proficiencies_text) + if self.background is not None: + all_proficiencies |= set(self.background.proficiencies_text) + all_proficiencies |= set(self.proficiencies_extra) # Create a single string out of all the proficiencies for txt in all_proficiencies: if not final_text: @@ -290,7 +336,8 @@ 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 f"{self.level}d{self.hit_dice_faces}" + return ' + '.join([f'{c.class_level}d{c.hit_dice_faces}' + for c in self.class_list]) @property def proficiency_bonus(self): @@ -333,6 +380,8 @@ class Character(): subclasses = char_props.pop('subclasses', []) if isinstance(subclasses, str): subclasses = [subclasses] + if len(subclasses) == 0: + subclasses = [None]*len(classes_levels) assert len(classes_levels) == len(subclasses), ( 'the length of classes_levels {:d} does not match length of ' 'subclasses {:d}'.format(len(classes_levels), len(subclasses))) diff --git a/dungeonsheets/classes.py b/dungeonsheets/classes.py index be14fe6..bd57724 100644 --- a/dungeonsheets/classes.py +++ b/dungeonsheets/classes.py @@ -14,13 +14,14 @@ class CharClass(): class_name = "" class_level = 1 hit_dice_faces = None - _proficiencies_text = () weapon_proficiencies = () + _proficiencies_text = () multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = () languages = () class_skill_choices = () num_skill_choices = 2 - spellcasing_ability = None + spellcasting_ability = None spell_slots_by_level = None subclass = None class_features_by_level = {lvl: () for lvl in range(1, 21)} @@ -254,7 +255,7 @@ class Druid(CharClass): max_cr = 1 max_swim = None max_fly = None - # Make adjustments for moon cirlce druids + # Make adjustments for moon circle druids if self.circle.lower() == "moon": if 2 <= self.class_level < 6: max_cr = 1 diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index 28e6071..64417f9 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -58,12 +58,10 @@ PDFTK_CMD = 'pdftk' def text_box(string): """Format a string for displaying in a text box.""" - # Remove line breaks - # new_string = string.replace('\n', ' ').replace('\r', ' ') - new_string = string - # Remove multiple whitespace - # new_string = ' '.join(new_string.split()) - new_string = new_string + # remove multiple whitespace without removing linebreaks + new_string = ' '.join(string.replace('\n', '\m').split()) + # Remove *single* line breaks, swap *multi* line breaks to single (fdf: \r) + new_string = new_string.replace('\m \m', '\r').replace('\m\m', '\r').replace('\m', ' ') return new_string @@ -110,11 +108,11 @@ def create_latex_pdf(char, basename, template): def create_spells_pdf(char, spell_class, basename, flatten=False): - class_level = (spell_class.class_name + ' ' + str(spell_class.level)) + class_level = (spell_class.class_name + ' ' + str(spell_class.class_level)) spell_level = lambda x : (x or '') fields = { 'Spellcasting Class 2': class_level, - 'SpellcastingAbility 2': spell_class.spellcasting_ability.capitalize(), + 'SpellcastingAbility 2': spell_class.spellcasting_ability.upper()[:3], 'SpellSaveDC 2': char.spell_save_dc(spell_class), 'SpellAtkBonus 2': mod_str(char.spell_attack_bonus(spell_class)), # Number of spell slots @@ -177,76 +175,76 @@ def create_spells_pdf(char, spell_class, basename, flatten=False): def create_character_pdf(char, basename, flatten=False): # Prepare the list of fields class_level = ' / '.join([f'{c.class_name} {c.class_level}' - for c in char.class_list]) + for c in char.class_list]) fields = { # Character description - 'CharacterName': character.name, + 'CharacterName': char.name, 'ClassLevel': class_level, - 'Background': str(character.background), - 'PlayerName': character.player_name, - 'Race ': str(character.race), - 'Alignment': character.alignment, - 'XP': str(character.xp), + 'Background': str(char.background), + 'PlayerName': char.player_name, + 'Race ': str(char.race), + 'Alignment': char.alignment, + 'XP': str(char.xp), # Abilities - '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, + '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, # Saving throws (proficiencies handled later) - '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), + '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), # Skills (proficiencies handled below) - '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.persuasian), - 'Religion': mod_str(character.religion), - 'SleightofHand': mod_str(character.sleight_of_hand), - 'Stealth ': mod_str(character.stealth), - 'Survival': mod_str(character.survival), + '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.persuasian), + 'Religion': mod_str(char.religion), + 'SleightofHand': mod_str(char.sleight_of_hand), + 'Stealth ': mod_str(char.stealth), + 'Survival': mod_str(char.survival), # Hit points - 'HDTotal': character.hit_dice, - 'HPMax': str(character.hp_max), + 'HDTotal': char.hit_dice, + 'HPMax': str(char.hp_max), # Personality traits and other features - '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_and_traits), + '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_and_traits), # Inventory - 'CP': character.cp, - 'SP': character.sp, - 'EP': character.ep, - 'GP': character.gp, - 'PP': character.pp, - 'Equipment': text_box(character.equipment), + 'CP': char.cp, + 'SP': char.sp, + 'EP': char.ep, + 'GP': char.gp, + 'PP': char.pp, + 'Equipment': text_box(char.equipment), } # Check boxes for proficiencies ST_boxes = { @@ -257,7 +255,7 @@ def create_character_pdf(char, basename, flatten=False): 'wisdom': 'Check Box 21', 'charisma': 'Check Box 22', } - for ability in character.saving_throw_proficiencies: + for ability in char.saving_throw_proficiencies: fields[ST_boxes[ability]] = CHECKBOX_ON # Add skill proficiencies skill_boxes = { @@ -280,7 +278,7 @@ def create_character_pdf(char, basename, flatten=False): 'stealth': 'Check Box 39', 'survival': 'Check Box 40', } - for skill in character.skill_proficiencies: + for skill in char.skill_proficiencies: try: fields[skill_boxes[skill.replace(' ', '_').lower()]] = CHECKBOX_ON except KeyError: @@ -289,20 +287,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, character.weapons): + for _fields, weapon in zip(weapon_fields, char.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: {character.armor}' - attack_str += '\n\r' - attack_str += f'Shield: {character.shield}' - attack_str += character.attacks_and_spellcasting + attack_str = f'Armor: {char.armor}' + attack_str += '\n \n' + attack_str += f'Shield: {char.shield}' + attack_str += '\n \n' + attack_str += char.attacks_and_spellcasting fields['AttacksSpellcasting'] = text_box(attack_str) # Other proficiencies and languages - prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text) - prof_text += "\n\nLanguages:\n" + text_box(character.languages) + prof_text = "Proficiencies:\n" + text_box(char.proficiencies_text) + prof_text += "\n\nLanguages:\n" + text_box(char.languages) fields['ProficienciesLang'] = prof_text # Prepare the actual PDF dirname = os.path.dirname(os.path.abspath(__file__)) @@ -405,8 +404,8 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): subprocess.call(popenargs) # Clean up temporary files os.remove(fdfname) - - + + def make_sheet(character_file, char=None, flatten=False): """Prepare a PDF character sheet from the given character file. @@ -426,7 +425,7 @@ def make_sheet(character_file, char=None, flatten=False): char_base = os.path.splitext(character_file)[0] + '_char' sheets = [char_base + '.pdf'] pages = [] - char_pdf = create_character_pdf(character=char, basename=char_base, + char_pdf = create_character_pdf(char=char, basename=char_base, flatten=flatten) pages.append(char_pdf) for spell_class in char.spellcasting_classes: @@ -443,7 +442,7 @@ def make_sheet(character_file, char=None, flatten=False): # Create spell book spellbook_base = os.path.splitext(character_file)[0] + '_spellbook' try: - create_spellbook_pdf(character=char, basename=spellbook_base) + create_spellbook_pdf(char=char, basename=spellbook_base) except exceptions.LatexNotFoundError as e: log.warning('``pdflatex`` not available. Skipping spellbook ' f'for {char.name}') @@ -454,7 +453,7 @@ def make_sheet(character_file, char=None, flatten=False): if len(wild_shapes) > 0: shapes_base = os.path.splitext(character_file)[0] + '_wild_shapes' try: - create_druid_shapes_pdf(character=char, basename=shapes_base) + create_druid_shapes_pdf(char=char, basename=shapes_base) except exceptions.LatexNotFoundError as e: log.warning('``pdflatex`` not available. Skipping wild shapes list ' f'for {char.name}') diff --git a/examples/druid.pdf b/examples/druid.pdf index bfc5040..8a1a08d 100644 Binary files a/examples/druid.pdf and b/examples/druid.pdf differ diff --git a/examples/druid_orig.pdf b/examples/druid_orig.pdf new file mode 100644 index 0000000..bfc5040 Binary files /dev/null and b/examples/druid_orig.pdf differ diff --git a/examples/makefile b/examples/makefile new file mode 100644 index 0000000..93247b7 --- /dev/null +++ b/examples/makefile @@ -0,0 +1,27 @@ +all: wizard druid rogue warlock + +clobber: + rm -f wizard.pdf rogue.pdf warlock.pdf druid.pdf + +redo: clobber all + +wizard: wizard.pdf + +rogue: rogue.pdf + +warlock: warlock.pdf + +druid: druid.pdf + +wizard.pdf: wizard.py + makesheets wizard.py + +rogue.pdf: rogue.py + makesheets rogue.py + +warlock.pdf: warlock.py + makesheets warlock.py + +druid.pdf: druid.py + makesheets druid.py + diff --git a/examples/rogue.pdf b/examples/rogue.pdf index 05378a4..a9fdcbc 100644 Binary files a/examples/rogue.pdf and b/examples/rogue.pdf differ diff --git a/examples/rogue_orig.pdf b/examples/rogue_orig.pdf new file mode 100644 index 0000000..05378a4 Binary files /dev/null and b/examples/rogue_orig.pdf differ diff --git a/examples/warlock.pdf b/examples/warlock.pdf index 4e4c840..dd926e8 100644 Binary files a/examples/warlock.pdf and b/examples/warlock.pdf differ diff --git a/examples/warlock_orig.pdf b/examples/warlock_orig.pdf new file mode 100644 index 0000000..4e4c840 Binary files /dev/null and b/examples/warlock_orig.pdf differ diff --git a/examples/wizard.pdf b/examples/wizard.pdf index c9b6094..c91ce78 100644 Binary files a/examples/wizard.pdf and b/examples/wizard.pdf differ diff --git a/examples/wizard.py b/examples/wizard.py index cf38751..ff8ad74 100644 --- a/examples/wizard.py +++ b/examples/wizard.py @@ -67,29 +67,29 @@ flaws = """I’ll do just about anything to uncover historical secrets that would add to my research.""" features_and_traits = ( - """Spellcasting Ability: Intelligence is your spellcasting ability for + """*Spellcasting Ability: Intelligence is your spellcasting ability for your spells. The saving throw DC to resist a spell you cast is 13. Your attack bonus when you make an attack with a spell is +5. See the rulebook for rules on casting your spells. - Arcane Recovery: You can regain some of your magical energy by + *Arcane Recovery: You can regain some of your magical energy by studying your spellbook. Once per day during a short rest, you can choose to recover expended spell slots with a combined level equal to or less than half your wizard level (rounded up). - Darkvision: You see in dim light within a 60-foot radius of you as + *Darkvision: You see in dim light within a 60-foot radius of you as if it were bright light, and in darkness in that radius as if it were dim light. You can’t discern color in darkness, only shades of gray. - Fey Ancestry: You have advantage on saving throws against being + *Fey Ancestry: You have advantage on saving throws against being charmed, and magic can’t put you to sleep. - Trance: Elves don’t need to sleep. They meditate deeply, remaining + *Trance: Elves don’t need to sleep. They meditate deeply, remaining semiconscious, for 4 hours a day and gain the same benefit a human does from 8 hours of sleep. - - Shelter of the Faithful: As a servant of Oghma, you command the + + *Shelter of the Faithful: As a servant of Oghma, you command the respect of those who share your faith, and you can perform the rites of Oghma. You and your companions can expect to receive free healing and care at a temple, shrine, or other established diff --git a/examples/wizard_orig.pdf b/examples/wizard_orig.pdf new file mode 100644 index 0000000..c9b6094 Binary files /dev/null and b/examples/wizard_orig.pdf differ diff --git a/setup.py b/setup.py index a8ff037..e498032 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,11 @@ setup(name='dungeonsheets', download_url = 'https://github.com/canismarko/dungeon-sheets/archive/master.zip', packages=['dungeonsheets'], package_data={ - 'dungeonsheets': ['blank-character-sheet-default.pdf', 'blank-spell-sheet-default.pdf', + 'dungeonsheets': ['blank-character-sheet-default.pdf', + 'blank-spell-sheet-default.pdf', 'spellbook_template.tex', '../VERSION', - 'character_template.txt'] + 'character_template.txt', + 'druid_shapes_template.tex'] }, install_requires=[ 'fdfgen', 'npyscreen', 'jinja2', 'pdfrw',