diff --git a/VERSION b/VERSION index 8adc70f..c18d72b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.0 \ No newline at end of file +0.8.1 \ No newline at end of file diff --git a/ben.pdf b/ben.pdf new file mode 100644 index 0000000..f24d2b4 Binary files /dev/null and b/ben.pdf differ diff --git a/ben.py b/ben.py new file mode 100644 index 0000000..b33b2a4 --- /dev/null +++ b/ben.py @@ -0,0 +1,90 @@ +"""This file describes the heroic adventurer Ben. + +It's used primarily for saving characters from create-character, +where there will be many missing sections. + +Modify this file as you level up and then re-generate the character +sheet by running ``makesheets`` from the command line. + +""" + +dungeonsheets_version = "0.8.0" + +name = "Ben" +classes_levels = ['barbarian 1', 'bard 1', 'cleric 1', 'druid 1', 'fighter 1', 'monk 1', 'ranger 1', 'rogue 1', 'sorceror 1', 'warlock 1', 'wizard 1', 'revised ranger 1'] +subclasses = ["Path of the Berserker", "College of Lore", "Arcana Domain", "Circle of the Land", "Arcane Archer", "Way of the Open Hand", , "Thief", "Draconic Bloodline", "The Archfey Patron", "School of Abjuration", ''] +player_name = "Ben" +background = "Mercenary Veteran" +race = "Hill Dwarf" +alignment = "Neutral good" +xp = 0 +hp_max = 10 + +# Ability Scores +strength = 14 +dexterity = 15 +constitution = 15 +intelligence = 12 +wisdom = 11 +charisma = 8 + +# Select what skills you're proficient with +skill_proficiencies = ('nature', 'animal handling', 'athletics', 'persuasion') + +# Named features / feats that aren't part of your classes, +# race, or background. +# Example: +# features = ('Tavern Brawler',) # take the optional Feat from PHB +features = () + +# If selecting among multiple feature options: ex Fighting Style +# Example (Fighting Style): +# feature_choices = ('Archery',) +feature_choices = () + +# Proficiencies and languages +languages = """Common, Dwarvish""" + +# Inventory +# TODO: Get yourself some money +cp = 0 +sp = 0 +ep = 0 +gp = 0 +pp = 0 + +# TODO: Put your equipped weapons and armor here +weapons = () # Example: ('shortsword', 'longsword') +armor = "" # Eg "light leather armor" +shield = "" # Eg "shield" + +equipment = """TODO: list the equipment and magic items your character carries""" + +attacks_and_spellcasting = """TODO: Describe how your character usually attacks +or uses spells.""" + +# List of known spells +# Example: spells_prepared = ('magic missile', 'mage armor') +spells_prepared = () # Todo: Learn some spells + +# Which spells have not been prepared +__spells_unprepared = () + +# all spells known +spells = spells_prepared + __spells_unprepared + +# Backstory +# Describe your backstory here +personality_traits = """TODO: How does your character behave? See the PHB for +examples of all the sections below""" + +ideals = """TODO: What does your character believe in?""" + +bonds = """TODO: Describe what debts your character has to pay, +and other commitments or ongoing quests they have.""" + +flaws = """TODO: Describe your characters interesting flaws. +""" + +features_and_traits = """TODO: Describe other features and abilities your +character has.""" diff --git a/dungeonsheets/background.py b/dungeonsheets/background.py index 78c546b..c2c50c3 100644 --- a/dungeonsheets/background.py +++ b/dungeonsheets/background.py @@ -12,7 +12,8 @@ class Background(): languages = () def __init__(self): - self.features = tuple([f() for f in self.features]) + cls = type(self) + self.features = tuple([f() for f in cls.features]) def __str__(self): return self.name diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 2d99584..0d29401 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -27,27 +27,27 @@ __version__ = read('../VERSION') dice_re = re.compile('(\d+)d(\d+)') 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), + # 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), } @@ -152,6 +152,10 @@ class Character(): def class_name(self): return ' / '.join([f'{c.class_name} {c.class_level}' for c in self.class_list]) + + @property + def subclasses(self): + return list([c.subclass or '' for c in self.class_list]) @property def speed(self): @@ -555,16 +559,17 @@ class Character(): 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))) + circle = char_props.pop('circle', None) class_list = [] for cl, sub in zip(classes_levels, subclasses): try: - c, lvl = cl.strip().split(' ') # " wizard 3 " => "wizard", "3" + c, _, lvl = cl.strip().rpartition(' ') # " wizard 3 " => "wizard", "3" except ValueError: raise ValueError( 'classes_levels not properly formatted. Each entry should ' 'be formatted \"class level\", but got {:s}'.format(cl)) try: - this_class = getattr(classes, c.capitalize()) + this_class = getattr(classes, c.title.replace(' ', '')) this_level = int(lvl) except AttributeError: raise AttributeError( @@ -572,6 +577,8 @@ class Character(): except ValueError: raise ValueError( 'level was not recognizable as an int: {:s}'.format(lvl)) + if issubclass(this_class, classes.Druid): + sub = circle or sub params = {} params['feature_choices'] = char_props.get('feature_choices', []) class_list += [this_class(this_level, subclass=sub, **params)] @@ -606,7 +613,7 @@ class Character(): self.save(filename, template_file=kwargs.get('template_file', 'character_template.txt')) - subprocess.call(['makesheets', filename) + subprocess.call(['makesheets', filename]) def read_character_file(filename): diff --git a/dungeonsheets/character_template.txt b/dungeonsheets/character_template.txt index bb31fc8..c992dad 100644 --- a/dungeonsheets/character_template.txt +++ b/dungeonsheets/character_template.txt @@ -9,6 +9,7 @@ dungeonsheets_version = "{{ char.dungeonsheets_version }}" name = "{{ char.name }}" classes_levels = {{ char.classes_levels }} +subclasses = {{ char.subclasses }} player_name = "{{ char.player_name }}" background = "{{ char.background.name }}" race = "{{ char.race.name }}" diff --git a/dungeonsheets/classes/__init__.py b/dungeonsheets/classes/__init__.py index 487c000..93dca66 100644 --- a/dungeonsheets/classes/__init__.py +++ b/dungeonsheets/classes/__init__.py @@ -1,6 +1,6 @@ __all__ = ('CharClass', 'Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk', 'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', - 'Wizard', 'Revisedranger', 'available_classes') + 'Wizard', 'RevisedRanger', 'available_classes') from .classes import CharClass from .barbarian import Barbarian @@ -10,11 +10,11 @@ from .druid import Druid from .fighter import Fighter from .monk import Monk from .paladin import Paladin -from .ranger import (Ranger, Revisedranger) +from .ranger import (Ranger, RevisedRanger) from .rogue import Rogue from .sorceror import Sorceror from .warlock import Warlock from .wizard import Wizard available_classes = [Barbarian, Bard, Cleric, Druid, Fighter, Monk, Ranger, - Rogue, Sorceror, Warlock, Wizard, Revisedranger] + Rogue, Sorceror, Warlock, Wizard, RevisedRanger] diff --git a/dungeonsheets/classes/barbarian.py b/dungeonsheets/classes/barbarian.py index 2f5dc99..bb1693b 100644 --- a/dungeonsheets/classes/barbarian.py +++ b/dungeonsheets/classes/barbarian.py @@ -1,8 +1,41 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (features, weapons) +from .classes import (CharClass, SubClass) +from collections import defaultdict +# PHB +class BerserkerPath(SubClass): + name = "Path of the Berserker" + class_features_by_level = defaultdict(list) + + +class TotemWarriorPath(SubClass): + name = "Path of the Totem Warrior" + class_features_by_level = defaultdict(list) + + +# SCAG +class BattleragerPath(SubClass): + name = "Path of the Battlerager" + class_features_by_level = defaultdict(list) + + +# XGTE +class AncestralGuardianPath(SubClass): + name = "Path of the Ancestral Guardian" + class_features_by_level = defaultdict(list) + + +class StormHeraldPath(SubClass): + name = "Path of the Storm Herald" + class_features_by_level = defaultdict(list) + + +class ZealotPath(SubClass): + name = "Path of the Zealot" + class_features_by_level = defaultdict(list) + + class Barbarian(CharClass): class_name = 'Barbarian' hit_dice_faces = 12 @@ -12,4 +45,7 @@ class Barbarian(CharClass): weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons) class_skill_choices = ('Animal Handling', 'Athletics', 'Intimidation', 'Nature', 'Perception', 'Survival') - + subclasses_available = (BerserkerPath, TotemWarriorPath, BattleragerPath, + AncestralGuardianPath, StormHeraldPath, ZealotPath) + features_by_level = defaultdict(list) + diff --git a/dungeonsheets/classes/bard.py b/dungeonsheets/classes/bard.py index 343cc4d..024eac3 100644 --- a/dungeonsheets/classes/bard.py +++ b/dungeonsheets/classes/bard.py @@ -1,6 +1,33 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +# PHB +class CollegeOfLore(SubClass): + name = "College of Lore" + class_features_by_level = defaultdict(list) + + +class CollegeOfValor(SubClass): + name = "College of Valor" + class_features_by_level = defaultdict(list) + + +# XGTE +class CollegeOfGlamour(SubClass): + name = "College of Glamour" + class_features_by_level = defaultdict(list) + + +class CollegeOfSwords(SubClass): + name = "College of Swords" + class_features_by_level = defaultdict(list) + + +class CollegeOfWhispers(SubClass): + name = "College of Whispers" + class_features_by_level = defaultdict(list) class Bard(CharClass): @@ -20,6 +47,9 @@ class Bard(CharClass): 'Religion', 'Sleight of Hand', 'Stealth', 'Survival') num_skill_choices = 3 + features_by_level = defaultdict(list) + subclasses_available = (CollegeOfLore, CollegeOfValor, CollegeOfGlamour, + CollegeOfSwords, CollegeOfWhispers) spellcasting_ability = 'charisma' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) diff --git a/dungeonsheets/classes/classes.py b/dungeonsheets/classes/classes.py index fb1de10..9614173 100644 --- a/dungeonsheets/classes/classes.py +++ b/dungeonsheets/classes/classes.py @@ -25,23 +25,56 @@ class CharClass(): def __init__(self, level, subclass=None, **params): self.class_level = level - if subclass in [None, '', 'None']: - self.subclass = None - else: - self.subclass = subclass + # Instantiate the features + self.features_by_level = defaultdict(list) + cls = type(self) + for i in range(1, 21): + fs = [f() for f in cls.features_by_level[i]] + self.features_by_level[i] = fs for k, v in params.items(): setattr(self, k, v) - # Instantiate the features + + # Apply subclass + self.subclass = self.select_subclass(subclass) + if isinstance(self.subclass, SubClass): + self.apply_subclass() + + def select_subclass(self, subclass_str): + """ + Return a SubClass object corresponding to given string. + + Intended to be replaced by classes so they can + define their own methods of picking subclass by string. + """ + if subclass_str in ['', 'None', 'none', None]: + return None + for sc in self.subclasses_available: + if subclass_str.lower() in sc.name.lower(): + return sc(level=self.class_level) + return None + + def apply_subclass(self): + if self.subclass is None: + return for i in range(1, 21): - self.features_by_level[i] = [f() for f in self.features_by_level[i]] - + self.features_by_level[i] += ([f() for f in + self.subclass.features_by_level[i]]) + for attr in ('weapon_proficiencies', '_proficiencies_text', + 'spells_known', 'spells_prepared'): + new_list = getattr(self, attr, ()) + getattr(self.subclass, attr, ()) + setattr(self, attr, new_list) + self.multiclass_weapon_proficiencies += (self.subclass.weapon_proficiencies) + self._multiclass_proficiencies_text += (self._proficiencies_text) + self.spellcasting_ability = (self.spellcasting_ability or + self.subclass.spellcasting_ability) + self.spell_slots_by_level = (self.spell_slots_by_level or + self.subclass.spell_slots_by_level) + @property def features(self): features = () for lvl in range(1, self.class_level+1): features += tuple(self.features_by_level[lvl]) - if self.subclass is not None and not isinstance(self.subclass, str): - features += tuple(self.subclass.features_by_level[lvl]) return features @property @@ -55,3 +88,26 @@ class CharClass(): return 0 else: return self.spell_slots_by_level[self.class_level][spell_level] + + +class SubClass(): + """ + A generic subclass object + """ + name = '' + features_by_level = defaultdict(list) + weapon_proficiencies = () + _proficiencies_text = () + spellcasting_ability = None + spell_slots_by_level = None + spells_known = () + spells_prepared = () + + def __init__(self, level): + self.class_level = level + + def __str__(self): + return self.name + + def __repr__(self): + return "\"{:s}\"".format(self.name) diff --git a/dungeonsheets/classes/cleric.py b/dungeonsheets/classes/cleric.py index 81278a1..2af48d1 100644 --- a/dungeonsheets/classes/cleric.py +++ b/dungeonsheets/classes/cleric.py @@ -1,6 +1,58 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +class KnowledgeDomain(SubClass): + name = "Knowledge Domain" + features_by_level = defaultdict(list) + + +class LifeDomain(SubClass): + name = "Life Domain" + features_by_level = defaultdict(list) + + +class LightDomain(SubClass): + name = "Light Domain" + features_by_level = defaultdict(list) + + +class NatureDomain(SubClass): + name = "Nature Domain" + features_by_level = defaultdict(list) + + +class TempestDomain(SubClass): + name = "Tempest Domain" + features_by_level = defaultdict(list) + + +class TrickeryDomain(SubClass): + name = "Trickery Domain" + features_by_level = defaultdict(list) + + +class WarDomain(SubClass): + name = "War Domain" + features_by_level = defaultdict(list) + + +# SCAG +class ArcanaDomain(SubClass): + name = "Arcana Domain" + features_by_level = defaultdict(list) + + +# XGTE +class ForgeDomain(SubClass): + name = "Forge Domain" + features_by_level = defaultdict(list) + + +class GraveDomain(SubClass): + name = "Grave Domain" + features_by_level = defaultdict(list) class Cleric(CharClass): @@ -12,6 +64,11 @@ class Cleric(CharClass): weapon_proficiencies = weapons.simple_weapons class_skill_choices = ('History', 'Insight', 'Medicine', 'Persuasion', 'Religion') + features_by_level = defaultdict(list) + subclasses_available = (KnowledgeDomain, LifeDomain, LightDomain, + NatureDomain, TempestDomain, TrickeryDomain, + WarDomain, ArcanaDomain, ForgeDomain, + GraveDomain) spellcasting_ability = 'wisdom' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) diff --git a/dungeonsheets/classes/druid.py b/dungeonsheets/classes/druid.py index a2cde20..c5ec122 100644 --- a/dungeonsheets/classes/druid.py +++ b/dungeonsheets/classes/druid.py @@ -1,18 +1,42 @@ from ..stats import findattr -from .. import (weapons, monsters, exceptions) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, monsters, exceptions, features) +from .classes import CharClass, SubClass +from collections import defaultdict import warnings import math +# PHB +class LandCircle(SubClass): + name = "Circle of the Land" + circle = "land" + features_by_level = defaultdict(list) + + +class MoonCircle(SubClass): + name = "Circle of the Moon" + circle = "moon" + features_by_level = defaultdict(list) + + +# XGTE +class DreamsCircle(SubClass): + name = "Circle of Dreams" + circle = "dreams" + features_by_level = defaultdict(list) + + +class ShepherdCircle(SubClass): + name = "Circle of the Shepherd" + circle = "shepherd" + features_by_level = defaultdict(list) + + class Druid(CharClass): class_name = 'Druid' - circle = "" # moon, land _wild_shapes = () hit_dice_faces = 8 saving_throw_proficiencies = ('intelligence', 'wisdom') - spellcasting_ability = 'wisdom' languages = 'Druidic' _proficiencies_text = ( 'Light armor', 'medium armor', @@ -26,6 +50,10 @@ class Druid(CharClass): class_skill_choices = ('Arcana', 'Animal Handling', 'Insight', 'Medicine', 'Nature', 'Perception', 'Religion', 'Survival') + features_by_class = defaultdict(list) + subclasses_available = (LandCircle, MoonCircle, DreamsCircle, + ShepherdCircle) + spellcasting_ability = 'wisdom' spell_slots_by_level = { 1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0), 2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0), @@ -49,17 +77,22 @@ class Druid(CharClass): 20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1), } - def __init__(self, level, subclass=None, **params): - if subclass is not None: - sc = str(subclass).lower() - if 'moon' in sc: - self.circle = 'moon' - params.pop('circle', '') - elif 'land' in sc: - self.circle = 'land' - params.pop('circle', '') - super().__init__(level, **params) - + def select_subclass(self, subclass_str): + if subclass_str in ['', 'None', 'none', None]: + return None + for sc in self.subclasses_available: + if ((subclass_str.lower() == sc.circle.lower()) + or (subclass_str.lower() in sc.name.lower())): + return sc(level=self.class_level) + return None + + @property + def circle(self): + if isinstance(self.subclass, SubClass): + return self.subclass.circle.lower() + else: + return '' + @property def all_wild_shapes(self): """Return all wild shapes, regardless of validity.""" @@ -130,7 +163,7 @@ class Druid(CharClass): max_swim = None max_fly = None # Make adjustments for moon circle druids - if self.circle.lower() == "moon": + if self.circle == "moon": if 2 <= self.class_level < 6: max_cr = 1 elif self.class_level >= 6: diff --git a/dungeonsheets/classes/fighter.py b/dungeonsheets/classes/fighter.py index 4994dc8..dc9d80f 100644 --- a/dungeonsheets/classes/fighter.py +++ b/dungeonsheets/classes/fighter.py @@ -1,8 +1,70 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict +# PHB +class Champion(SubClass): + name = "Champion" + features_by_level = defaultdict(list) + + +class BattleMaster(SubClass): + name = "Battle Master" + features_by_level = defaultdict(list) + + +class EldritchKnight(SubClass): + name = "Eldritch Knight" + features_by_level = defaultdict(list) + spellcasting_ability = 'intelligence' + multiclass_spellslots_by_level = { + # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) + 1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + 2: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + 3: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0), + 4: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 5: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 6: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 7: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 8: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 9: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 10: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 11: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 12: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 13: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 14: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 15: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 16: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 17: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 18: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 19: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0), + 20: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0), + } + + +# SCAG +class PurpleDragonKnight(SubClass): + name = "Purple Dragon Knight" + features_by_level = defaultdict(list) + + +# XGTE +class ArcaneArcher(SubClass): + name = "Arcane Archer" + features_by_level = defaultdict(list) + + +class Cavalier(SubClass): + name = "Cavalier" + features_by_level = defaultdict(list) + + +class Samurai(SubClass): + name = "Samurai" + features_by_level = defaultdict(list) + + class Fighter(CharClass): class_name = 'Fighter' hit_dice_faces = 10 @@ -17,4 +79,7 @@ class Fighter(CharClass): class_skill_choices = ('Acrobatics', 'Animal Handling', 'Athletics', 'History', 'Insight', 'Intimidation', 'Perception', 'Survival') - + features_by_level = defaultdict(list) + subclasses_available = (Champion, BattleMaster, EldritchKnight, + PurpleDragonKnight, ArcaneArcher, Cavalier, + Samurai) diff --git a/dungeonsheets/classes/monk.py b/dungeonsheets/classes/monk.py index 6603603..780a72c 100644 --- a/dungeonsheets/classes/monk.py +++ b/dungeonsheets/classes/monk.py @@ -1,10 +1,45 @@ __all__ = ('Monk') from .. import (features, weapons) -from .classes import CharClass +from .classes import CharClass, SubClass from collections import defaultdict +class OpenHandWay(SubClass): + name = "Way of the Open Hand" + features_by_level = defaultdict(list) + + +class ShadowWay(SubClass): + name = "Way of Shadow" + features_by_level = defaultdict(list) + + +class FourElementsWay(SubClass): + name = "Way of the Four Elements" + features_by_level = defaultdict(list) + + +class SunSoulWay(SubClass): + name = "Way of the Sun Soul" + features_by_level = defaultdict(list) + + +class LongDeathWay(SubClass): + name = "Way of the Long Death" + features_by_level = defaultdict(list) + + +class DrunkenMasterWay(SubClass): + name = "Way of the Drunken Master" + features_by_level = defaultdict(list) + + +class KenseiWay(SubClass): + name = "Way of the Kensei" + features_by_level = defaultdict(list) + + class Monk(CharClass): class_name = 'Monk' hit_dice_faces = 8 @@ -15,7 +50,10 @@ class Monk(CharClass): weapon_proficiencies = (weapons.Shortsword, weapons.Unarmed) + weapons.simple_weapons class_skill_choices = ('Acrobatics', 'Athletics', 'History', 'Insight', 'Religion', 'Stealth') - subclasses_available = ('SunSoul', 'OpenHand') + subclasses_available = (OpenHandWay, ShadowWay, + FourElementsWay, SunSoulWay, + LongDeathWay, DrunkenMasterWay, + KenseiWay) features_by_level = defaultdict(list) features_by_level[1] = [features.UnarmoredDefense, features.MartialArts] @@ -25,19 +63,3 @@ class Monk(CharClass): for f in self.features_by_level[1]: if isinstance(f, features.MartialArts): f.level = self.class_level - if subclass == 'sunsoul': - self.subclass = SunSoul(level=self.class_level) - else: - self.subclass = None - if self.subclass is not None: - self._proficiencies_text += self.subclass._proficiencies_text - self.weapon_proficiences += self.subclass.weapon_proficiencies - - -class SunSoul: - class_features_by_level = defaultdict(list) - weapon_proficiencies = () - _profiencies_text = () - - def __init__(self, level): - self.class_level = level diff --git a/dungeonsheets/classes/paladin.py b/dungeonsheets/classes/paladin.py index ca2fde2..fff2893 100644 --- a/dungeonsheets/classes/paladin.py +++ b/dungeonsheets/classes/paladin.py @@ -1,7 +1,37 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + +class OathOfDevotion(SubClass): + name = "Oath of Devotion" + features_by_level = defaultdict(list) + + +class OathOfAncients(SubClass): + name = "Oath of The Ancients" + features_by_level = defaultdict(list) + + +class OathOfVengance(SubClass): + name = "Oath of Vengance" + features_by_level = defaultdict(list) + + +class OathOfCrown(SubClass): + name = "Oath of The Crown" + features_by_level = defaultdict(list) + + +class OathOfConquest(SubClass): + name = "Oath of Conquest" + features_by_level = defaultdict(list) + + +class OathOfRedemption(SubClass): + name = "Oath of Redemption" + features_by_level = defaultdict(list) + class Paladin(CharClass): class_name = 'Paladin' @@ -12,6 +42,9 @@ class Paladin(CharClass): weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons class_skill_choices = ("Athletics", 'Insight', 'Intimidation', 'Medicine', 'Persuasion', 'Religion') + features_by_level = defaultdict(list) + subclasses_available = (OathOfDevotion, OathOfAncients, OathOfVengance, + OathOfCrown, OathOfConquest, OathOfRedemption) spellcasting_ability = 'charisma' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) diff --git a/dungeonsheets/classes/ranger.py b/dungeonsheets/classes/ranger.py index 30f5006..766016d 100644 --- a/dungeonsheets/classes/ranger.py +++ b/dungeonsheets/classes/ranger.py @@ -1,10 +1,37 @@ -__all__ = ('Ranger', 'Revisedranger') +__all__ = ('Ranger', 'RevisedRanger') from .. import (weapons, features) -from .classes import CharClass +from .classes import CharClass, SubClass from collections import defaultdict +# PHB +class Hunter(SubClass): + name = "Hunter" + features_by_level = defaultdict(list) + + +class BeastMaster(SubClass): + name = "Beast Master" + features_by_level = defaultdict(list) + + +# XGTE +class GloomStalker(SubClass): + name = "Gloom Stalker" + features_by_level = defaultdict(list) + + +class HorizonWalker(SubClass): + name = "Horizon Walker" + features_by_level = defaultdict(list) + + +class MonsterSlayer(SubClass): + name = "Monster Slayer" + features_by_level = defaultdict(list) + + class Ranger(CharClass): class_name = 'Ranger' hit_dice_faces = 10 @@ -16,8 +43,10 @@ class Ranger(CharClass): 'Investigation', 'Nature', 'Perception', 'Stealth', 'Survival') num_skill_choices = 3 - spellcasting_ability = 'wisdom' features_by_level = defaultdict(list) + subclasses_available = (Hunter, BeastMaster, GloomStalker, + HorizonWalker, MonsterSlayer) + spellcasting_ability = 'wisdom' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) 1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0), @@ -49,6 +78,24 @@ class Ranger(CharClass): self.features_by_level[2].append(fighting_style) -# Custom Classes -class Revisedranger(Ranger): - class_name = 'Revisedranger' +# Revised Ranger +class BeastConclave(SubClass): + name = "Beast Conclave" + features_by_level = defaultdict(list) + + +class HunterConclave(SubClass): + name = "Hunter Conclave" + features_by_level = defaultdict(list) + + +class DeepStalkerConclave(SubClass): + name = "Deep Stalker Conclave" + features_by_level = defaultdict(list) + + +class RevisedRanger(Ranger): + class_name = 'Revised Ranger' + features_by_level = defaultdict(list) + subclasses_available = (BeastConclave, HunterConclave, DeepStalkerConclave) + diff --git a/dungeonsheets/classes/rogue.py b/dungeonsheets/classes/rogue.py index d51178c..bbf7a29 100644 --- a/dungeonsheets/classes/rogue.py +++ b/dungeonsheets/classes/rogue.py @@ -1,6 +1,62 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +# PHB +class Thief(SubClass): + name = "Thief" + features_by_level = defaultdict(list) + + +class Assassin(SubClass): + name = "Assassin" + features_by_level = defaultdict(list) + + +class ArcaneTrickster(SubClass): + name = "Arcane Trickster" + features_by_level = defaultdict(list) + spellcasting_ability = 'intelligence' + multiclass_spellslots_by_level = { + # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) + 1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + 2: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + 3: (3, 2, 0, 0, 0, 0, 0, 0, 0, 0), + 4: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 5: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 6: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0), + 7: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 8: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 9: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0), + 10: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 11: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 12: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0), + 13: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 14: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 15: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0), + 16: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 17: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 18: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 19: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0), + 20: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0), + } + + +# XGTE +class Inquisitive(SubClass): + name = "Inquisitive" + features_by_level = defaultdict(list) + + +class Mastermind(SubClass): + name = "Mastermind" + features_by_level = defaultdict(list) + + +class Swashbuckler(SubClass): + name = "Swashbuckler" + features_by_level = defaultdict(list) class Rogue(CharClass): @@ -17,4 +73,6 @@ class Rogue(CharClass): 'Insight', 'Intimidation', 'Investigation', 'Perception', 'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth') - + features_by_level = defaultdict(list) + subclasses_available = (Thief, Assassin, ArcaneTrickster, + Inquisitive, Mastermind, Swashbuckler) diff --git a/dungeonsheets/classes/sorceror.py b/dungeonsheets/classes/sorceror.py index e802b4d..83efb51 100644 --- a/dungeonsheets/classes/sorceror.py +++ b/dungeonsheets/classes/sorceror.py @@ -1,6 +1,33 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +# PHB +class DraconicBloodline(SubClass): + name = "Draconic Bloodline" + features_by_level = defaultdict(list) + + +class WildMagic(SubClass): + name = "Wild Magic" + features_by_level = defaultdict(list) + + +# XGTE +class DivineSoul(SubClass): + name = "Divine Soul" + features_by_level = defaultdict(list) + + +class ShadowMagic(SubClass): + name = "Shadow Magic" + features_by_level = defaultdict(list) + + +class StormSorcery(SubClass): + name = "Storm Sorcery" + features_by_level = defaultdict(list) class Sorceror(CharClass): @@ -14,6 +41,9 @@ class Sorceror(CharClass): weapons.LightCrossbow) class_skill_choices = ('Arcana', 'Deception', 'Insight', 'Intimidation', 'Persuasion', 'Religion') + features_by_level = defaultdict(list) + subclasses_available = (DraconicBloodline, WildMagic, DivineSoul, + ShadowMagic, StormSorcery) spellcasting_ability = 'charisma' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) diff --git a/dungeonsheets/classes/warlock.py b/dungeonsheets/classes/warlock.py index 6d9a0a7..3c5bc32 100644 --- a/dungeonsheets/classes/warlock.py +++ b/dungeonsheets/classes/warlock.py @@ -1,6 +1,39 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +# PHB +class Archfey(SubClass): + name = "The Archfey Patron" + features_by_level = defaultdict(list) + + +class Fiend(SubClass): + name = "The Fiend Patron" + features_by_level = defaultdict(list) + + +class GreatOldOne(SubClass): + name = "The Great Old One Patron" + features_by_level = defaultdict(list) + + +# SCAG +class Undying(SubClass): + name = "The Undying Patron" + features_by_level = defaultdict(list) + + +# XGTE +class Celestial(SubClass): + name = "The Celestial Patron" + features_by_level = defaultdict(list) + + +class Hexblade(SubClass): + name = "Hexblade Patron" + features_by_level = defaultdict(list) class Warlock(CharClass): @@ -12,6 +45,9 @@ class Warlock(CharClass): 'Intimidation', 'Investigation', 'Nature', 'Religion') weapon_proficiencies = weapons.simple_weapons + features_by_level = defaultdict(list) + subclasses_available = (Archfey, Fiend, GreatOldOne, Undying, Celestial, + Hexblade) spellcasting_ability = 'charisma' spell_slots_by_level = { 1: (2, 1, 0, 0, 0, 0, 0, 0, 0, 0), diff --git a/dungeonsheets/classes/wizard.py b/dungeonsheets/classes/wizard.py index c1605bf..c618718 100644 --- a/dungeonsheets/classes/wizard.py +++ b/dungeonsheets/classes/wizard.py @@ -1,6 +1,59 @@ -from .. import (weapons) -from .. import features as feats -from .classes import CharClass +from .. import (weapons, features) +from .classes import CharClass, SubClass +from collections import defaultdict + + +# PHB +class Abjuration(SubClass): + name = "School of Abjuration" + features_by_level = defaultdict(list) + + +class Conjuration(SubClass): + name = "School of Conjuration" + features_by_level = defaultdict(list) + + +class Divination(SubClass): + name = "School of Divination" + features_by_level = defaultdict(list) + + +class Enchantment(SubClass): + name = "School of Enchantment" + features_by_level = defaultdict(list) + + +class Evocation(SubClass): + name = "School of Evocation" + features_by_level = defaultdict(list) + + +class Illusion(SubClass): + name = "School of Illusion" + features_by_level = defaultdict(list) + + +class Necromancy(SubClass): + name = "School of Necromancy" + features_by_level = defaultdict(list) + + +class Transmutation(SubClass): + name = "School of Transmutation" + features_by_level = defaultdict(list) + + +# SCAG +class Bladeslinging(SubClass): + name = "School of Bladeslinging" + features_by_level = defaultdict(list) + + +# XGTE +class WarMagic(SubClass): + name = "School of War Magic" + features_by_level = defaultdict(list) class Wizard(CharClass): @@ -14,6 +67,10 @@ class Wizard(CharClass): weapons.LightCrossbow) class_skill_choices = ('Arcana', 'History', 'Investigation', 'Medicine', 'Religion') + features_by_level = defaultdict(list) + subclasses_available = (Abjuration, Conjuration, Divination, Enchantment, + Evocation, Illusion, Necromancy, Transmutation, + Bladeslinging, WarMagic) spellcasting_ability = 'intelligence' spell_slots_by_level = { # char_lvl: (cantrips, 1st, 2nd, 3rd, ...) diff --git a/dungeonsheets/create_character.py b/dungeonsheets/create_character.py index 5b28800..e375a2a 100755 --- a/dungeonsheets/create_character.py +++ b/dungeonsheets/create_character.py @@ -243,7 +243,10 @@ class SubclassForm(npyscreen.ActionForm): def __init__(self, newclass, level, num=1, **kwargs): self.class_num = num self.parent_class = newclass - self.subclass_options = newclass.subclasses_available or ('None',) + if len(newclass.subclasses_available) > 0: + self.subclass_options = [sc.name for sc in newclass.subclasses_available] + else: + self.subclass_options = ('None',) self.level = level super().__init__(**kwargs) diff --git a/dungeonsheets/empty_template.tex b/dungeonsheets/empty_template.tex index 71ed65f..3e865ba 100644 --- a/dungeonsheets/empty_template.tex +++ b/dungeonsheets/empty_template.tex @@ -12,6 +12,7 @@ dungeonsheets_version = "{{ char.dungeonsheets_version }}" name = "{{ char.name }}" classes_levels = {{ char.classes_levels }} +subclasses = {{ char.subclasses }} player_name = "{{ char.player_name }}" background = "{{ char.background.name }}" race = "{{ char.race.name }}" diff --git a/dungeonsheets/features/__init__.py b/dungeonsheets/features/__init__.py index 1a11c1f..c1af069 100644 --- a/dungeonsheets/features/__init__.py +++ b/dungeonsheets/features/__init__.py @@ -14,3 +14,4 @@ from .warlock import * from .wizard import * from .races import * from .backgrounds import * +from .feats import * diff --git a/dungeonsheets/features/feats.py b/dungeonsheets/features/feats.py new file mode 100644 index 0000000..e69de29 diff --git a/dungeonsheets/features_template.tex b/dungeonsheets/features_template.tex index 7b65675..8b27eb6 100644 --- a/dungeonsheets/features_template.tex +++ b/dungeonsheets/features_template.tex @@ -5,7 +5,7 @@ \usepackage[dvipsnames]{color} \definecolor{mygrey}{gray}{0.7} -\title{Features and Traits} +\title{Features and Subclass} \author{[[ character.name ]]} \date{} @@ -13,6 +13,14 @@ \maketitle +[% for sc in character.subclasses if sc not in ['', None, 'None', 'none']%] + + \section**{[[ sc.name ]]} + + [[ sc.__doc__|rst_to_latex ]] + +[% endfor %] + [% for feat in character.features %] \section*{[[ feat.name ]]} diff --git a/dungeonsheets/race.py b/dungeonsheets/race.py index f8bf9d4..e15076d 100644 --- a/dungeonsheets/race.py +++ b/dungeonsheets/race.py @@ -26,9 +26,13 @@ class Race(): spells_prepared = () def __init__(self): - self.features = tuple([f() for f in self.features]) + cls = type(self) + # Instantiate the features + self.features = tuple([f() for f in cls.features]) + self.features_by_level = defaultdict(list) for i in range(1, 21): - self.features_by_level[i] = [f() for f in self.features_by_level[i]] + self.features_by_level[i] = [f()for f in + cls.features_by_level[i]] def __str__(self): return self.name diff --git a/setup.py b/setup.py index 3acbcfa..03d76b2 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ setup(name='dungeonsheets', 'dungeonsheets': ['blank-character-sheet-default.pdf', 'blank-spell-sheet-default.pdf', 'spellbook_template.tex', '../VERSION', + 'empty_template.txt', 'character_template.txt', 'features_template.tex', 'druid_shapes_template.tex'] },