diff --git a/VERSION b/VERSION index 899f24f..ac39a10 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.0 \ No newline at end of file +0.9.0 diff --git a/ben.pdf b/ben.pdf new file mode 100644 index 0000000..ea608bd Binary files /dev/null and b/ben.pdf differ diff --git a/ben.py b/ben.py new file mode 100644 index 0000000..1aab057 --- /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.3" + +name = "Ben" +classes_levels = ['paladin 1'] +subclasses = ["Oath of The Ancients"] +player_name = "Ben" +background = "Charlatan" +race = "Hill Dwarf" +alignment = "Neutral good" +xp = 0 +hp_max = 10 + +# Ability Scores +strength = 15 +dexterity = 14 +constitution = 15 +intelligence = 12 +wisdom = 11 +charisma = 8 + +# Select what skills you're proficient with +skill_proficiencies = ('intimidation', 'athletics', 'deception', 'sleight of hand') + +# 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/ToDoNotes.txt b/dungeonsheets/ToDoNotes.txt index e230e24..1a74b02 100644 --- a/dungeonsheets/ToDoNotes.txt +++ b/dungeonsheets/ToDoNotes.txt @@ -1,5 +1,4 @@ Add multiclass proficiencies to classes - Add Warlock Incantations Add Warlock multiclass spell slots @@ -7,11 +6,6 @@ Add disadvantage on STEALTH with armor Add race / class AC bonuses -Add subclasses Add features -Auto-add features to PDF -Add features description LaTeX page **hard** integrate features automatically into math - -Add Character.save() option to save to text file Add Inspiration points 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..ca96930 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), } @@ -137,10 +137,6 @@ class Character(): 'race': race, 'background': background}) self.set_attrs(**attrs) - # instantiate any spells not listed properly - for S in self.spells_prepared: - if S not in [type(spl) for spl in self.spells]: - self._spells += (S(),) def __str__(self): return self.name @@ -152,6 +148,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): @@ -259,7 +259,7 @@ class Character(): @property def spells(self): - spells = set(self._spells) + spells = set(self._spells) | set(self._spells_prepared) for f in self.features: spells |= set(f.spells_known) | set(f.spells_prepared) for c in self.spellcasting_classes: @@ -333,7 +333,7 @@ class Character(): name=f, source='Unknown', __doc__="""Unknown Feature. Add to features.py""")) warnings.warn(msg) - self.custom_features = tuple(F() for F in _features) + self.custom_features += tuple(F() for F in _features) elif (attr == 'spells') or (attr == 'spells_prepared'): # Create a list of actual spell objects _spells = [] @@ -555,16 +555,18 @@ 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" + c = c.title().replace(' ', '') 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) this_level = int(lvl) except AttributeError: raise AttributeError( @@ -572,6 +574,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)] @@ -592,7 +596,7 @@ class Character(): char=self, ) # Render the template - src_path = os.path.dirname(__file__) + src_path = os.path.join(os.path.dirname(__file__), 'forms/') text = jinja2.Environment( loader=jinja2.FileSystemLoader(src_path or './') ).get_template(template_file).render(context) @@ -601,12 +605,10 @@ class Character(): f.write(text) def to_pdf(self, filename, **kwargs): + from .make_sheets import make_sheet if filename.endswith('.pdf'): filename = filename.replace('pdf', 'py') - self.save(filename, - template_file=kwargs.get('template_file', - 'character_template.txt')) - subprocess.call(['makesheets', filename) + make_sheet(filename, char=self, flatten=kwargs.get('flatten', True)) def read_character_file(filename): diff --git a/dungeonsheets/classes/__init__.py b/dungeonsheets/classes/__init__.py index 487c000..b35bd00 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] +available_classes = [Barbarian, Bard, Cleric, Druid, Fighter, Monk, Paladin, + Ranger, Rogue, Sorceror, Warlock, Wizard, RevisedRanger] diff --git a/dungeonsheets/classes/barbarian.py b/dungeonsheets/classes/barbarian.py index 2f5dc99..6d38aab 100644 --- a/dungeonsheets/classes/barbarian.py +++ b/dungeonsheets/classes/barbarian.py @@ -1,15 +1,113 @@ -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): + """For some barbarians, rage is a means to an end-—that end being + violence. The Path of the Berserker is a path of untrammeled fury, slick + with blood. As you enter the berserker’s rage, you thrill in the chaos of + battle, heedless of your own health or well-being. + + """ + name = "Path of the Berserker" + class_features_by_level = defaultdict(list) + + +class TotemWarriorPath(SubClass): + """The Path of the Totem Warrior is a spiritual journey, as the barbarian + accepts a spirit animal as guide, protector, and inspiration. In battle, + your totem spirit fills you with supernatural might, adding magical fuel to + your barbarian rage. + + Most barbarian tribes consider a totem animal to be kin to a particular + clan. In such cases, it is unusual for an individual to have more than one + totem animal spirit, though exceptions exist. + + """ + name = "Path of the Totem Warrior" + class_features_by_level = defaultdict(list) + + +# SCAG +class BattleragerPath(SubClass): + """Known as Kuldjargh (literally "axe idiot") in Dwarvish, battleragers are + dwarf followers of the gods of war and take the Path of the + Battlerager. They specialize in wearing bulky, spiked armor and throwing + themselves into combat, striking with their body itself and giving + themselves over to the fury of battle. + + """ + name = "Path of the Battlerager" + class_features_by_level = defaultdict(list) + + +# XGTE +class AncestralGuardianPath(SubClass): + """Some barbarians hail from cultures that revere their ancestors. These + tribes teach that the warriors of the past linger in the world as mighty + spirits, who can guide and protect the living. When a barbarian who follows + this path rages, the barbarian contacts the spirit world and calls on these + guardian spirits for aid. + + Barbarians who draw on their ancestral guardians can better fight to + protect their tribes and their allies. In order to cement ties to their + ancestral guardians, barbarians who follow this path cover themselves in + elabo— rate tattoos that celebrate their ancestors’ deeds. These tattoos + tell sagas of victories against terrible monsters and other fearsome + rivals. + + """ + name = "Path of the Ancestral Guardian" + class_features_by_level = defaultdict(list) + + +class StormHeraldPath(SubClass): + """All barbarians harbor a fury within. Their rage grants them superior + strength, durability, and speed. Barbarians who follow the Path of the + Storm Herald learn to transform that rage into a mantle of primal magic, + which swirls around them. When in a fury, a barbarian ofthis path taps into + the forces of nature to create powerful magical effects. + + Storm heralds are typically elite champions who train alongside druids, + rangers, and others sworn to protect nature. Other storm heralds hone their + craft in lodges in regions wracked by storms, in the frozen reaches at the + world’s end, or deep in the hottest deserts. + + """ + name = "Path of the Storm Herald" + class_features_by_level = defaultdict(list) + + +class ZealotPath(SubClass): + """Some deities inspire their followers to pitch themselves into a ferocious + battle fury. These barbarians are zealots—warriors who channel their rage + into powerful disn plays of divine power. + + A variety of gods across the worlds of D&D inspire their followers to + embrace this path. Tempus from the Forgotten Realms and Hextor and Erythnul + of Greyhawk are all prime examples. In general, the gods who inspire + zealots are deities of combat, destruction, and violence. Not all are evil, + but few are good + + """ + name = "Path of the Zealot" + class_features_by_level = defaultdict(list) + + class Barbarian(CharClass): class_name = 'Barbarian' hit_dice_faces = 12 saving_throw_proficiencies = ('strength', 'constitution') + weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons) _proficiencies_text = ('light armor', 'medium armor', 'shields', 'simple weapons', 'martial weapons') - weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons) + multiclass_weapon_proficiencies = weapon_proficiencies + _multiclass_proficiencies_text = ('shields', 'simple 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..b3e06cf 100644 --- a/dungeonsheets/classes/bard.py +++ b/dungeonsheets/classes/bard.py @@ -1,6 +1,116 @@ -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): + """Bards of the College o f Lore know something about most things, collecting + bits of knowledge from sources as diverse as scholarly tomes and peasant + tales. Whether singing folk ballads in taverns or elaborate compositions in + royal courts, these bards use their gifts to hold audiences + spellbound. When the applause dies down, the audience members might find + themselves questioning everything they held to be true, from their faith in + the priesthood of the local temple to their loyalty to the king. + + The loyalty of these bards lies in the pursuit of beauty and truth, not in + fealty to a monarch or following the tenets of a deity. A noble who keeps + such a bard as a herald or advisor knows that the bard would rather be + honest than politic. + + The college’s members gather in libraries and sometimes in actual colleges, + complete with classrooms and dormitories, to share their lore with one + another. They also meet at festivals or affairs of state, where they can + expose corruption, unravel lies, and poke fun at selfimportant figures of + authority. + + """ + name = "College of Lore" + class_features_by_level = defaultdict(list) + + +class CollegeOfValor(SubClass): + """Bards of the College of Valor are daring skalds whose tales keep alive the + memory of the great heroes of the past, and thereby inspire a new + generation of heroes. These bards gather in mead halls or around great + bonfires to sing the deeds of the mighty, both past and present. They + travel the land to witness great events firsthand and to ensure that the + memory of those events doesn’t pass from the world. With their songs, they + inspire others to reach the same heights of accomplishment as the heroes of + old + + """ + name = "College of Valor" + class_features_by_level = defaultdict(list) + + +# XGTE +class CollegeOfGlamour(SubClass): + """The College of Glamour is the home Of bards who mas— tered their craft in + the vibrant realm of the Feywild or under the tutelage Of someone who + dwelled there. Tutored by satyrs, eladrin, and other fey, these bards + learn to use their magic to delight and captivate others. + + The bards of this college are regarded with a mixture of awe and + fear. Their performances are the stuff of legend. These bards are so + eloquent that a speech or song that one of them performs can cause captors + tO release the bard unharmed and can lull a furious dragon into + complacency. The same magic that allows them to quell beasts can also bend + minds. Villainous bards Of this college can leech Off a community for + weeks, misusing their magic to turn their hosts into thralls. Heroic bards + of this college instead use this power to gladden the downtrodden and + undermine oppressors. + + """ + name = "College of Glamour" + class_features_by_level = defaultdict(list) + + +class CollegeOfSwords(SubClass): + """Bards of the College of Swords are called blades, and they entertain + through daring feats of weapon prowess. Blades perform stunts such as sword + swallowing, knife throwing and juggling, and mock combats. Though they use + their weapons to entertain, they are also highly trained and skilled + warriors in their own right. + + Their talent with weapons inspires many blades to lead double lives. One + blade might use a circus troupe as cover for nefarious deeds such as + assassination, robbery, and blackmail. Other blades strike at the wicked, + bringingjustice to bear against the cruel and powerful. Most troupes are + happy to accept a blade’s talent for the excitement it adds to a + performance, but few entertainers fully trust a blade in their ranks. + + Blades who abandon their lives as entertainers have often run into trouble + that makes maintaining their secret activities impossible. A blade caught + stealing or engaging in vigilante justice is too great a liability for most + troupes. With their weapon skills and magic, these blades either take up + work as enforcers for thieves’ guilds or strike out on their own as + adventurers. + + """ + name = "College of Swords" + class_features_by_level = defaultdict(list) + + +class CollegeOfWhispers(SubClass): + """Most folk are happy to welcome a bard into their midst. Bards of the + College of Whispers use this to their advantage. They appear to be like + other bards, sharing news, singing songs, and telling tales to the + audiences they gather. In truth, the College of Whispers teaches its + students that they are wolves among sheep. These bards use their knowledge + and magic to uncover secrets and turn them against others through extortion + and threats. + + Many other bards hate the College of Whispers, viewing it as a parasite + that uses a bard’s reputation to acquire wealth and power. For this reason, + members of this college rarely reveal their true nature. They typically + claim to follow some other college, or they keep their actual calling + secret in order to infiltrate and exploit royal courts and other settings + of power + + """ + name = "College of Whispers" + class_features_by_level = defaultdict(list) class Bard(CharClass): @@ -19,7 +129,13 @@ class Bard(CharClass): 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival') + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = ('Light Armor', '[choose one skill]', + '[choose one musical instrument]') 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..ea3a930 100644 --- a/dungeonsheets/classes/classes.py +++ b/dungeonsheets/classes/classes.py @@ -1,4 +1,5 @@ from collections import defaultdict +from ..features import Feature, FeatureSelector class CharClass(): @@ -23,25 +24,66 @@ class CharClass(): subclasses_available = () features_by_level = defaultdict(list) - def __init__(self, level, subclass=None, **params): + def __init__(self, level, subclass=None, feature_choices=[], + **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 = [] + for f in cls.features_by_level[i]: + if issubclass(f, FeatureSelector): + fs.append(f(feature_choices=feature_choices)) + elif issubclass(f, Feature): + fs.append(f()) + 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) + # All subclass proficiencies transfer, regardless of if this is primary class + 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 +97,27 @@ class CharClass(): return 0 else: return self.spell_slots_by_level[self.class_level][spell_level] + + +class SubClass(): + """ + A generic subclass object. Add more detail in the __doc__ attribute. + """ + 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.__doc__ = self.__doc__ or SubClass.__doc__ + 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..e2e2e97 100644 --- a/dungeonsheets/classes/cleric.py +++ b/dungeonsheets/classes/cleric.py @@ -1,6 +1,183 @@ -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): + """The gods of knowledge—including Oghma, Boccob, Gilean, Aureon, and + Thoth—value learning and understanding above all. Some teach that knowledge + is to be gathered and shared in libraries and universities, or promote the + practical knowledge of craft and invention. Some deities hoard knowledge + and keep its secrets to themselves. And some promise their followers that + they will gain tremendous power if they unlock the secrets of the + multiverse. Followers of these gods study esoteric lore, collect old tomes, + delve into the secret places of the earth, and learn all they can. Some + gods of knowledge promote the practical knowledge of craft and invention, + including smith deities like Gond, Reorx, Onatar, Moradin, Hephaestus, and + Goibhniu. + + """ + name = "Knowledge Domain" + features_by_level = defaultdict(list) + + +class LifeDomain(SubClass): + """The Life domain focuses on the vibrant positive energy—one of the + fundamental forces of the universe— that sustains all life. The gods of + life promote vitality and health through healing the sick and wounded, + caring for those in need, and driving away the forces of death and + undeath. Almost any non-evil deity can claim influence over this domain, + particularly agricultural deities (such as Chauntea, Arawai, and Demeter), + sun gods (such as Lathander, Pelor, and Re-Horakhty), gods of healing or + endurance (such as Ilmater, Mishakal, Apollo, and Diancecht), and gods of + home and community (such as Hestia, Hathor, and Boldrei). + + """ + name = "Life Domain" + features_by_level = defaultdict(list) + + +class LightDomain(SubClass): + """Gods of light—including Helm, Lathander, Pholtus, Branchala, the Silver + Flame, Belenus, Apollo, and Re-Horakhty—promote the ideals of rebirth and + renewal, truth, vigilance, and beauty, often using the symbol of the + sun. Some of these gods are portrayed as the sun itself or as a charioteer + who guides the sun across the sky. Others are tireless sentinels whose eyes + pierce every shadow and see through every deception. Some are deities of + beauty and artistry, who teach that art is a vehicle for the soul's + improvement. Clerics of a god of light are enlightened souls infused with + radiance and the power of their gods’ discerning vision, charged with + chasing away lies and burning away darkness. + + """ + name = "Light Domain" + features_by_level = defaultdict(list) + + +class NatureDomain(SubClass): + """Gods of nature are as varied as the natural world itself, from inscrutable + gods of the deep forests (such as Silvanus, Obad-Hai, Chislev, Balinor, and + Pan) to friendly deities associated with particular springs and groves + (such as Eldath). Druids revere nature as a whole and might serve one of + these deities, practicing mysterious rites and reciting all-but-forgotten + prayers in their own secret tongue. But many of these gods have clerics as + well, champions who take a more active role in advancing the interests of + a particular nature god. These clerics might hunt the evil monstrosities + that despoil the woodlands, bless the harvest of the faithful, or wither + the crops of those who anger their gods. + + """ + name = "Nature Domain" + features_by_level = defaultdict(list) + + +class TempestDomain(SubClass): + """Gods whose portfolios include the Tempest domain - including Talos, + Umberlee, Kord, Zeboim, the Devourer, Zeus, and Thor — govern storms, sea, + and sky. They include gods of lightning and thunder, gods of earthquakes, + some fire gods, and certain gods of violence, physical strength, and + courage. In some pantheons, a god of this domain rules over other deities + and is known for swift justice delivered by thunderbolts. In the pantheons + of seafaring people, gods of this domain are ocean deities and the patrons + of sailors. Tempest gods send their clerics to inspire fear in the common + folk, either to keep those folk on the path of righteousness or to + encourage them to offer sacrifices of propitiation to ward off divine + wrath. + + """ + name = "Tempest Domain" + features_by_level = defaultdict(list) + + +class TrickeryDomain(SubClass): + """Gods of trickery—such as Tymora, Beshaba, Olidammara, the Traveler, Garl + Glittergold, and Loki—are mischief-makers and instigators who stand as a + constant challenge to the accepted order among both gods and + mortals. They’re patrons of thieves, scoundrels, gamblers, rebels, and + liberators. Their clerics are a disruptive force in the world, puncturing + pride, mocking tyrants, stealing from the rich, freeing captives, and + flouting hollow traditions. They prefer subterfuge, pranks, deception, and + theft rather than direct confrontation. + + """ + name = "Trickery Domain" + features_by_level = defaultdict(list) + + +class WarDomain(SubClass): + """War has many manifestations. It can make heroes of ordinary people. It can + be desperate and horrific, with acts of cruelty and cowardice eclipsing + instances of excellence and courage. In either case, the gods of war watch + over warriors and reward them for their great deeds. The clerics of such + gods excel in battle, inspiring others to fight the good fight or offering + acts of violence as prayers. Gods of war include champions of honor and + chivalry (such as Torm, Heironeous, and KiriJolith) as well as gods of + destruction and pillage (such as Erythnul, the Fury, Gruumsh, and Ares) and + gods of conquest and domination (such as Bane, Hextor, and + Maglubiyet). Other war gods (such as Tempus, Nike, and Nuada) take a more + neutral stance, promoting war in all its manifestations and supporting + warriors in any circumstance. + + """ + name = "War Domain" + features_by_level = defaultdict(list) + + +# SCAG +class ArcanaDomain(SubClass): + """Magic is an energy that suffuses the multiverse and that fuels both + destruction and creation. Gods of the Arcana domain know the secrets and + potential of magic intimately. For some of these gods, magical knowledge + is a great responsibility that comes with a special understanding of the + nature of reality. Other gods of Arcana see magic as pure power, to be used + as its wielder sees fit. + + The gods of this domain are often associated with knowledge, as learning + and arcane power tend to go hand-in-hand. In the Realms, deities of this + domain include Azuth and Mystra, as well as Corellon Larethian of the + elven pantheon. In other worlds, this domain includes Hecate, Math + Mathonwy, and Isis; the triple moon gods of Solinari , Lunitari, and + Nuitari of Krynn; and Boccob, Vecna, and WeeJas of Greyhawk. + + """ + name = "Arcana Domain" + features_by_level = defaultdict(list) + + +# XGTE +class ForgeDomain(SubClass): + """The gods of the forge are patrons of artisans who work with metal, from a + humble blacksmith who keeps a village in horseshoes and plow blades to the + mighty elf artisan whose diamond-tipped arrows of mithral have felled demon + lords. The gods of the forge teach that, with patience and hard work, even + the most intractable metal can be transformed from a lump of ore to a beau— + tifully wrought object. Clerics of these deities search for objects lost to + the forces of darkness, liberate mines overrun by ores, and uncover rare + and wondrous materials necessary to create potent magic items. Followers + of these gods take great pride in their work, and they are willing to craft + and use heavy armor and powerful weapons to protect them. Deities of this + domain include Gond, Reorx, Onatar, Moradin, Hephaestus, and Goibhniu. + + """ + name = "Forge Domain" + features_by_level = defaultdict(list) + + +class GraveDomain(SubClass): + """Gods of the grave watch over the line between life and death. To these + deities, death and the afterlife are a foundational part of the + multiverse. To desecrate the peace of the dead is an abomination. Deities + of the grave include Kelemvor, Wee jas, the ancestral spirits of the + Undying Court, Hades, Anubis, and Osiris. Followers of these deities seek + to put wandering spirits to rest, destroy the undead, and ease the + suffering of the dying. Their magic also allows them to stave off death for + a time. particularly for a person who still has some great work to + accomplish in the world. This is a delay of death, not a denial of it, for + death will eventually get its due. + + """ + name = "Grave Domain" + features_by_level = defaultdict(list) class Cleric(CharClass): @@ -10,8 +187,15 @@ class Cleric(CharClass): _proficiencies_text = ('light armor', 'medium armor', 'shields', 'all simple weapons') weapon_proficiencies = weapons.simple_weapons + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = ('light armor', 'medium armor', 'shields') 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..f8d5173 100644 --- a/dungeonsheets/classes/druid.py +++ b/dungeonsheets/classes/druid.py @@ -1,18 +1,91 @@ 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): + """The Circle of the Land is made up of mystics and sages who safeguard + ancient knowledge and rites through a vast oral tradition. These druids + meet within sacred circles of trees or standing stones to whisper primal + secrets in Druidic. The circle’s wisest members preside as the chief + priests of communities that hold to the Old Faith and serve as advisors to + the rulers of those folk. As a member of this circle, your magic is + influenced by the land where you were initiated into the circle’s + mysterious rites + + """ + name = "Circle of the Land" + circle = "land" + features_by_level = defaultdict(list) + + +class MoonCircle(SubClass): + """Druids of the Circle of the Moon are fierce guardians of the wilds. Their + order gathers under the full moon to share news and trade warnings. They + haunt the deepest parts of the wilderness, where they might go for weeks on + end before crossing paths with another humanoid creature, let alone another + druid. + + Changeable as the moon, a druid of this circle might prowl as a great cat + one night, soar over the treetops as an eagle the next day, and crash + through the undergrowth in bear form to drive off a trespassing + monster. The wild is in the druid's blood. + + """ + name = "Circle of the Moon" + circle = "moon" + features_by_level = defaultdict(list) + + +# XGTE +class DreamsCircle(SubClass): + """Druids who are members of the Circle of Dreams hail from regions that have + strong ties to the Feywild and its dreamlike realms. The druids’ + guardianship of the natural world makes for a natural alliance between them + and good-aligned fey. These druids seek to fill the world with dreamy + wonder. Their magic mends wounds and brings joy to downcast hearts, and the + realms they protect are gleaming, fruitful places, where dream and reality + blur together and where the weary can find rest. + + """ + name = "Circle of Dreams" + circle = "dreams" + features_by_level = defaultdict(list) + + +class ShepherdCircle(SubClass): + """Druids of the Circle of the Shepherd commune with the spirits of nature, + especially the spirits of beasts and the fey, and call to those spirits for + aid. These druids recognize that all living things play a role in the + natural world, yet they focus on protecting animals and fey creatures that + have difficulty defending themselves. Shepherds, as they are known, see + such creatures as their charges. They ward off monsters that threaten them, + rebuke hunters who kill more prey than necessary, and prevent civilization + from encroaching on rare animal habitats and on sites sacred to the + fey. Many of these druids are happiest far from cities and towns, content + to spend their days in the company of animals and the fey creatures of the + wilds. + + Members of this circle become adventurers to oppose forces that threaten + their charges or to seek knowledge and power that will help them safeguard + their charges better. Wherever these druids go, the spirits of the wil— + derness are with them + + """ + 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', @@ -23,9 +96,17 @@ class Druid(CharClass): weapons.Javelin, weapons.Mace, weapons.Quarterstaff, weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear) + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = ( + 'Light armor', 'medium armor', + 'shields (druids will not wear armor or use shields made of metal)') 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 +130,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 +216,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..1f092a3 100644 --- a/dungeonsheets/classes/fighter.py +++ b/dungeonsheets/classes/fighter.py @@ -1,6 +1,165 @@ -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): + """The archetypal Champion focuses on the development of raw physical power + honed to deadly perfection. Those who model themselves on this archetype + combine rigorous training with physical excellence to deal devastating + blows. + + """ + name = "Champion" + features_by_level = defaultdict(list) + + +class BattleMaster(SubClass): + """Those who emulate the archetypal Battle Master employ martial techniques + passed down through generations. To a Battle Master, combat is an academic + field, sometimes including subjects beyond battle such as weaponsmithing + and calligraphy. Not every fighter absorbs the lessons of history, theory, + and artistry that are reflected in the Battle Master archetype, but those + who do are well-rounded fighters of great skill and knowledge + + """ + name = "Battle Master" + features_by_level = defaultdict(list) + + +class EldritchKnight(SubClass): + """The archetypal Eldritch Knight combines the martial mastery common to all + fighters with a careful study of magic. Eldritch Knights use magical + techniques similar to those practiced by wizards. They focus their study on + two of the eight schools of magic: abjuration and evocation. Abjuration + spells grant an Eldritch Knight additional protection in battle, and + evocation spells deal damage to many foes at once, extending the fighter’s + reach in combat. These knights learn a comparatively small number of + spells, committing them to memory instead of keeping them in a spellbook. + + """ + 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): + """Purple Dragon knights are warriors who hail from the kingdom of + Cormyr. Pledged to protect the crown, they take the fight against evil + beyond their kingdom's borders. They are tasked with wandering the land as + knights errant, relying on their judgment, bravery, and fidelity to the + code of chivalry to guide them in defeating evildoers. + + A Purple Dragon knight inspires greatness in others by committing brave + deeds in battle. The mere presence of a knight in a hamlet is enough to + cause some ores and bandits to seek easier prey. A lone knight is a skilled + warrior, but a knight leading a band of allies can transform even the most + poorly equipped militia into a ferocious war band. + + A knight prefers to lead through deeds, not words. As a knight spearheads + an attack, the knight's actions can awaken reserves of courage and + conviction in allies that they never suspected they had. + + """ + name = "Purple Dragon Knight" + features_by_level = defaultdict(list) + + +# XGTE +class ArcaneArcher(SubClass): + """An Arcane Archer studies a unique elven method of archery that weaves magic + into attacks to produce supernatural effects. Arcane Archers are some of + the most elite warriors among the elves. They stand watch over the fringes + of elven domains, keeping a keen eye out for trespassers and using + magic—infused arrows to defeat monsters and invaders before they can reach + elven set— tlements. Over the centuries, the methods of these elf archers + have been learned by members of other races who can also balance arcane + aptitude with archery. + + """ + name = "Arcane Archer" + features_by_level = defaultdict(list) + + +class Cavalier(SubClass): + """The archetypal Cavalier excels at mounted combat. Usually born among the + nobility and raised at court, a Cavalier is equally at home leading a + cavalry charge or exchanging repartee at a state dinner. Cavaliers also + learn how to guard those in their charge from harm, often serving as the + protectors of their superiors and of the weak. Compelled to right wrongs or + earn prestige, many of these fighters leave their lives of comfort to + embark on glorious adventure + + """ + name = "Cavalier" + features_by_level = defaultdict(list) + + +class Samurai(SubClass): + """The Samurai is a fighter who draws on an implacable fighting spirit to + overcome enemies. A Samurai’s resolve is nearly unbreakable, and the + enemies in a Samurai's path have two choices: yield or die fighting + + """ + name = "Samurai" + features_by_level = defaultdict(list) + + +# Custom +class Gunslinger(SubClass): + """Most warriors and combat specialists spend their years perfecting the + classic arts of swordplay, archery, or polearm tactics. Whether duelist or + infantry, martial weapons were seemingly perfected long ago, and the true + challenge is to master them. + + However, some minds couldn't stop with the innovation of the + crossbow. Experimentation with alchemical components and rare metals have + unlocked the secrets of controlled explosive force. The few who survive + these trials of ingenuity may become the first to create, and deftly wield, + the first firearms. + + This archetype focuses on the ability to design, craft, and utilize + powerful, yet dangerous ranged weapons. Through creative innovation and + immaculate aim, you become a distance force of death on the + battlefield. However, not being a perfect science, firearms carry an + inherent instability that can occastionally leave you without a functional + means of attack. This is the danger of new, untested technologies in a + world where arcane energies that rule the elements are ever present. + + Should this path of powder, fire, and metal call to you, keep your wits + about you, hold on to your convictions as a fighter, and let skill meet + luck to guide your bullets to strike true. + + """ + name = "Gunslinger" + features_by_level = defaultdict(list) + weapon_proficiencies = (weapons.firearms) + _proficiencies_text = ('firearms') class Fighter(CharClass): @@ -17,4 +176,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, Gunslinger) diff --git a/dungeonsheets/classes/monk.py b/dungeonsheets/classes/monk.py index 6603603..48527ec 100644 --- a/dungeonsheets/classes/monk.py +++ b/dungeonsheets/classes/monk.py @@ -1,10 +1,112 @@ __all__ = ('Monk') from .. import (features, weapons) -from .classes import CharClass +from .classes import CharClass, SubClass from collections import defaultdict +# PHB +class OpenHandWay(SubClass): + """Monks of the Way of the Open Hand are the ultimate masters of martial arts + combat, whether armed or unarmed. They learn techniques to push and trip + their opponents, manipulate ki to heal damage to their bodies, and practice + advanced meditation that can protect them from harm. + + """ + name = "Way of the Open Hand" + features_by_level = defaultdict(list) + + +class ShadowWay(SubClass): + """Monks of the Way of Shadow follow a tradition that values stealth and + subterfuge. These monks might be called ninjas or shadowdancers, and they + serve as spies and assassins. Sometimes the members of a ninja monastery + are family members, forming a clan sworn to secrecy about their arts and + missions. Other monasteries are more like thieves’ guilds, hiring out their + services to nobles, rich merchants, or anyone else who can pay their + fees. Regardless of their methods, the heads of these monasteries expect + the unquestioning obedience of their students + + """ + name = "Way of Shadow" + features_by_level = defaultdict(list) + + +class FourElementsWay(SubClass): + """You follow a monastic tradition that teaches you to harness the + elements. When you focus your ki, you can align yourself with the forces of + creation and bend the four elements to your will, using them as an + extension of your body. Some members of this tradition dedicate themselves + to a single element, but others weave the elements together. + + Many monks of this tradition tattoo their bodies with representations of + their ki powers, commonly imagined as coiling dragons, but also as + phoenixes, fish, plants, mountains, and cresting waves. + + """ + name = "Way of the Four Elements" + features_by_level = defaultdict(list) + + +# SCAG +class SunSoulWay(SubClass): + """Monks of the Way of the Sun Soul learn to channel their own life energy + into searing bolts of light. They teach that meditation can unlock the + ability to unleash the indomitable light shed by the soul of every living + creature + + """ + name = "Way of the Sun Soul" + features_by_level = defaultdict(list) + + +class LongDeathWay(SubClass): + """Monks of the Way of the Long Death are obsessed with the meaning and + mechanics of dying. They capture creatures and prepare elaborate + experiments to capture, record, and understand the moments of their + demise. They then use this knowledge to guide their understanding of + martial arts, yielding a deadly fighting style. + + """ + name = "Way of the Long Death" + features_by_level = defaultdict(list) + + +# XGTE +class DrunkenMasterWay(SubClass): + """The Way of the Drunken Master teaches its students to move with the jerky, + unpredictable movements of a drunkard. A drunken master sways, tottering on + unsteady feet, to present what seems like an incompetent combatant who + proves frustrating to engage. The drunken master’s erratic stumbles conceal + a carefully executed dance of blocks, parries, advances, attacks, and + retreats. + + A drunken master often enjoys playing the fool to bring gladness to the + despondent or to demonstrate humility to the arrogant, but when battle is + joined, the drunken master can be a maddening, masterful foe + + """ + name = "Way of the Drunken Master" + features_by_level = defaultdict(list) + + +class KenseiWay(SubClass): + """Monks of the Way of the Kensei train relentlessly with their weapons, to + the point where the weapon becomes an extension of the body. Founded on a + mastery of sword fighting, the tradition has expanded to include many + different weapons. + + A kensei sees a weapon in much the same way a calligrapher or painter + regards a pen or brush. Whatever the weapon, the kensei views it as a tool + used to express the beauty and precision of the martial arts. That such + mastery makes a kensei a peerless warrior is but a side effect of intense + devotion, practice, and study. + + """ + name = "Way of the Kensei" + features_by_level = defaultdict(list) + + class Monk(CharClass): class_name = 'Monk' hit_dice_faces = 8 @@ -13,9 +115,15 @@ class Monk(CharClass): 'simple weapons', 'shortswords', 'unarmed', "one type of artisan's tools or one musical instrument") weapon_proficiencies = (weapons.Shortsword, weapons.Unarmed) + weapons.simple_weapons + multiclass_weapon_proficiencies = weapon_proficiencies + _multiclass_proficiencies_text = ('simple weapons', 'shortswords', + 'unarmed') 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 +133,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..76db64a 100644 --- a/dungeonsheets/classes/paladin.py +++ b/dungeonsheets/classes/paladin.py @@ -1,7 +1,223 @@ -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): + """The Oath of Devotion binds a paladin to the loftiest ideals of justice, + virtue, and order. Sometim es called cavaliers, white knights, or holy + warriors, these paladins meet the ideal of the knight in shining armor, + acting with honor in pursuit o f justice and the greater good. They hold + themselves to the highest standards of conduct, and some, for better or + worse, hold the rest of the world to the same standards. Many who swear + this oath are devoted to gods of law and good and use their gods’ tenets as + the measure o f their devotion. They hold angels—the perfect servants o f + good—as their ideals, and incorporate images of angelic wings into their + helmets or coats of arms. + + **Tenets of Devotion**: Though the exact words and strictures of the Oath + of Devotion vary, paladins of this oath share these tenets. + + --Honesty. Don’t lie or cheat. Let your word be your promise. + + --Courage. Never fear to act, though caution is wise. + + --Compassion. Aid others, protect the weak, and punish those who threaten + them. Show mercy to your foes, but temper it with wisdom. + + --Honor. Treat others with fairness, and let your honorable deeds be an + example to them. Do as much good as possible while causing the least amount + of harm. + + --Duty. Be responsible for your actions and their consequences, protect + those entrusted to your care, and obey those who have just authority over + you. + + """ + name = "Oath of Devotion" + features_by_level = defaultdict(list) + + +class OathOfAncients(SubClass): + """The Oath of the Ancients is as old as the race of elves and the rituals of + the druids. Sometimes called fey knights, green knights, or horned knights, + paladins who swear this oath cast their lot with the side of the light in + the cosm ic struggle against darkness because they love the beautiful and + life-giving things of the world, not necessarily because they believe in + principles of honor, courage, and justice. They adorn their armor and + clothing with images of growing things—leaves, antlers, or flowers—to + reflect their commitment to preserving life and light in the world. + + **Tenets of the Ancients**: The tenets of the Oath of the Ancients have + been preserved for uncounted centuries. This oath emphasizes the + principles of good above any concerns of law or chaos. Its four central + principles are simple. + + --Kindle the Light. Through your acts of mercy, kindness, and forgiveness, + kindle the light of hope in the world, beating back despair. + + --Shelter the Light. Where there is good, beauty, love, and laughter in the + world, stand against the w ickedness that would swallow it. Where life + flourishes, stand against the forces that would render it barren. + + --Preserve Your Own Light. Delight in song and laughter, in beauty and + art. If you allow the light to die in your own heart, you can’t preserve it + in the world. + + --Be the Light. Be a glorious beacon for all who live in despair. Let the + light of your joy and courage shine forth in all your deeds. + + """ + name = "Oath of The Ancients" + features_by_level = defaultdict(list) + + +class OathOfVengance(SubClass): + """The Oath of Vengeance is a solemn commitment to punish those who have + committed a grievous sin. When evil forces slaughter helpless villagers, + when an entire people turns against the will of the gods, when a thieves’ + guild grows too violent and powerful, when a dragon rampages through the + countryside—at times like these, paladins arise and swear an Oath of + Vengeance to set right that which has gone wrong. To these paladins— + sometimes called avengers or dark knights—their own purity is not as + important as delivering justice. + + **Tenets of Vengance**: The tenets of the Oath of Vengeance vary by + paladin, but all the tenets revolve around punishing wrongdoers by any + means necessary. Paladins who uphold these tenets are willing to sacrifice + even their own righteousness to mete out justice upon those who do evil, so + the paladins are often neutral or lawful neutral in alignment. The core + principles of the tenets are brutally simple. + + --Fight the Greater Evil. Faced with a choice of fighting my sworn foes or + combating a lesser evil. I choose the greater evil. + + --No Mercy for the Wicked. Ordinary foes might win my mercy, but my sworn + enemies do not. + + --By Any Means Necessary. My qualms can’t get in the way of exterminating + my foes. + + --Restitution. If my foes wreak ruin on the world, it is because I failed + to stop them. I must help those harmed by their misdeeds. + + """ + name = "Oath of Vengance" + features_by_level = defaultdict(list) + + +class OathOfCrown(SubClass): + """The Oath of the Crown is sworn to the ideals of civilization, be it the + spirit of a nation, fealty to a sovereign, or service to a deity of law and + rulership. The paladins who swear this oath dedicate themselves to serving + society and, in particular, the just laws that hold society together. These + paladins are the watchful guardians on the walls, standing against the + chaotic tides of barbarism that threaten to tear down all that + civilization has built, and are commonly known as guardians, exemplars, + or sentinels. Often, paladins who swear this oath are members of an order + of knighthood in service to a nation or a sovereign, and undergo their oath + as part of their admission to the order's ranks. + + **Tenets of the Crown**: The tenets of the Oath of the Crown are often set + by the sovereign to which their oath is sworn, but generally emphasize + the following tenets. + + --Law. The law is paramount. It is the mortar that holds the stones of + civilization together, and it must be respected. + + --Loyalty. Your word is your bond. Without loyalty, oaths and laws are + meaningless. + + --Courage. You must be willing to do what needs to be done for the sake of + order, even in the face of overwhelming odds. If you don't act, then who + will? + + --Responsibility. You must deal with the consequences of your actions, and + you are responsible for fulfilling your duties and obligations. + + """ + name = "Oath of The Crown" + features_by_level = defaultdict(list) + + +class OathOfConquest(SubClass): + """The Oath of Conquest calls to paladins who seek glory in battle and the + subjugation of their enemies. It isn’t enough for these paladins to + establish order. They must crush the forces of chaos. Sometimes called + knight ty- rants or iron mongers, those who swear this oath gather into + grim orders that serve gods or philosophies of war and well-ordered might. + + Some of these paladins go so far as to consort with the powers of the Nine + Hells, valuing the rule of law over the balm of mercy. The archdevil Bel, + warlord of Avernus, counts many of these paladins—called hell knights—as + his most ardent supporters. Hell knights cover their armor with trophies + taken from fallen en— emies, a grim~warning to any who dare oppose them and + the decrees of their lords. These knights are often most fiercely resisted + by other paladins of this oath, who believe that the hell knights have + wandered too far into darkness. + + **Tenets of Conquest**: A paladin who takes this oath has the tenets of + conquest seared on the upper arm. + + --Douse the Flame of Hope. It is not enough + to merely defeat an enemy in battle. Your victory must be so over- whelming + that your enemies' will to fight is shattered forever. A blade can end a + life. Fear can end an empire. + + --Rule with an Iron Fist. Once you have conquered, tolerate no + dissent. Your word is law. Those who obey it shall be favored. Those who + defy it shall be punished as an example to all who might follow. + + --Strength Above All. You shall rule until a stronger one arises. Then you + must grow mightier and meet the challenge, or fall to your own ruin. + + """ + name = "Oath of Conquest" + features_by_level = defaultdict(list) + + +class OathOfRedemption(SubClass): + """The Oath of Redemption sets a paladin on a difficult path, one that requires + a holy warrior to use violence only as a last resort. Paladins who dedicate + themselves to this oath believe that any person can be redeemed and that + the path of benevolence and justice is one that anyone can walk. These + paladins face evil creatures in the hope of turning their foes to the + light, and they slay their enemies only when such a deed will clearly save + other lives. Paladins who follow this path are known as redeemers. + + While redeemers are idealists, they are no fools. Re— deemers know that + undead, demons, devils, and other supernatural threats can be inherently + evil. Against such fees, paladins who swear this oath bring the full wrath + of their weapons and spells to bear. Yet the re- deemers still pray that, + one day, even creatures of wick- edness will invite their own redemption. + + **Tenets of Redemption**: The tenets of the Oath of Redemption hold a + paladin to a high standard of peace and justice. + + --Peace. Violence is a weapon of last resort. Diplomacy and understanding + are the paths to long-lasting peace. + + --Innocence. All people begin life in an innocent state, and it is their + environment or the influence of dark forces that drives them to evil. By + setting the proper example, and working to heal the wounds of a deeply + flawed world, you can set anyone on a righteous path. + + --Patience. Change takes time. Those who have walked the path of the wicked + must be given reminders to keep them honest and true. Once you have planted + the seed of righteousness in a creature, you must work day after day to + allow that seed to survive and flourish. + + --Wisdom. Your heart and mind must stay clear, for eventually you will be + forced to admit defeat. While ev- ery creature can be redeemed, some are so + far along the path of evil that you have no choice but to end their lives + for the greater good. Any such action must be carefully weighed and the + consequences fully understood, but once you have made the decision, follow + through with it knowing your path is just. + + """ + name = "Oath of Redemption" + features_by_level = defaultdict(list) + class Paladin(CharClass): class_name = 'Paladin' @@ -10,8 +226,14 @@ class Paladin(CharClass): _proficiencies_text = ('All armor', 'shields', 'simple weapons', 'martial weapons') weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons + multiclass_weapon_proficiencies = weapon_proficiencies + _multiclass_proficiencies_text = ('light armor', 'medium armor', 'shields', + 'simple 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..e697727 100644 --- a/dungeonsheets/classes/ranger.py +++ b/dungeonsheets/classes/ranger.py @@ -1,10 +1,76 @@ -__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): + """Emulating the Hunter archetype means accepting your place as a bulwark + between civilization and the terrors of the wilderness. As you walk the + Hunter’s path, you learn specialized techniques for fighting the threats + you face, from rampaging ogres and hordes of orcs to towering giants and + terrifying dragons. + + """ + name = "Hunter" + features_by_level = defaultdict(list) + + +class BeastMaster(SubClass): + """The Beast Master archetype embodies a friendship between the civilized + races and the beasts of the world. United in focus, beast and ranger work + as one to fight the monstrous foes that threaten civilization and the + wilderness alike. Emulating the Beast Master archetype means committing + yourself to this ideal, working in partnership with an animal as its + companion and friend. + + """ + name = "Beast Master" + features_by_level = defaultdict(list) + + +# XGTE +class GloomStalker(SubClass): + """Gloom Stalkers are at home in the darkest places: deep under the earth, in + gloomy alleyways, in primeval forests, and wherever else the light + dims. Most folk enter such places with trepidation, but a Gloom Stalker + ventures boldly into the darkness, seeking to ambush threats before they + can reach the broader world. Such rangers are often found in the Underdark, + but they will go any place Where evil lurks in the shadows + + """ + name = "Gloom Stalker" + features_by_level = defaultdict(list) + + +class HorizonWalker(SubClass): + """Horizon Walkers guard the world against threats that originate from other + planes or that seek to ravage the mortal realm with otherworldly + magic. They seek out planar portals and keep watch over them, venturing to + the Inner Planes and the Outer Planes as needed to pursue their foes. These + rangers are also friends to any forces in the multiverse—especially + benevolent dragons, fey, and elementals—that work to preserve life and the + order of the planes + + """ + name = "Horizon Walker" + features_by_level = defaultdict(list) + + +class MonsterSlayer(SubClass): + """You have dedicated yourself to hunting down creatures of the night and + wielders of grim magic. A Monster Slayer seeks out vampires, dragons, evil + fey, fiends, and other magical threats. Trained in supernatural tech- + niques to overcome such monsters, Slayers are experts at unearthing and + defeating mighty, mystical foes. + + """ + name = "Monster Slayer" + features_by_level = defaultdict(list) + + class Ranger(CharClass): class_name = 'Ranger' hit_dice_faces = 10 @@ -12,12 +78,18 @@ class Ranger(CharClass): _proficiencies_text = ("light armor", "medium armor", "shields", "simple weapons", "martial weapons") weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons + multiclass_weapon_proficiencies = weapon_proficiencies + _multiclass_proficiencies_text = ('light armor', 'medium armor', 'shields', + 'simple weapons', 'martial weapons', + '[choose one skill from Ranger list]') class_skill_choices = ('Animal Handling', 'Athletics', 'Insight', '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), @@ -48,7 +120,44 @@ class Ranger(CharClass): feature_choices=params.get('feature_choices', [])) self.features_by_level[2].append(fighting_style) + +# Revised Ranger +class BeastConclave(SubClass): + """Many rangers are more at home in the wilds than in civilization, to the + point where animals consider them kin. Rangers of the Beast Conclave + develop a close bond with a beast, then further strengthen that bond + through the use of magic. + + """ + name = "Beast Conclave" + features_by_level = defaultdict(list) + + +class HunterConclave(SubClass): + """Some rangers seek to master weapons to better protect civilization from the + terrors of the wilderness. Members of the Hunter Conclave learn specialized + fighting techniques for use against the most dire threats, from rampaging + ogres and hordes of orcs to towering giants and terrifying dragons + + """ + name = "Hunter Conclave" + features_by_level = defaultdict(list) + + +class DeepStalkerConclave(SubClass): + """Most folk descend into the depths of the Underdark only under the most + pressing conditions, undertaking some desperate quest or following the + promise of vast riches. All too often, evil festers beneath the earth + unnoticed, and rangers of the Deep Stalker Conclave strive to uncover and + defeat such threats before they can reach the surface. + + """ + 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) -# Custom Classes -class Revisedranger(Ranger): - class_name = 'Revisedranger' diff --git a/dungeonsheets/classes/rogue.py b/dungeonsheets/classes/rogue.py index d51178c..77101e5 100644 --- a/dungeonsheets/classes/rogue.py +++ b/dungeonsheets/classes/rogue.py @@ -1,6 +1,118 @@ -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): + """You hone your skills in the larcenous arts. Burglars, bandits, cutpurses, + and other criminals typically follow this archetype, but so do rogues who + prefer to think of themselves as professional treasure seekers, explorers, + delvers, and investigators. In addition to improving your agility and + stealth, you learn skills useful for delving into ancient ruins, reading + unfamiliar languages, and using magic items you normally couldn’t employ + + """ + name = "Thief" + features_by_level = defaultdict(list) + + +class Assassin(SubClass): + """You focus your training on the grim art of death. Those who adhere to this + archetype are diverse: hired killers, spies, bounty hunters, and even + specially anointed priests trained to exterminate the enemies of their + deity. Stealth, poison, and disguise help you eliminate your foes with + deadly efficiency + + """ + name = "Assassin" + features_by_level = defaultdict(list) + + +class ArcaneTrickster(SubClass): + """Some rogues enhance their fine-honed skills of stealth and agility with + magic, learning tricks of enchantment and illusion. These rogues include + pickpockets and burglars, but also pranksters, mischief-makers, and a + significant number of adventurers. + + """ + 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): + """As an archetypal Inquisitive, you excel at rooting out se- crets and + unraveling mysteries. You rely on your sharp eye for detail, but also on + your finely honed ability to read the words and deeds of other creatures to + deter- mine their true intent. You excel at defeating creatures that hide + among and prey upon ordinary folk, and your mastery of lore and your keen + deductions make you well equipped to expose and end hidden evils + + """ + name = "Inquisitive" + features_by_level = defaultdict(list) + + +class Mastermind(SubClass): + """Your focus is on people and on the influence and secrets they have. Many + spies, courtiers, and schemers follow this archetype, leading lives of + intrigue. Words are your weapons as often as knives or poison, and secrets + and favors are some of your favorite treasures. + + """ + name = "Mastermind" + features_by_level = defaultdict(list) + + +class Scout(SubClass): + """You are skilled in stealth and surviving far from the streets of a city, + allowing you to scout ahead of your companions during expeditions. Rogues + who embrace this archetype are at home in the wilderness and among + barbarians and rangers, and many Scouts serve as the eyes and ears of war + bands. Ambusher, spy, bounty hunter#these are just a few of the roles that + Scouts as- sume as they range the world. + + """ + name = "Scout" + features_by_level = defaultdict(list) + + +class Swashbuckler(SubClass): + """You focus your training on the art of the blade, relying on speed, + elegance, and charm in equal parts. While some warriors are brutes clad in + heavy armor, your method of fighting looks almost like a performance. Du— + elists and pirates typically belong to this archetype. A Swashbuckler + excels in single combat, and can fight with two weapons while safely + darting away from an opponent + + """ + name = "Swashbuckler" + features_by_level = defaultdict(list) class Rogue(CharClass): @@ -13,8 +125,13 @@ class Rogue(CharClass): weapon_proficiencies = weapons.simple_weapons + ( weapons.HandCrossbow, weapons.Longsword, weapons.Rapier, weapons.Shortsword) + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = ('light armor', "thieves' tools", + '[choose one skill from Rogue list]') class_skill_choices = ('Acrobatics', 'Athletics', 'Deception', 'Insight', 'Intimidation', 'Investigation', 'Perception', 'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth') - + features_by_level = defaultdict(list) + subclasses_available = (Thief, Assassin, ArcaneTrickster, + Inquisitive, Mastermind, Scout, Swashbuckler) diff --git a/dungeonsheets/classes/sorceror.py b/dungeonsheets/classes/sorceror.py index e802b4d..12a56dd 100644 --- a/dungeonsheets/classes/sorceror.py +++ b/dungeonsheets/classes/sorceror.py @@ -1,6 +1,94 @@ -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): + """Your innate magic comes from draconic magic that was mingled with your + blood or that of your ancestors. Most often, sorcerers with this origin + trace their descent back to a mighty sorcerer of ancient times who made a + bargain with a dragon or who might even have claimed a dragon parent. Some + of these bloodlines are well established in the world, but most are + obscure. Any given sorcerer could be the first of a new bloodline, as a + result of a pact or some other exceptional circumstance. + + """ + name = "Draconic Bloodline" + features_by_level = defaultdict(list) + + +class WildMagic(SubClass): + """Your innate magic comes from the wild forces of chaos that underlie the + order of creation. You might have endured exposure to some form o f raw + magic, perhaps through a planar portal leading to Limbo, the Elemental + Planes, or the mysterious Far Realm. Perhaps you were blessed by a powerful + fey creature or marked by a demon. Or your magic could be a fluke of your + birth, with no apparent cause or reason. However it came to be, this + chaotic magic churns within you, waiting for any outlet. + + """ + name = "Wild Magic" + features_by_level = defaultdict(list) + + +# XGTE +class DivineSoul(SubClass): + """Sometimes the spark of magic that fuels a sorcerer comes from a divine + source that glimmers within the soul. Having such a blessed soul is a sign + that your innate magic might come from a distant but powerful familial + connection to a divine being. Perhaps your ances— tor was an angel, + transformed into a mortal and sent to fight in a god’s name. Or your birth + might align with an ancient prophecy, marking you as a servant of the gods + or a chosen vessel of divine magic. + + A Divine Soul, with a natural magnetism, is seen as a threat by some + religious hierarchies. As an outsider who commands sacred power, a Divine + Soul can undermine an existing order by claiming a direct tie to the + divine. + + In some cultures, only those who can claim the power of a Divine Soul may + command religious power. In these lands, ecclesiastical positions are + dominated by a few bloodlines and preserved over generations + + """ + name = "Divine Soul" + features_by_level = defaultdict(list) + + +class ShadowMagic(SubClass): + """You are a creature of shadow, for your innate magic comes from the + Shadowfell itself. You might trace your lineage to an entity from that + place, or perhaps you were exposed to its fell energy and transformed by + it. + + The power of shadow magic casts a strange pall over your physical + presence. The spark of life that sustains you is muffled, as if it + struggles to remain viable against the dark energy that imbues your + soul. At your option, you can pick from or roll on the Shadow Sorcerer + Quirks table to create a quirk for your character + + """ + name = "Shadow Magic" + features_by_level = defaultdict(list) + + +class StormSorcery(SubClass): + """Your innate magic comes from the power of elemental air. Many with this + power can trace their magic back to a near-death experience caused by the + Great Rain, but perhaps you were born during a howling gale so powerful + that folk still tell stories of it, or your lineage might include the + influence of potent air creatures such as djinn. Whatever the case, the + magic of the storm permeates your being. + + Storm sorcerers are invaluable members of a ship’s crew. Their magic allows + them to exert control over wind and weather in their immediate area. Their + abilities also prove useful in repelling attacks by sahuagin, pirates, + and other waterborne threats. + + """ + name = "Storm Sorcery" + features_by_level = defaultdict(list) class Sorceror(CharClass): @@ -12,8 +100,13 @@ class Sorceror(CharClass): weapon_proficiencies = (weapons.Dagger, weapons.Dart, weapons.Sling, weapons.Quarterstaff, weapons.LightCrossbow) + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = () 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..62865cc 100644 --- a/dungeonsheets/classes/warlock.py +++ b/dungeonsheets/classes/warlock.py @@ -1,6 +1,115 @@ -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): + """Your patron is a lord or lady of the fey, a creature of legend who holds + secrets that were forgotten before the mortal races were born. This being’s + motivations are often inscrutable, and sometimes whimsical, and might + involve a striving for greater magical power or the settling of age-old + grudges. Beings of this sort include the Prince of Frost; the Queen of Air + and Darkness, ruler of the Gloaming Court; Titania of the Summer Court; her + consort Oberon, the Green Lord; Hyrsam, the Prince of Fools; and ancient + hags + + """ + name = "The Archfey Patron" + features_by_level = defaultdict(list) + + +class Fiend(SubClass): + """You have made a pact with a fiend from the lower planes o f existence, a + being whose aims are evil, even if you strive against those aims. Such + beings desire the corruption or destruction of all things, ultimately + including you. Fiends powerful enough to forge a pact include demon lords + such as Demogorgon, Orcus, Fraz’Urb-luu, and Baphomet; archdevils such as + Asmodeus, Dispater, Mephistopheles, and Belial; pit fiends and balors that + are especially mighty; and ultroloths and other lords of the yugoloths + + """ + name = "The Fiend Patron" + features_by_level = defaultdict(list) + + +class GreatOldOne(SubClass): + """Your patron is a mysterious entity whose nature is utterly foreign to the + fabric of reality. It might come from the Far Realm, the space beyond + reality, or it could be one of the elder gods known only in legends. Its + motives are incomprehensible to mortals, and its knowledge so immense and + ancient that even the greatest libraries pale in comparison to the vast + secrets it holds. The Great Old One might be unaware of your existence or + entirely indifferent to you, but the secrets you have learned allow you to + draw your magic from it. + + Entities of this type include Ghaunadar, called That Which Lurks; + Tharizdun, the Chained God; Dendar, the Night Serpent; Zargon, the + Returner; Great Cthulhu; and other unfathomable beings. + + """ + name = "The Great Old One Patron" + features_by_level = defaultdict(list) + + +# SCAG +class Undying(SubClass): + """Death holds no sway over your patron, who has un- locked the secrets of + everlasting life, although such a prize- like all power- comes at a + price. Once mortal, the Undying has seen mortal lifetimes pass like the + sea- sons, like the flicker of endless days and nights. It has the secrets + of the ages to share, secrets of life and death. Beings of this sort + include Vecna, Lord of the Hand and the Eye; the dread Iuz; the lich-queen + Vol; the Undying Court of Aerenal; Vlaakith, lich-queen of the githyanki; + and the deathless wizard Fistandantalus. + + In the Realms, Undying patrons include Larloch the Shadow King, legendary + master of Warlock's Crypt, and Gilgeam, the God-King of Unther + + """ + name = "The Undying Patron" + features_by_level = defaultdict(list) + + +# XGTE +class Celestial(SubClass): + """Your patron is a powerful being of the Upper Planes. You have bound yourself + to an ancient empyrean, solar, ki-rin, unicorn, or other entity that + resides in the planes of everlasting bliss. Your pact with that being + allows you to experience the barest touch of the holy light that illu— + minates the multiverse. + + Being connected to such power can cause changes in your behavior and + beliefs. You might find yourself driven to annihilate the undead, to defeat + fiends, and to protect the innocent. At times, your heart might also be + filled with a longing for the celestial realm of your patron, and a desire + to wander that paradise for the rest of your days. But you know that your + mission is among mortals for now, and that your pact binds you to bring + light to the dark places of the world. + + """ + name = "The Celestial Patron" + features_by_level = defaultdict(list) + + +class Hexblade(SubClass): + """You have made your pact with a mysterious entity from the Shadowfell—a force + that manifests in sentient magic weapons carved from the stuff of + shadow. The mighty sword Blackrazor is the most notable of these weapons, + which have been spread across the multiverse over the ages. The shadowy + force behind these weapons can offer power to warlocks who form pacts with + it. Many hexhlade warlocks create weapons that emulate those formed in the + Shadowfell. Others forgo such arms, content to weave the dark magic of that + plane into their spellcasting. + + Because the Raven Queen is known to have forged the first of these weapons, + many sages speculate that she and the force are one and that the weapons, + along with hexblade warlocks, are tools she uses to manipulate events on + the Material Plane to her inscrutable ends + + """ + name = "Hexblade Patron" + features_by_level = defaultdict(list) class Warlock(CharClass): @@ -12,6 +121,11 @@ class Warlock(CharClass): 'Intimidation', 'Investigation', 'Nature', 'Religion') weapon_proficiencies = weapons.simple_weapons + multiclass_weapon_proficiencies = weapon_proficiencies + _multiclass_proficiencies_text = ('light armor', '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..a5f0cf7 100644 --- a/dungeonsheets/classes/wizard.py +++ b/dungeonsheets/classes/wizard.py @@ -1,6 +1,159 @@ -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): + """The School of Abjuration emphasizes magic that blocks, banishes, or + protects. Detractors of this school say that its tradition is about denial, + negation rather than positive assertion. You understand, however, that + ending harmful effects, protecting the weak, and banishing evil influences + is anything but a philosophical void. It is a proud and respected vocation. + + Called abjurers, members of this school are sought when baleful spirits + require exorcism, when important locations must be guarded against magical + spying, and when portals to other planes of existence must be closed. + + """ + name = "School of Abjuration" + features_by_level = defaultdict(list) + + +class Conjuration(SubClass): + """As a conjurer, you favor spells that produce objects and creatures out o f + thin air. You can conjure billowing clouds of killing fog or summon + creatures from elsewhere to fight on your behalf. As your mastery grows, + you learn spells of transportation and can teleport yourself across vast + distances, even to other planes of existence, in an instant + + """ + name = "School of Conjuration" + features_by_level = defaultdict(list) + + +class Divination(SubClass): + """The counsel of a diviner is sought by royalty and commoners alike, for all + seek a clearer understanding of the past, present, and future. As a + diviner, you strive to part the veils of space, time, and consciousness so + that you can see clearly. You work to master spells of discernment, remote + viewing, supernatural knowledge, and foresight. + + """ + name = "School of Divination" + features_by_level = defaultdict(list) + + +class Enchantment(SubClass): + """As a member of the School of Enchantment, you have honed your ability to + magically entrance and beguile other people and monsters. Some enchanters + are peacemakers who bewitch the violent to lay down their arms and charm + the cruel into showing mercy. Others are tyrants who magically bind the + unwilling into their service. Most enchanters fall somewhere in between. + + """ + name = "School of Enchantment" + features_by_level = defaultdict(list) + + +class Evocation(SubClass): + """You focus your study on magic that creates powerful elemental effects such + as bitter cold, searing flame, rolling thunder, crackling lightning, and + burning acid. Some evokers find employment in military forces, serving as + artillery to blast enemy armies from afar. Others use their spectacular + power to protect the weak, while some seek their own gain as bandits, + adventurers, or aspiring tyrants. + + """ + name = "School of Evocation" + features_by_level = defaultdict(list) + + +class Illusion(SubClass): + """You focus your studies on magic that dazzles the senses, befuddles the + mind, and tricks even the wisest folk. Your magic is subtle, but the + illusions crafted by your keen mind make the impossible seem real. Some + illusionists—including many gnome w izards—are benign tricksters who use + their spells to entertain. Others are more sinister masters of deception, + using their illusions to frighten and fool others for their personal gain. + + """ + name = "School of Illusion" + features_by_level = defaultdict(list) + + +class Necromancy(SubClass): + """The School of Necromancy explores the cosm ic forces of life, death, and + undeath. As you focus your studies in this tradition, you learn to + manipulate the energy that animates all living things. As you progress, you + learn to sap the life force from a creature as your magic destroys its + body, transforming that vital energy into magical power you can manipulate. + + Most people see necromancers as menacing, or even villainous, due to the + close association with death. Not all necromancers are evil, but the forces + they manipulate are considered taboo by many societies + + """ + name = "School of Necromancy" + features_by_level = defaultdict(list) + + +class Transmutation(SubClass): + """You are a student of spells that modify energy and matter. To you, the + world is not a fixed thing, but eminently mutable, and you delight in being + an agent of change. You wield the raw stuff of creation and learn to alter + both physical forms and mental qualities. Your magic gives you the tools to + become a smith on reality’s forge. + + Some transmuters are tinkerers and pranksters, turning people into toads + and transforming copper into silver for fun and occasional profit. Others + pursue their magical studies with deadly seriousness, seeking the power of + the gods to make and destroy worlds + + """ + name = "School of Transmutation" + features_by_level = defaultdict(list) + + +# SCAG +class Bladeslinging(SubClass): + """**Restriction: Elves Only** + + Bladesingers are elves who bravely defend their people and lands. They are + elf wizards who master a school of sword fighting grounded in a tradition + of arcane magic. In combat, a bladesinger uses a series of intricate, + elegant maneuvers that fend off harm and allow the bladesinger to channel + magic into devastating attacks and a cunning defense + + """ + name = "School of Bladeslinging" + features_by_level = defaultdict(list) + + +# XGTE +class WarMagic(SubClass): + """A variety of arcane colleges specialize in training wiz— ards for war. The + tradition of War Magic blends principles of evocation and abjuration, + rather than specializing in either of those schools. It teaches + techniques that empower a caster’s spells, while also providing methods for + wizards to bolster their own defenses. + + Followers of this tradition are known as war mages. They see their magic + as both a weapon and armor, a resource superior to any piece of steel. War + mages act fast in battle, using their spells to seize tactical control of a + situation. Their spells strike hard, while their defensive skills foil + their opponents“ attempts to counterattack. War mages are also adept at + turning other spellcasters’ magical energy against them. + + In great battles, a war mage often works with evokers, abjurers, and other + types of wizards. Evokers, in particular, sometimes tease war mages for + splitting their attention between offense and defense. A war mage’s typical + response: “What good is being able to throw a mighty fireball if I die + before I can cast it? + + """ + name = "School of War Magic" + features_by_level = defaultdict(list) class Wizard(CharClass): @@ -12,8 +165,14 @@ class Wizard(CharClass): weapon_proficiencies = (weapons.Dagger, weapons.Dart, weapons.Sling, weapons.Quarterstaff, weapons.LightCrossbow) + multiclass_weapon_proficiencies = () + _multiclass_proficiencies_text = () 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..06a363a 100755 --- a/dungeonsheets/create_character.py +++ b/dungeonsheets/create_character.py @@ -38,11 +38,11 @@ class App(npyscreen.NPSAppManaged): def save_character(self): # Save the file filename = self.getForm("SAVE").filename.value - self.character.save(filename, template_file='empty_template.tex') + self.character.save(filename, template_file='empty_template.txt') # Create the PDF character sheet if self.getForm('SAVE').make_pdf.value: log.debug("Creating PDF") - self.character.to_pdf(filename, template_file='empty_template.tex') + self.character.to_pdf(filename, template_file='empty_template.txt') subprocess.call(['makesheets', filename]) def update_max_hp(self): @@ -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/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/backgrounds.py b/dungeonsheets/features/backgrounds.py index 0a6164e..0400ad7 100644 --- a/dungeonsheets/features/backgrounds.py +++ b/dungeonsheets/features/backgrounds.py @@ -193,7 +193,7 @@ class CitySecrets(Feature): # Swords Coast Adventurer's Guide class AllEyesOnYou(Feature): - """Your accent, mannerisms, figures of speech, and per- haps even your + """Your accent, mannerisms, figures of speech, and perhaps even your appearance all mark you as foreign. Curious glances are directed your way wherever you go, which can be a nuisance, but you also gain the friendly interest of scholars and others intrigued by far-off lands, to say nothing @@ -224,7 +224,7 @@ class EarToTheGround(Feature): class WatchersEye(Feature): """Your experience in enforcing the law, and dealing with lawbreakers, gives - you a feel for local laws and crimi- nals. You can easily find the local + you a feel for local laws and criminals. You can easily find the local outpost of the watch or a simila r organization, and just as easily pick out the dens of criminal activity in a community, although you're more likely to be welcome in the former locations rather than the latter. @@ -237,9 +237,9 @@ class WatchersEye(Feature): class RespectOfTheStoutFolk(Feature): """As well respected as clan crafters are among outsiders, no one esteems them quite so highly as dwarves do. You always have free room and board in any - place where shield dwarves or gold dwarves dwell, and the individu- als in + place where shield dwarves or gold dwarves dwell, and the individuals in such a settlement might vie among themselves to determine who can offer you - (and possibly your compa- triots) the finest accommodations and assistance. + (and possibly your compatriots) the finest accommodations and assistance. """ name = "Respect of the Stout Folk" @@ -257,7 +257,7 @@ class LibraryAccess(Feature): and you know how to navigate those connections with some ease. Additionally, you are likely to gain preferential treatment at other - libraries across the Realms, as profes- sional courtesy shown to a fellow + libraries across the Realms, as professional courtesy shown to a fellow scholar. """ @@ -267,7 +267,7 @@ class LibraryAccess(Feature): class CourtFunctionary(Feature): """Your knowledge of how bureaucracies function lets you gain access to the - records and inner workings of any no- ble court or government you + records and inner workings of any noble court or government you encounter. You know who the movers and shakers are, whom to go to for the favors you seek, and what the current intrigues of interest in the group are. @@ -279,10 +279,10 @@ class CourtFunctionary(Feature): class SafeHaven(Feature): """As a faction agent, you have access to a secret network of supporters and - operatives who can provide assis- tance on your adventures. You know a set + operatives who can provide assistance on your adventures. You know a set of secret signs and passwords you can use to identify such operatives , who can provide you with access to a hidden safe house, free room and board, or - assistance in finding informa- tion. These agents never risk their lives + assistance in finding information. These agents never risk their lives for you or risk revealing their true identities. """ @@ -321,7 +321,7 @@ class KnightlyRegard(Feature): """You receive shelter and succor from members of your knightly order and those who are sympathetic to its aims. If your order is a religious one, you can gain aid from temples and other religious communities of your - deity. Knights of civic orders can get help from the com- munity- whether a + deity. Knights of civic orders can get help from the community- whether a lone settlement or a great nation- that they serve, and knights of philosophical orders can find help from those they have aided in pursuit of their ideals , and those who share those ideals. @@ -329,7 +329,7 @@ class KnightlyRegard(Feature): This help comes in the form of shelter and meals, and healing when appropriate, as well as occasionally risky assistance, such as a band of local citizens rallying to aid a sorely pressed knight in a fight , or - those who sup- port the order helping to smuggle a knight out of town when + those who support the order helping to smuggle a knight out of town when he or she is being hunted unjustly. """ @@ -343,7 +343,7 @@ class MercenaryLife(Feature): little about any such company, including the names and reputations of its commanders and leaders, and who has hired them recently. You can find the taverns and festhalls where mercenaries abide in any area, as long as you - speak the language. You can find mercenary work between adven- tures + speak the language. You can find mercenary work between adventures sufficient to maintain a comfortable lifestyle (see "Practicing a Profession" under "Downtime Activities" in chapter 8 of the Player's Handbook). @@ -376,7 +376,7 @@ class KeptInStyle(Feature): cord your debt and send an accounting to your family's estate in Waterdeep to settle what you owe. - This advantage enables you to live a comfortable life- style without having + This advantage enables you to live a comfortable lifestyle without having to pay 2 gp a day for it, or reduces the cost of a wealthy or aristocratic lifestyle by that amount. You may not maintain a less affluent lifestyle and use the difference as income-the benefit is a line of credit, not an diff --git a/dungeonsheets/features/feats.py b/dungeonsheets/features/feats.py new file mode 100644 index 0000000..e69de29 diff --git a/dungeonsheets/features/features.py b/dungeonsheets/features/features.py index db2f152..c3bc5eb 100644 --- a/dungeonsheets/features/features.py +++ b/dungeonsheets/features/features.py @@ -83,3 +83,19 @@ class Feature(): return -100 +class FeatureSelector(Feature): + """ + A feature with multiple possible choices. + """ + options = dict() + + def __init__(self, feature_choices=[]): + keep_source = self.source + # Look for matching feature_choices + for selection in feature_choices: + if selection.lower() in self.options(): + new_feat = self.options[selection.lower()] + self.__dict__.update(new_feat.__dict__) + new_feat.__init__(self) + break + self.source = keep_source diff --git a/dungeonsheets/features/races.py b/dungeonsheets/features/races.py index 888d567..8e13164 100644 --- a/dungeonsheets/features/races.py +++ b/dungeonsheets/features/races.py @@ -395,7 +395,7 @@ class RadiantSoul(Feature): Your transformation lasts for 1 minute or until you end it as a bonus action. During it, you have a flying speed of 30 feet, and once on each of - your turns, you can deal ex- tra radiant damage to one target when you deal + your turns, you can deal extra radiant damage to one target when you deal damage to it with an attack or a spell. The extra radiant damage equals your level. @@ -409,13 +409,13 @@ class RadiantSoul(Feature): class RadiantConsumption(Feature): """Starting at 3rd level, you can use your action to unleash the divine energy - within your- self, causing a searing light to radiate from you, pour out of + within yourself, causing a searing light to radiate from you, pour out of your eyes and mouth, and threaten to char you. Your transformation lasts for 1 minute or until you end ii as a bonus action. During it, you shed bright light in a 10-foot radius and dim light for an additional 10 feet,and at the end of each of your turns, you and - each crea- ture within 10 feet of you take radiant damage equal to half + each creature within 10 feet of you take radiant damage equal to half your level (rounded up). In addition, once on each of your turns, you can deal extra radiant damage to one target when you deal damage to it with an attack or a spell. The extra radiant damage equals your level. @@ -489,7 +489,7 @@ class SpeechOfBeastAndLeaf(Feature): # Goliath class StonesEndurance(Feature): - """You can focus yourself to occa- sionally shrug off injury. When you take + """You can focus yourself to occasionally shrug off injury. When you take damage, you can use your reaction to roll a dl2. Add your Constitution modifier to the number rolled, and reduce the damage by that total. After you use this trait, you can't use it again until you finish a short or long @@ -614,7 +614,7 @@ class ControlAirAndWater(Feature): class EmissaryOfTheSea(Feature): - """Aquatic beasts have an extraor- dinary affinity with your people. You can + """Aquatic beasts have an extraordinary affinity with your people. You can communicate simple ideas with beasts that can breathe water. They can understand the meaning of your words, though you have no special ability to understand them in return. diff --git a/dungeonsheets/features/ranger.py b/dungeonsheets/features/ranger.py index 6f7c4ff..8918d3b 100644 --- a/dungeonsheets/features/ranger.py +++ b/dungeonsheets/features/ranger.py @@ -1,4 +1,4 @@ -from .features import Feature +from .features import Feature, FeatureSelector from .. import (weapons, armor) @@ -14,24 +14,8 @@ def select_ranger_fighting_style(feature_choices=[]): return TwoWeaponFighting() else: return RangerFightingStyle() - -class RangerFightingStyle(Feature): - """ - Select a Fighting Style by choosing in feature_choices: - archery - - defense - - dueling - - two-weapon fighting - """ - name = "Fighting Style (Select One)" - source = "Ranger" - - class Archery(Feature): """ You gain a +2 bonus to attack rolls you make @@ -84,3 +68,25 @@ class TwoWeaponFighting(Feature): name = "Fighting Style (Two-Weapon Fighting)" source = "Ranger" needs_implementation = True + + +class RangerFightingStyle(FeatureSelector): + """ + Select a Fighting Style by choosing in feature_choices: + + archery + + defense + + dueling + + two-weapon fighting + """ + options = {'archery': Archery, + 'defense': Defense, + 'dueling': Dueling, + 'two-weapon fighting': TwoWeaponFighting, + 'two-weapon': TwoWeaponFighting, + 'dual wield': TwoWeaponFighting} + name = "Fighting Style (Select One)" + source = "Ranger" diff --git a/dungeonsheets/features/warlock.py b/dungeonsheets/features/warlock.py index e69de29..3f39b3b 100644 --- a/dungeonsheets/features/warlock.py +++ b/dungeonsheets/features/warlock.py @@ -0,0 +1,335 @@ +from .features import Feature +from .. import spells + + +# 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): + for s in self.at_will_spells: + self.cast_spell_at_will(s) + + +# PHB +class AgonizingBlast(Invocation): + """When you cast eldritch blast, add your Charisma modifier to the damage it + deals on a hit. + + """ + name = "Agonizing Blast" + needs_implementation = True + + +class ArmorOfShadows(Invocation): + """You can cast mage armor on yourself at will, without expending a spell slot + or material components + + """ + name = "Armor of Shadows" + at_will_spells = (spells.MageArmor,) + + +class AscendantStep(Invocation): + """You can cast levitate on yourself at will, without expending a spell slot + or material components. + + **Prerequisite: 9th level** + + """ + name = 'Ascendant Step' + at_will_spells = (spells.Levitate,) + + +class BeastSpeech(Invocation): + """You can cast speak with animals at will, without expending a spell slot. + + """ + name = "Beast Speech" + at_will_spells = (spells.SpeakWithAnimals,) + + +class BeguilingInfluence(Invocation): + """You gain proficiency in the Deception and Persuasion skills. + + """ + name = "Beguiling Influence" + needs_implementation = True + + +class BewitchingWhispers(Invocation): + """You can cast compulsion once using a warlock spell slot. You can’t do so + again until you finish a long rest + + **Prerequisite**: 7th Level + """ + name = "Bewitching Whispers" + + +class BookOfAncientSecrets(Invocation): + """You can now inscribe magical rituals in your Book of Shadows. Choose two + 1st-level spells that have the ritual tag from any class’s spell list. The + spells appear in the book and don’t count against the number of spells you + know. With your Book of Shadows in hand, you can cast the chosen spells as + rituals. You can’t cast the spells except as rituals, unless you’ve learned + them by some other means. You can also cast a warlock spell you know as a + ritual if it has the ritual tag. + + On your adventures, you can add other ritual spells to your Book o f + Shadows. When you find such a spell, you can add it to the book if the + spell’s level is equal to or less than half your warlock level (rounded up) + and if you can spare the time to transcribe the spell. For each level of + the spell, the transcription process takes 2 hours and costs 50 gp for the + rare inks needed to inscribe it + + """ + name = "Book of Ancient Secrets" + + +class ChainsOfCarceri(Invocation): + """You can cast hold monster at will—targeting a celestial, fiend, or + elemental—without expending a spell slot or material components. You must + finish a long rest before you can use this invocation on the same creature + again. + + **Prerequisites**: 15th level, Pact of the Chain Feature + """ + name = "Chains of Carceri" + + +class DevilsSight(Invocation): + """You can see normally in darkness, both magical and nonmagical, to a + distance of 120 feet. + + """ + name = "Devil's Sight" + + +class DreadfulWord(Invocation): + """You can cast confusion once using a warlock spell slot. You can’t do so + again until you finish a long rest. + + """ + name = "Dreadful Word" + + +class EldritchSight(Invocation): + """You can cast detect magic at will, without expending a spell slot. + + """ + name = "Eldritch Sight" + at_will_spells = (spells.DetectMagic,) + + +class EldritchSpear(Invocation): + """When you cast eldritch blast, its range is 300 feet. + + """ + name = "Eldritch Spear" + needs_implementation = True + + +class EyesOfTheRuneKeeper(Invocation): + """ + You can read all writing. + + """ + name = "Eyes of the Rune Keeper" + + +class FiendishVigor(Invocation): + """You can cast false life on yourself at will as a 1st-level spell, without + expending a spell slot or material components. + + """ + name = "Fiendish Vigor" + at_will_spells = (spells.FalseLife,) + + +class GazeOfTwoMinds(Invocation): + """You can use your action to touch a willing humanoid and perceive through + its senses until the end of your next turn. As long as the creature is on + the same plane of existence as you, you can use your action on subsequent + turns to maintain this connection, extending the duration until the end of + your next turn. While perceiving through the other creature’s senses, you + benefit from any special senses possessed by that creature, and you are + blinded and deafened to your own surroundings. + + """ + name = "Gaze of Two Minds" + + +class LifeDrinker(Invocation): + """When you hit a creature with your pact weapon, the creature takes extra + necrotic damage equal to your Charisma modifier (minimum 1). + + **Prerequisite**: 12th Level, Pact of the Blade + """ + name = "Life Drinker" + needs_implementation = True + + +class MaskOfManyFaces(Invocation): + """You can cast disguise self at will, without expending a spell slot. + + """ + name = "Mask of Many Faces" + at_will_spells = (spells.DisguiseSelf,) + + +class MasterOfMyriadForms(Invocation): + """ + You can cast alter self at will, without expending a spell slot. + + **Prerequisite**: 15th Level + """ + name = "Master of Myriad Forms" + at_will_spells = (spells.AlterSelf,) + + +class MinionsOfChaos(Invocation): + """You can cast conjure elemental once using a warlock spell slot. You can’t + do so again until you finish a long rest. + + **Prerequisite**: 9th Level + """ + name = "Minions of Chaos" + + +class MireTheMind(Invocation): + """You can cast slow once using a warlock spell slot. You can’t do so again + until you finish a long rest. + + """ + name = "Mire the Mind" + + +class MistyVisions(Invocation): + """You can cast silent image at will, without expending a spell slot or + material components. + + """ + name = "Misty Visions" + at_will_spells = (spells.SilentImage,) + + +class OneWithShadows(Invocation): + """When you are in an area of dim light or darkness, you can use your action + to become invisible until you move or take an action or a reaction. + + **Prerequisite**: 5th Level + """ + name = "One with Shadows" + + +class OtherworldlyLeap(Invocation): + """You can cast jump on yourself at will, without expending a spell slot or + material components. + + **Prerequisite**: 9th Level + + """ + name = "Otherworldly Leap" + at_will_spells = (spells.Jump,) + + +class RepellingBlast(Invocation): + """When you hit a creature with eldritch blast, you can push the creature up +to 10 feet away from you in a straight line. + + """ + name = "Repelling Blast" + + +class SculptorOfFlesh(Invocation): + """You can cast polymorph once using a warlock spell slot. You can’t do so + again until you finish a long rest. + + **Prerequisite**: 7th Level + """ + name = "Sculptor of Flesh" + + +class SignOfIllOmen(Invocation): + """You can cast bestow curse once using a warlock spell slot. You can’t do so + again until you finish a long rest. + + **Prerequisite**: 5th Level + + """ + name = "Sign of Ill Omen" + + +class ThiefOfFiveFates(Invocation): + """You can cast bane once using a warlock spell slot. You can’t do so again + until you finish a long rest. + + """ + name = "Thief of Five Fates" + needs_implementation = True + + +class ThirstingBlade(Invocation): + """You can attack with your pact weapon twice, instead of once, whenever you + take the Attack action on your turn. + + **Prerequisite**: 5th Level, Pact of the Blade + """ + name = "Thirsting Blade" + + +class VisionsOfDistantRealms(Invocation): + """ + You can cast arcane eye at will, without expending a spell slot + + **Prerequisite**: 15th level + """ + name = "Visions of Distant Realms" + at_will_spells = (spells.ArcaneEye,) + + +class VoiceOfTheChain(Invocation): + """You can communicate telepathically with your familiar and perceive through + your familiar’s senses as long as you are on the same plane of + existence. Additionally, while perceiving through your familiar’s senses, + you can also speak through your familiar in your own voice, even if your + familiar is normally incapable of speech. + + **Prerequisite**: Pact of the Chain + + """ + name = "Voice of the Chain" + + +class WhispersOfTheGrave(Invocation): + """You can cast speak with dead at will, without expending a spell slot + + **Prerequsite**: 9th Level + + """ + name = "Whispers of the Grave" + at_will_spells = (spells.SpeakWithDead,) + + +class WitchSight(Invocation): + """You can see the true form of any shapechanger or creature concealed by + illusion or transmutation magic while the creature is within 30 feet of you + and within line of sight. + + """ + name = "Witch Sight" diff --git a/dungeonsheets/blank-character-sheet-default.pdf b/dungeonsheets/forms/blank-character-sheet-default.pdf similarity index 100% rename from dungeonsheets/blank-character-sheet-default.pdf rename to dungeonsheets/forms/blank-character-sheet-default.pdf diff --git a/dungeonsheets/blank-spell-sheet-default.pdf b/dungeonsheets/forms/blank-spell-sheet-default.pdf similarity index 100% rename from dungeonsheets/blank-spell-sheet-default.pdf rename to dungeonsheets/forms/blank-spell-sheet-default.pdf diff --git a/dungeonsheets/character_template.txt b/dungeonsheets/forms/character_template.txt similarity index 95% rename from dungeonsheets/character_template.txt rename to dungeonsheets/forms/character_template.txt index bb31fc8..1d287f0 100644 --- a/dungeonsheets/character_template.txt +++ b/dungeonsheets/forms/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 }}" @@ -28,7 +29,7 @@ charisma = {{ char.charisma.value }} skill_proficiencies = {{ char.skill_proficiencies }} # Named features / feats that aren't part of your classes, -# race, or background. +# race, or background. Also include Eldritch Invocations. # Example: # features = ('Tavern Brawler',) # take the optional Feat from PHB features = {{ char.custom_features }} diff --git a/dungeonsheets/druid_shapes_template.tex b/dungeonsheets/forms/druid_shapes_template.tex similarity index 100% rename from dungeonsheets/druid_shapes_template.tex rename to dungeonsheets/forms/druid_shapes_template.tex diff --git a/dungeonsheets/empty_template.tex b/dungeonsheets/forms/empty_template.txt similarity index 98% rename from dungeonsheets/empty_template.tex rename to dungeonsheets/forms/empty_template.txt index 71ed65f..3e865ba 100644 --- a/dungeonsheets/empty_template.tex +++ b/dungeonsheets/forms/empty_template.txt @@ -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_template.tex b/dungeonsheets/forms/features_template.tex similarity index 75% rename from dungeonsheets/features_template.tex rename to dungeonsheets/forms/features_template.tex index 7b65675..5869438 100644 --- a/dungeonsheets/features_template.tex +++ b/dungeonsheets/forms/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/spellbook_template.tex b/dungeonsheets/forms/spellbook_template.tex similarity index 97% rename from dungeonsheets/spellbook_template.tex rename to dungeonsheets/forms/spellbook_template.tex index 7e2db7a..2a01e2d 100644 --- a/dungeonsheets/spellbook_template.tex +++ b/dungeonsheets/forms/spellbook_template.tex @@ -5,7 +5,7 @@ \usepackage[dvipsnames]{color} \definecolor{mygrey}{gray}{0.7} -\title{Spells and Incantations} +\title{Spell Descriptions} \author{[[ character.name ]]} \date{} diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index 6860b7f..fb2246c 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -45,7 +45,7 @@ def rst_to_latex(rst): jinja_env = Environment( - loader=PackageLoader('dungeonsheets', ''), + loader=PackageLoader('dungeonsheets', 'forms'), block_start_string='[%', block_end_string='%]', variable_start_string='[[', @@ -183,7 +183,7 @@ def create_spells_pdf(char, basename, flatten=False): # for field in field_names: # fields.append((field, field)) # Make the actual pdf - dirname = os.path.dirname(os.path.abspath(__file__)) + dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'forms/') src_pdf = os.path.join(dirname, 'blank-spell-sheet-default.pdf') make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) @@ -318,7 +318,7 @@ def create_character_pdf(char, basename, flatten=False): 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__)) + dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'forms/') src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf') return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) 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/dungeonsheets/spells/spells_j_m.py b/dungeonsheets/spells/spells_j_m.py index ac541d9..9ca0eed 100644 --- a/dungeonsheets/spells/spells_j_m.py +++ b/dungeonsheets/spells/spells_j_m.py @@ -1,6 +1,22 @@ from .spells import Spell +class Jump(Spell): + """You touch a creature. The creature’s jump distance is tripled until the + spell ends. + + """ + name = "Jump" + level = 1 + casting_time = '1 action' + casting_range = "Touch" + components = ("V", "S", "M") + materials = "A grasshoppers hind leg" + duration = "1 minute" + magic_school = "Transmutation" + classes = ("Druid", 'Ranger', 'Sorceror', 'Wizard') + + class Knock(Spell): """Choose an object that you can see within range. The object can be a door, a box, a chest, a set of manacles, a padlock, or another diff --git a/examples/druid.pdf b/examples/druid.pdf index 6065972..829ba2e 100644 Binary files a/examples/druid.pdf and b/examples/druid.pdf differ diff --git a/examples/rogue.pdf b/examples/rogue.pdf index 203c88d..1cdda69 100644 Binary files a/examples/rogue.pdf and b/examples/rogue.pdf differ diff --git a/examples/warlock.pdf b/examples/warlock.pdf index 09bd2a5..dd762a8 100644 Binary files a/examples/warlock.pdf and b/examples/warlock.pdf differ diff --git a/examples/wizard.pdf b/examples/wizard.pdf index 9098e2b..8042eb7 100644 Binary files a/examples/wizard.pdf and b/examples/wizard.pdf differ diff --git a/setup.py b/setup.py index 3acbcfa..55fe5c1 100644 --- a/setup.py +++ b/setup.py @@ -20,11 +20,8 @@ setup(name='dungeonsheets', download_url = 'https://github.com/canismarko/dungeon-sheets/archive/master.zip', packages=find_packages(), package_data={ - 'dungeonsheets': ['blank-character-sheet-default.pdf', - 'blank-spell-sheet-default.pdf', - 'spellbook_template.tex', '../VERSION', - 'character_template.txt', 'features_template.tex', - 'druid_shapes_template.tex'] + 'dungeonsheets': ['forms/*pdf', 'forms/*.tex', 'forms/*.txt', + '../VERSION'] }, install_requires=[ 'fdfgen', 'npyscreen', 'jinja2', 'pdfrw',