From bba4f3af3b93e6e8126d41f90ce800d5235dc4ca Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Tue, 30 Jan 2024 22:13:21 +0100 Subject: [PATCH 01/10] Latex feature pages: Sort feats by type This patch gives the option to sort feats by type instead of alphabetically. Basically, it just adds the feats in the same order that dungeonsheets finds them, which goes from character feat to class feats to racial feats to background feats. The patch implements this feature using a command-line option. The user can specify --feats-by-type or -N to order feats by type. Default is alphabetical order. --- dungeonsheets/character.py | 31 +++++++++++++++++++++++ dungeonsheets/forms/features_template.tex | 5 ++-- dungeonsheets/make_sheets.py | 19 +++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index a1c737e2..07f82eef 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -488,6 +488,37 @@ def features(self): return sorted(tuple(fts), key=(lambda x: x.name)) + @property + def features_by_type(self): + fts: dict[str, list[type[Feature]]] = { + "Feats": [], + "Class Features": [], + "Racial Features": [], + "Background Features": [], + } + for c in self.class_list: + for item in list(c.features): + fts["Class Features"].append(item) + # Add player choices, but only if they're not automically given already: + for item in self.custom_features: + if not item in fts: + if item.source == "Feats": + fts["Feats"].append(item) + else: + fts["Class Features"].append(item) + if self.race is not None: + for item in getattr(self.race, "features", ()): + fts["Racial Features"].append(item) + # some races have level-based features (Ex: Aasimar) + if hasattr(self.race, "features_by_level"): + for lvl in range(1, self.level + 1): + for item in list(self.race.features_by_level[lvl]): + fts["Racial Features"].append(item) + if self.background is not None: + for item in getattr(self.background, "features", ()): + fts["Background Features"].append(item) + return [val for key in fts for val in fts[key]] + @property def custom_features_text(self): return tuple([f.name for f in self.custom_features]) diff --git a/dungeonsheets/forms/features_template.tex b/dungeonsheets/forms/features_template.tex index 2949297c..873adaf8 100644 --- a/dungeonsheets/forms/features_template.tex +++ b/dungeonsheets/forms/features_template.tex @@ -1,13 +1,14 @@ \pdfbookmark[0]{Features}{Features} \section*{Features} +[%- if feat_order -%][% set feat_list = character.features_by_type %][% else %][% set feat_list = character.features %][% endif %] [% if use_dnd_decorations %] - [%- for feat in character.features %] + [%- for feat in feat_list %] \pdfbookmark[1]{[[ feat.name ]]}{Features - [[ feat.name ]]} \DndFeatHeader{[[ feat.name ]]}[Source: [[ feat.source ]]] [[- feat.__doc__|rst_to_latex(use_dnd_decorations=use_dnd_decorations) -]] [%- endfor -%] [% else %] - [%- for feat in character.features %] + [%- for feat in feat_list %] \pdfbookmark[1]{[[ feat.name ]]}{Features - [[ feat.name ]]} \subsection*{[[ feat.name ]]} \textbf{Source:} [[ feat.source ]] \\ diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index 623c8207..ea97d621 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -72,7 +72,8 @@ def __call__( character: Character, content_suffix: str = "tex", use_dnd_decorations: bool = False, - spell_order: bool = False + spell_order: bool = False, + feat_order: bool = False, ): template = jinja_env.get_template( self.template_name.format(suffix=content_suffix) @@ -81,6 +82,7 @@ def __call__( character=character, use_dnd_decorations=use_dnd_decorations, spell_order=spell_order, + feat_order=feat_order, ordinals=ORDINALS, ) @@ -164,6 +166,7 @@ def make_sheet( debug: bool = False, use_tex_template: bool = False, spell_order: bool = False, + feat_order: bool = False, ): """Make a character or GM sheet into a PDF. Parameters @@ -204,6 +207,7 @@ def make_sheet( debug=debug, use_tex_template=use_tex_template, spell_order=spell_order, + feat_order=feat_order, ) return ret @@ -394,6 +398,7 @@ def make_character_content( content_format: str, fancy_decorations: bool = False, spell_order: bool = False, + feat_order: bool = False, ) -> List[str]: """Prepare the inner content for a character sheet. @@ -455,6 +460,7 @@ def make_character_content( character, content_suffix=content_format, use_dnd_decorations=fancy_decorations, + feat_order=feat_order, ) ) if character.magic_items: @@ -554,6 +560,7 @@ def make_character_sheet( debug: bool = False, use_tex_template: bool = False, spell_order: bool = False, + feat_order: bool = False, ): """Prepare a PDF character sheet from the given character file. @@ -594,6 +601,7 @@ def make_character_sheet( content_format=content_suffix, fancy_decorations=fancy_decorations, spell_order=spell_order, + feat_order=feat_order, ) # Typeset combined LaTeX file if output_format == "pdf": @@ -703,6 +711,7 @@ def _build(filename, args) -> int: fancy_decorations=args.fancy_decorations, use_tex_template=args.use_tex_template, spell_order=args.spell_order, + feat_order=args.feat_order, ) except exceptions.CharacterFileFormatError: # Only raise the failed exception if this file is explicitly given @@ -748,6 +757,14 @@ def main(args=None): help="Order spells by level in the feature pages.", dest="spell_order", ) + parser.add_argument( + "--feats-by-type", + "-N", + default=False, + action="store_true", + help="Order feats by type in the feature pages.", + dest="feat_order", + ) parser.add_argument( "--fancy-decorations", "--fancy", From 34f0bb773cb45770f402055f5f1b897d2522e6ad Mon Sep 17 00:00:00 2001 From: PJBrs Date: Wed, 3 Sep 2025 19:57:45 +0200 Subject: [PATCH 02/10] Latex feature pages: Add headers for feat types If the user has selected to order feats by type, this patch adds headers for each feat type instead of one Features heading. Headers include Feats (if applicable), Class Features, Racial Features and Background Features. --- dungeonsheets/character.py | 2 +- dungeonsheets/forms/features_template.tex | 41 +++++++++++++---------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 07f82eef..83264ece 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -517,7 +517,7 @@ def features_by_type(self): if self.background is not None: for item in getattr(self.background, "features", ()): fts["Background Features"].append(item) - return [val for key in fts for val in fts[key]] + return fts @property def custom_features_text(self): diff --git a/dungeonsheets/forms/features_template.tex b/dungeonsheets/forms/features_template.tex index 873adaf8..7482c14c 100644 --- a/dungeonsheets/forms/features_template.tex +++ b/dungeonsheets/forms/features_template.tex @@ -1,20 +1,25 @@ -\pdfbookmark[0]{Features}{Features} -\section*{Features} -[%- if feat_order -%][% set feat_list = character.features_by_type %][% else %][% set feat_list = character.features %][% endif %] -[% if use_dnd_decorations %] - [%- for feat in feat_list %] - \pdfbookmark[1]{[[ feat.name ]]}{Features - [[ feat.name ]]} - \DndFeatHeader{[[ feat.name ]]}[Source: [[ feat.source ]]] - [[- feat.__doc__|rst_to_latex(use_dnd_decorations=use_dnd_decorations) -]] - [%- endfor -%] -[% else %] - [%- for feat in feat_list %] - \pdfbookmark[1]{[[ feat.name ]]}{Features - [[ feat.name ]]} - \subsection*{[[ feat.name ]]} - \textbf{Source:} [[ feat.source ]] \\ +[%- if feat_order -%] + [%- set feat_dict = character.features_by_type -%] +[%- else -%] + [%- set feat_dict = {"Features": character.features} -%] +[%- endif -%] +[%- for feat_type, feat_list in feat_dict.items() -%] + [%- if feat_list|length -%] + \pdfbookmark[0]{[[ feat_type ]]}{[[ feat_type ]]} + \section*{[[ feat_type ]]} + [% endif %] + [%- for feat in feat_list -%] + [%- if use_dnd_decorations %] + \pdfbookmark[1]{[[ feat.name ]]}{[[ feat_type ]] - [[ feat.name ]]} + \DndFeatHeader{[[ feat.name ]]}[Source: [[ feat.source ]]] + [%- else %] + \pdfbookmark[1]{[[ feat.name ]]}{[[ feat_type ]] - [[ feat.name ]]} + \subsection*{[[ feat.name ]]} + \textbf{Source:} [[ feat.source ]] + [%- endif %] [%- if feat.needs_implementation %] \textbf{**Not included in stats on Character Sheet} - [%- endif -%] - [[- feat.__doc__|rst_to_latex -]] - [%- endfor -%] -[% endif %] + [%- endif %] + [[ feat.__doc__|rst_to_latex(use_dnd_decorations=use_dnd_decorations) ]] + [% endfor %] +[%- endfor %] From 5e4ace8aa00acfa97780218c006c60d5d6826791 Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 16:03:00 +0200 Subject: [PATCH 03/10] Latex feature pages: Advanced feat sorting This patch enables additional sorting for features that are classes of other features. This works as is for metamagic, in the sense that it sorts all metamagic options under the metamagic feature, by checking whether a metamagic option is the direct child of the Metamagic class. The same is intended for other feature options. The intended relations are: Bloodhunter: Rites --> CrimsonRites BloodCurses --> BloodMaledict Formulas --> Formulas Fighter: Maneuver --> CombatSuperiority TrickShot --> AdeptMarksman Warlock: Invocation --> EldritchInvocation However, these need some additional work to lay the right class relations. --- dungeonsheets/character.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 83264ece..f427291e 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -496,16 +496,27 @@ def features_by_type(self): "Racial Features": [], "Background Features": [], } + other_feat_choices = list() + # Add player choices; distinguish between general feats and + # feat choices such as fighting styles and metamagic options. + for item in self.custom_features: + if item.source == "Feats": + fts["Feats"].append(item) + else: + other_feat_choices.append(item) for c in self.class_list: for item in list(c.features): - fts["Class Features"].append(item) - # Add player choices, but only if they're not automically given already: - for item in self.custom_features: - if not item in fts: - if item.source == "Feats": - fts["Feats"].append(item) - else: + if item not in other_feat_choices: fts["Class Features"].append(item) + # Now check whether any items in class_feat_choices + # is a subclass of current item. + for choice in other_feat_choices: + if choice.__class__.__bases__[0] is item.__class__: + fts["Class Features"].append(choice) + # Make sure we didn't miss any feat choices: + for choice in other_feat_choices: + if choice not in fts["Class Features"]: + fts["Class Features"].insert(0, choice) if self.race is not None: for item in getattr(self.race, "features", ()): fts["Racial Features"].append(item) From 5485248867cbfbedbd92b40f9483646fcddf46cf Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 12:16:12 +0200 Subject: [PATCH 04/10] Feat sorting: Sort Battle Master maneuvers Make the Maneuver class an alias to CombatSuperiority, so that Battle Master Maneuvers can be easily sorted as instances of CombatSuperiority. --- dungeonsheets/features/fighter.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dungeonsheets/features/fighter.py b/dungeonsheets/features/fighter.py index a58d646a..578e9cf4 100644 --- a/dungeonsheets/features/fighter.py +++ b/dungeonsheets/features/fighter.py @@ -383,13 +383,7 @@ class Relentless(Feature): # Maneuvers -class Maneuver(Feature): - """ - A generic Maneuver - """ - - name = "Maneuver" - source = "Fighter Maneuver (Battle Master)" +Maneuver = CombatSuperiority class BaitAndSwitch(Maneuver): From 38f6b491fdebc93033b00f3d4c9233b999226d07 Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 12:19:50 +0200 Subject: [PATCH 05/10] Feat sorting: Sort TrickShots under AdeptMarksman Allow the player choices for Trick Shots of the Gunslinger Fighter subclass to be sorted as instances of AdeptMarksman, by aliasing AdeptMarksman as TrickShot and by making all Trick Shot features an instance of TrickShot --- dungeonsheets/features/fighter.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dungeonsheets/features/fighter.py b/dungeonsheets/features/fighter.py index 578e9cf4..625198ec 100644 --- a/dungeonsheets/features/fighter.py +++ b/dungeonsheets/features/fighter.py @@ -1225,6 +1225,9 @@ class AdeptMarksman(Feature): source = "Fighter (Gunslinger" +TrickShot = AdeptMarksman + + class QuickDraw(Feature): """When you reach 7th level, you add your proficiency bonus to your initiative. You can also stow a firearm, then draw another firearm as a @@ -1275,7 +1278,7 @@ class HemorrhagingCritical(Feature): source = "Fighter (Gunslinger)" -class BullyingShot(Feature): +class BullyingShot(TrickShot): """You can use the powerful blast and thundering sound of your firearm to shake the resolve of a creature. You can expend one grit point while making a Charisma (Intimidation) check to gain advantage on the roll. @@ -1286,7 +1289,7 @@ class BullyingShot(Feature): source = "Gunslinger (Trick Shot)" -class DazingShot(Feature): +class DazingShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to attempt to dizzy your opponent. On a hit, the creature suffers normal damage and must make a Constitution saving throw or suffer @@ -1298,7 +1301,7 @@ class DazingShot(Feature): source = "Gunslinger (Trick Shot)" -class DeadeyeShot(Feature): +class DeadeyeShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to gain advantage on the attack roll. @@ -1308,7 +1311,7 @@ class DeadeyeShot(Feature): source = "Gunslinger (Trick Shot)" -class DisarmingShot(Feature): +class DisarmingShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to attempt to shoot an object from their hands. On a hit, the creature suffers normal damage and must succeed on a Strength saving throw @@ -1321,7 +1324,7 @@ class DisarmingShot(Feature): source = "Gunslinger (Trick Shot)" -class ForcefulShot(Feature): +class ForcefulShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to attempt to trip them up and force them back. On a hit, the creature suffers normal damage and must succeed on a Strength saving throw @@ -1333,7 +1336,7 @@ class ForcefulShot(Feature): source = "Gunslinger (Trick Shot)" -class PiercingShot(Feature): +class PiercingShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to attempt to fire through multiple opponents. The initial attack gains a +1 to the firearm's misfire score. On a hit, the creature suffers @@ -1347,7 +1350,7 @@ class PiercingShot(Feature): source = "Gunslinger (Trick Shot)" -class WingingShot(Feature): +class WingingShot(TrickShot): """When you make a firearm attack against a creature, you can expend one grit point to attempt to topple a moving target. On a hit, the creature suffers normal damage and must make a Strength saving throw or be knocked prone. @@ -1358,7 +1361,7 @@ class WingingShot(Feature): source = "Gunslinger (Trick Shot)" -class ViolentShot(Feature): +class ViolentShot(TrickShot): """When you make a firearm attack against a creature, you can expend one or more grit points to enhance the volatility of the attack. For each grit point expended, the attack gains a +2 to the firearm's misfire score. If From 6e6e05a6b522f17828a7f506bfb09d9d83fa0025 Mon Sep 17 00:00:00 2001 From: PJBrs Date: Fri, 9 May 2025 13:22:29 +0200 Subject: [PATCH 06/10] Feat sorting: Sort Arcane Shots Make all Arcane Shot options a subclass of ArcaneShot, so that they all get sorted directly after the Arcane Shot feature text. --- dungeonsheets/features/fighter.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dungeonsheets/features/fighter.py b/dungeonsheets/features/fighter.py index 625198ec..58635798 100644 --- a/dungeonsheets/features/fighter.py +++ b/dungeonsheets/features/fighter.py @@ -817,7 +817,7 @@ class EverReadyShot(Feature): source = "Fighter (Arcane Archer)" -class BanishingArrow(Feature): +class BanishingArrow(ArcaneShot): """You use abjuration magic to try to temporarily banish your target to a harmless location in the Feywild. The creature hit by the arrow must also succeed on a Charisma saving throw or be banished. While banished in this @@ -834,7 +834,7 @@ class BanishingArrow(Feature): source = "Fighter (Arcane Archer)" -class BeguilingArrow(Feature): +class BeguilingArrow(ArcaneShot): """Your enchantment magic causes this arrow to temporarily beguile its target. The creature hit by the arrow takes an extra 2d6 psychic damage, and choose one of your allies within 30 feet of the target. The target must @@ -850,7 +850,7 @@ class BeguilingArrow(Feature): source = "Fighter (Arcane Archer)" -class BurstingArrow(Feature): +class BurstingArrow(ArcaneShot): """You imbue your arrow with force energy drawn from the school of evocation. The energy detonates after your attack. Immediately after the arrow hits the creature, the target and all other creatures within 10 @@ -863,7 +863,7 @@ class BurstingArrow(Feature): source = "Fighter (Arcane Archer)" -class EnfeeblingArrow(Feature): +class EnfeeblingArrow(ArcaneShot): """You weave necromantic magic into your arrow. The creature hit by the arrow takes an extra 2d6 necrotic damage. The target must also succeed on a Constitution saving throw, or the damage dealt by its weapon attacks is @@ -876,7 +876,7 @@ class EnfeeblingArrow(Feature): source = "Fighter (Arcane Archer)" -class GraspingArrow(Feature): +class GraspingArrow(ArcaneShot): """When this arrow strikes its target, conjuration magic creates grasping, poisonous brams bles, which wrap around the target. The creature hit by the arrow takes an extra 2(16 poison damage, its speed is reduced by 10 feet, @@ -894,7 +894,7 @@ class GraspingArrow(Feature): source = "Fighter (Arcane Archer)" -class PiercingArrow(Feature): +class PiercingArrow(ArcaneShot): """You use transmutation magic to give your arrow an ethereal quality. When you use this option, you don't make an attack roll for the attack. Instead, the arrow shoots forward in a line, which is 1 foot wide and 30 feet long, @@ -911,7 +911,7 @@ class PiercingArrow(Feature): source = "Fighter (Arcane Archer)" -class SeekingArrow(Feature): +class SeekingArrow(ArcaneShot): """Using divination magic, you grant your arrow the ability to seek out a target. When you use this option, you don't make an attack roll for the attack. Instead, choose one creature you have seen in the past minute. The @@ -931,7 +931,7 @@ class SeekingArrow(Feature): source = "Fighter (Arcane Archer)" -class ShadowArrow(Feature): +class ShadowArrow(ArcaneShot): """You weave illusion magic into your arrow, causing it to occlude your fees vision with shadows. The creature hit by the arrow takes an extra 2d6 psychic damage, and it must succeed on a Wisdom saving throw or be unable @@ -941,7 +941,7 @@ class ShadowArrow(Feature): """ - name = "Shadow Arrow" + name = "Arcane Shot: Shadow Arrow" source = "Fighter (Arcane Archer)" From f6b31fb9ef7f7436921dafb183c89251df6d1bee Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 12:22:12 +0200 Subject: [PATCH 07/10] Feat sorting: Sort Eldritch Invocations Allow Invocations to be sorted under the Eldritch Invocations feature by aliasing the EldritchInvocation class as Invocation. --- dungeonsheets/features/warlock.py | 40 +++++++++++++------------------ 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/dungeonsheets/features/warlock.py b/dungeonsheets/features/warlock.py index 3deaad3a..14fcd975 100644 --- a/dungeonsheets/features/warlock.py +++ b/dungeonsheets/features/warlock.py @@ -21,6 +21,22 @@ class EldritchInvocation(Feature): name = "Eldritch Invocations" source = "Warlock" + at_will_spells = () + + def cast_spell_at_will(self, spell): + s = spell() + s.level = 0 + if "M" in s.components: + c = list(s.components) + c.remove("M") + s.components = tuple(c) + self.spells_known += (s,) + self.spells_prepared += (s,) + + def __init__(self, owner): + super().__init__(owner) + for s in self.at_will_spells: + self.cast_spell_at_will(s) class PactOfTheChain(Feature): @@ -780,29 +796,7 @@ class LimitedWish(Feature): # All Invocations -class Invocation(Feature): - """ - A generic Eldritch Invocation. Add details in features/warlock.py - """ - - name = "Unnamed Invocation" - source = "Warlock (Eldritch Invocations)" - at_will_spells = () - - def cast_spell_at_will(self, spell): - s = spell() - s.level = 0 - if "M" in s.components: - c = list(s.components) - c.remove("M") - s.components = tuple(c) - self.spells_known += (s,) - self.spells_prepared += (s,) - - def __init__(self, owner): - super().__init__(owner) - for s in self.at_will_spells: - self.cast_spell_at_will(s) +Invocation = EldritchInvocation # PHB From 454d6f096e8f077c281c689a6cf8a5f199b7eaa2 Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 15:27:52 +0200 Subject: [PATCH 08/10] Feat sorting: Bloodhunter Rites, Blood Curses and Formulas This patch aliases Rites to CrimsonRites and BloodCurses to BloodMaledict, so that all rites can be sorted under the Crimson Rites feat, and all the blood curses under the Blood Maledict feat. Furthermore, it integrates the two Formulas classes. --- dungeonsheets/features/bloodhunter.py | 125 +++++++++++--------------- 1 file changed, 52 insertions(+), 73 deletions(-) diff --git a/dungeonsheets/features/bloodhunter.py b/dungeonsheets/features/bloodhunter.py index 742a3451..93c7177e 100644 --- a/dungeonsheets/features/bloodhunter.py +++ b/dungeonsheets/features/bloodhunter.py @@ -27,6 +27,22 @@ class BloodMaledict(Feature): name = "Blood Maledict" source = "Blood Hunter" + at_will_spells = () + + def cast_spell_at_will(self, spell): + s = spell() + s.level = 0 + if "M" in s.components: + c = list(s.components) + c.remove("M") + s.components = tuple(c) + self.spells_known += (s,) + self.spells_prepared += (s,) + + def __init__(self, owner): + super().__init__(owner) + for s in self.at_will_spells: + self.cast_spell_at_will(s) class BloodHunterFightingStyle(FeatureSelector): @@ -75,6 +91,22 @@ class CrimsonRites(Feature): name = "Crimson Rites" source = "Blood Hunter" + at_will_spells = () + + def cast_spell_at_will(self, spell): + s = spell() + s.level = 0 + if "M" in s.components: + c = list(s.components) + c.remove("M") + s.components = tuple(c) + self.spells_known += (s,) + self.spells_prepared += (s,) + + def __init__(self, owner): + super().__init__(owner) + for s in self.at_will_spells: + self.cast_spell_at_will(s) class ExtraAttackBloodHunter(Feature): @@ -147,29 +179,7 @@ class SanguineMastery(Feature): # All Rites -class Rites(Feature): - """ - A generic Rite. Add details in features/bloodhunter.py - """ - - name = "Unnamed rite" - source = "BloodHunter (Crimson Rites)" - at_will_spells = () - - def cast_spell_at_will(self, spell): - s = spell() - s.level = 0 - if "M" in s.components: - c = list(s.components) - c.remove("M") - s.components = tuple(c) - self.spells_known += (s,) - self.spells_prepared += (s,) - - def __init__(self, owner): - super().__init__(owner) - for s in self.at_will_spells: - self.cast_spell_at_will(s) +Rites = CrimsonRites class RiteOfTheFlame(Rites): @@ -226,29 +236,7 @@ class RiteOfTheRoar(Rites): #Blood Curses -class BloodCurses(Feature): - """ - A generic BloodCurse. Add details in features/bloodhunter.py - """ - - name = "Unnamed Curse" - source = "BloodHunter (Blood Maledict)" - at_will_spells = () - - def cast_spell_at_will(self, spell): - s = spell() - s.level = 0 - if "M" in s.components: - c = list(s.components) - c.remove("M") - s.components = tuple(c) - self.spells_known += (s,) - self.spells_prepared += (s,) - - def __init__(self, owner): - super().__init__(owner) - for s in self.at_will_spells: - self.cast_spell_at_will(s) +BloodCurses = BloodMaledict class BloodCurseoftheAnxious(BloodCurses): @@ -535,8 +523,24 @@ class Formulas(Feature): name = "Formulas" source = "Blood Hunter (Order of the Mutant)" - - + at_will_spells = () + + def cast_spell_at_will(self, spell): + s = spell() + s.level = 0 + if "M" in s.components: + c = list(s.components) + c.remove("M") + s.components = tuple(c) + self.spells_known += (s,) + self.spells_prepared += (s,) + + def __init__(self, owner): + super().__init__(owner) + for s in self.at_will_spells: + self.cast_spell_at_will(s) + + class Mutagencraft(Feature): """At 3rd level, you can concoct a single mutagen when you finish a short or long rest. Starting at 7th level, the number of mutagens you can create when you finish a rest increases to two, and at 15th level, you can now create three mutagens. @@ -593,31 +597,6 @@ class ExaltedMutation(Feature): #Formulas -class Formulas(Feature): - """ - A generic Formula. Add details in features/bloodhunter.py - """ - - name = "Unnamed rite" - source = "BloodHunter (Crimson Rites)" - at_will_spells = () - - def cast_spell_at_will(self, spell): - s = spell() - s.level = 0 - if "M" in s.components: - c = list(s.components) - c.remove("M") - s.components = tuple(c) - self.spells_known += (s,) - self.spells_prepared += (s,) - - def __init__(self, owner): - super().__init__(owner) - for s in self.at_will_spells: - self.cast_spell_at_will(s) - - class Aether(Formulas): """**Prerequisite: 11th level.** You gain a flying speed of 20 feet for 1 hour. From 997775f7bfda8c314110b009ae05ff9779f85388 Mon Sep 17 00:00:00 2001 From: PJBrs Date: Sun, 31 Aug 2025 15:34:04 +0200 Subject: [PATCH 09/10] tests: Add test for features_by_type --- tests/test_character.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/test_character.py b/tests/test_character.py index 40b857c1..96266a36 100644 --- a/tests/test_character.py +++ b/tests/test_character.py @@ -189,6 +189,35 @@ def test_proficiencies_by_type(self): char.proficiencies_by_type["Weapons"].lower() + char.proficiencies_by_type["Other"].lower()) + def test_features_by_type(self): + char = Character( + classes=["Fighter", "Sorcerer"], + subclasses=["Gunslinger", "Divine Soul"], + levels=[15, 5], + race="Protector Aasimar", + features = ("Bullying Shot", "Disarming Shot", "Forceful Shot", "Violent Shot", + "Winging Shot", "Twinned Spell", "Empowered Spell", "resilient", "turn undead"), + feature_choices = ("great-weapon fighting",), + background = "Pirate" + ) + assert str(char.features_by_type["Feats"][0]) == "Resilient" + feats = "\n".join([str(feat) for feat in char.features_by_type["Class Features"]]) + e = ( + "Channel Divinity: Turn Undead\nFighting Style (Great Weapon Fighting)\nSecond Wind\n" + "Action Surge\nGunsmith\nAdept Marksman\nBullying Shot\n" + "Disarming Shot\nForceful Shot\nViolent Shot\nWinging Shot\n" + "Extra Attack (3x)\nQuick Draw\nIndomitable (2x/LR)\nRapid Repair\nLightning Repaid\n" + "Divine Magic\nFavored by the Gods\nFont of Magic\nMetamagic\nTwinned Spell\n" + "Empowered Spell" + ) + assert(feats == e) + feats = "\n".join([str(feat) for feat in char.features_by_type["Racial Features"]]) + e = ( + "Darkvision (60')\nCelestial Resistance\nHealing Hands\n" + "Light Bearer\nAasimar Radiant Soul" + ) + assert str(char.features_by_type["Background Features"][0]) == "Ship\'s Passage" + def test_proficiency_bonus(self): char = Character() char.level = 1 @@ -247,7 +276,7 @@ def test_wield_shield(self): # Try passing an Armor object directly char.wield_shield(Shield) self.assertEqual(char.armor_class, 15) - + def test_carrying_weight(self): char = Character(race="lightfoot halfling", strength=12) # Check carrying capacity @@ -366,9 +395,9 @@ class Beast(monsters.Monster): not_beast = monsters.Monster() not_beast.description = "monster" self.assertFalse(low_druid.can_assume_shape(not_beast)) - + class BeastMasterTestCase(TestCase): - + def test_ranger_beast(self): char = Ranger(6, subclasses = ["Beast Master"]) char.ranger_beast = "Panther" @@ -386,6 +415,3 @@ def test_ranger_beast(self): char = Ranger(3, subclasses = ["Beast Master"]) char.ranger_beast = "Panther" self.assertEqual(char.ranger_beast.hp_max, 13) - - - From 822b74767902c0f36b6a5f17cabfde74e14deaff Mon Sep 17 00:00:00 2001 From: PJ Beers Date: Sun, 9 Jun 2024 22:39:03 +0200 Subject: [PATCH 10/10] README.rst: Document feature sorting option Document the makesheets commandline option to sort features by type. --- README.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2de2a927..8643acee 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ Installation .. note:: - Dungeon sheets requires **at least python 3.6**. This is mostly due + dungeon-sheets requires **at least python 3.6**. This is mostly due to the liberal use of f-strings_. If you want to use it with previous versions of python 3, you'll probably have to replace all the f-strings with the older ``.format()`` method or string @@ -72,8 +72,9 @@ pdftk is available in Debian and derivatives as **pdftk**, the package is not available in some RPM distributions, such as Fedora and CentOS. One alternative would be to build your PC sheets using docker. -If the ``pdflatex`` command is available on your system, spellcasters -will include a spellbook with descriptions of each spell known. If +If the ``pdflatex`` command is available on your system, dungeon-sheets +will include a description of a character's features. For spellcasters, +it will include a spellbook with descriptions of each spell known. If not, then this feature will be skipped. In order to properly format descriptions for spells/features/etc., @@ -115,6 +116,14 @@ By default, your character's spells are ordered alphabetically. If you would like your spellbook to be ordered by level, you can use the ``-S`` option to do so. +Furthermore, your character's features are ordered alphabetically by +default as well. Pass the ``-N`` option to order feats by type +(character feats, class feats, racial feats and background feat) and, +if applicable, by sub-type (e.g., for Sorcerers, metamagic feature +choices are collected under the Metamagic feature; for the Battle +Master subclass, Maneuver feature choices are collected under +the Combat Superiority feature.) + If you'd like a **step-by-step walkthrough** for creating a new character, just run ``create-character`` from a command line and a helpful menu system will take care of the basics for you.