diff --git a/dungeonsheets/__init__.py b/dungeonsheets/__init__.py index 5195747..ba96660 100644 --- a/dungeonsheets/__init__.py +++ b/dungeonsheets/__init__.py @@ -1,8 +1,14 @@ -from . import weapons, character, features, race, background, spells +__all__ = ('__version__', 'Character', 'weapons', 'features', + 'character', 'race', 'background', 'spells') + +from . import weapons, features, race, background, spells +from .character import Character import os + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + __version__ = read('../VERSION') diff --git a/dungeonsheets/background.py b/dungeonsheets/background.py index 3786658..78c546b 100644 --- a/dungeonsheets/background.py +++ b/dungeonsheets/background.py @@ -11,6 +11,9 @@ class Background(): features = () languages = () + def __init__(self): + self.features = tuple([f() for f in self.features]) + def __str__(self): return self.name @@ -19,17 +22,20 @@ class Acolyte(Background): name = "Acolyte" skill_proficiencies = ('insight', 'religion') languages = ("[choose one]", "[choose one]") + features = (feats.ShelterOfTheFaithful,) class Charlatan(Background): name = "Charlatan" skill_proficiencies = ('deception', 'sleight of hand') + features = (feats.FalseIdentity,) class Criminal(Background): name = "Criminal" skill_proficiencies = ('deception', 'stealth') - + features = (feats.CriminalContact,) + class Spy(Criminal): name = "Spy" @@ -38,21 +44,24 @@ class Spy(Criminal): class Entertainer(Background): name = "Entertainer" skill_proficiencies = ('acrobatics', 'performance') + features = (feats.ByPopularDemand,) class Gladiator(Entertainer): name = "Gladiator" - + class FolkHero(Background): name = "Folk Hero" skill_proficiencies = ('animal handling', 'survival') + features = (feats.RusticHospitality,) class GuildArtisan(Background): name = "Guild Artisan" skill_proficiencies = ('insight', 'persuasion') languages = ("[choose one]", "[choose one]") + features = (feats.GuildMembership,) class GuildMerchant(GuildArtisan): @@ -63,12 +72,14 @@ class Hermit(Background): name = "Hermit" skill_proficiencies = ("medicine", "religion") languages = ("[choose one]", ) + features = (feats.Discovery,) class Noble(Background): name = "Noble" skill_proficiencies = ("history", 'persuasion') languages = ("[choose one]", ) + features = (feats.PositionOfPrivilege,) class Knight(Noble): @@ -79,17 +90,20 @@ class Outlander(Background): name = "Outlander" skill_proficiencies = ('athletics', 'survival') languages = ("[choose one]", ) - + features = (feats.Wanderer,) + class Sage(Background): name = "Sage" skill_proficiencies = ('arcana', 'history') languages = ("[choose one]", '[choose one]') + features = (feats.Researcher,) class Sailor(Background): name = "Sailor" skill_proficiencies = ('athletics', 'perception') + features = (feats.ShipsPassage,) class Pirate(Sailor): @@ -99,29 +113,122 @@ class Pirate(Sailor): class Soldier(Background): name = "Soldier" skill_proficiencies = ('athletics', 'intimidation') + features = (feats.MilitaryRank,) class Urchin(Background): name = "Urchin" skill_proficiencies = ('sleight of hand', 'stealth') + features = (feats.CitySecrets,) + +# Sword's Coast Adventurers Guide +class CityWatch(Background): + name = "City Watch" + skill_proficiencies = ('athletics', 'insight') + languages = ('[choose one]', '[choose one]') + features = (feats.WatchersEye,) + + +class ClanCrafter(Background): + name = "Clan Crafter" + skill_proficiencies = ('history', 'insight') + languages = ('Dwarvish') + features = (feats.RespectOfTheStoutFolk,) + + +class CloisteredScholar(Background): + name = "Cloistered Scholar" + skill_proficiencies = ('history',) + skill_choices = ('arcana', 'nature', 'religion') + num_skill_choices = 1 + languages = ('[choose one]', '[choose one]') + features = (feats.LibraryAccess,) + + +class Courtier(Background): + name = "Courtier" + skill_proficiencies = ("insight", 'persuasion') + languages = ('[choose one]', '[choose one]') + features = (feats.CourtFunctionary,) + + +class FactionAgent(Background): + name = "Faction Agent" + skill_proficiencies = ('insight',) + skill_choices = ('animal handling', 'arcana', 'deception', + 'history', 'intimidation', 'investigation', + 'medicine', 'nature', 'perception', 'performance', + 'persuasion', 'religion', 'survival') + num_skill_choices = 1 + languages = ('[choose one]', '[choose one]') + features = (feats.SafeHaven,) + + +class FarTraveler(Background): + name = 'Far Traveler' + skill_proficiencies = ('insight', 'perception') + languages = ('[choose one]',) + features = (feats.AllEyesOnYou,) + + +class Inheritor(Background): + name = "Inheritor" + skill_proficiencies = ('survival',) + skill_choices = ('arcana', 'history', 'religion') + num_skill_choices = 1 + languages = ('[choose one]',) + features = (feats.Inheritance,) + + +class KnightOfTheOrder(Background): + name = "Knight of the Order" + skill_proficiencies = ('persuasion',) + skill_choices = ('arcana', 'history', 'nature', 'religion') + num_skill_choices = 1 + languages = ('[choose one]') + features = (feats.KnightlyRegard,) + + +class MercenaryVeteran(Background): + name = "Mercenary Veteran" + skill_proficiencies = ('athletics', 'persuasion') + features = (feats.MercenaryLife,) + class UrbanBountyHunter(Background): name = 'Urban Bounty Hunter' skill_proficiencies = () skill_choices = ('Deception', 'Insight', 'Persuasion', 'Stealth') num_skill_choices = 2 + features = (feats.EarToTheGround,) + +class UthgardtTribeMember(Background): + name = "Uthgardt Tribe Member" + skill_profifiencies = ('athletics', 'survival') + languages = ('[choose one]') + features = (feats.UthgardtHeritage,) + + +class WaterdhavianNoble(Background): + name = "Waterdhavian Noble" + skill_proficiencies = ('history', 'persuasion') + languages = ('[choose one]') + features = (feats.KeptInStyle,) -class FarTraveler(Background): - name = 'Far Traveler' - skill_proficiencies = ('insight', 'perception') - languages = ('[choose one]',) + +PHB_backgrounds = [Acolyte, Charlatan, Criminal, Spy, Entertainer, + Gladiator, FolkHero, GuildArtisan, GuildMerchant, + Hermit, Noble, Knight, Outlander, Sage, Sailor, + Pirate, Soldier, Urchin] +SCAG_backgrounds = [CityWatch, ClanCrafter, CloisteredScholar, Courtier, + FactionAgent, FarTraveler, Inheritor, KnightOfTheOrder, + MercenaryVeteran, UrbanBountyHunter, UthgardtTribeMember, + WaterdhavianNoble] -available_backgrounds = [Acolyte, Charlatan, Criminal, Spy, Entertainer, - Gladiator, FolkHero, GuildArtisan, GuildMerchant, - Hermit, Noble, Knight, Outlander, Sage, Sailor, - Pirate, Soldier, Urchin, UrbanBountyHunter, - FarTraveler] +available_backgrounds = PHB_backgrounds + SCAG_backgrounds +__all__ = tuple([b.name for b in available_backgrounds]) + ( + 'PHB_backgrounds', 'SCAG_backgrounds', 'available_backgrounds') diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index d72d9ea..2d99584 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -1,4 +1,5 @@ """Tools for describing a player character.""" +__all__ = ('Character',) import re import os @@ -12,10 +13,17 @@ from .stats import Ability, Skill, findattr from .dice import read_dice_str from . import (weapons, race, background, spells, armor, monsters, exceptions, classes, features) -from .__init__ import __version__ from .weapons import Weapon from .armor import Armor, NoArmor, Shield, NoShield + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +__version__ = read('../VERSION') + + dice_re = re.compile('(\d+)d(\d+)') multiclass_spellslots_by_level = { @@ -112,8 +120,8 @@ class Character(): _proficiencies_text = tuple() # Magic spellcasting_ability = None - spells = tuple() - spells_prepared = tuple() + _spells = tuple() + _spells_prepared = tuple() # Features IN MAJOR DEVELOPMENT custom_features = () feature_choices = () @@ -132,7 +140,7 @@ class Character(): # 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(),) + self._spells += (S(),) def __str__(self): return self.name @@ -249,6 +257,28 @@ class Character(): else: return multiclass_spellslots_by_level[eff_level][spell_level] + @property + def spells(self): + spells = set(self._spells) + for f in self.features: + spells |= set(f.spells_known) | set(f.spells_prepared) + for c in self.spellcasting_classes: + spells |= set(c.spells_known) | set(c.spells_prepared) + if self.race is not None: + spells |= set(self.race.spells_known) | set(self.race.spells_prepared) + return tuple(spells) + + @property + def spells_prepared(self): + spells = set(self._spells_prepared) + for f in self.features: + spells |= set(f.spells_prepared) + for c in self.spellcasting_classes: + spells |= set(c.spells_prepared) + if self.race is not None: + spells |= set(self.race.spells_prepared) + return tuple(spells) + def set_attrs(self, **attrs): """Bulk setting of attributes. Useful for loading a character from a dictionary.""" @@ -322,10 +352,10 @@ class Character(): # Save list of spells to character atribute if attr == 'spells': # Instantiate them all for the spells list - self.spells = tuple(S() for S in _spells) + self._spells = tuple(S() for S in _spells) else: # Instantiate them all for the spells list - self.spells_prepared = tuple(S() for S in _spells) + self._spells_prepared = tuple(S() for S in _spells) else: if not hasattr(self, attr): warnings.warn(f"Setting unknown character attribute {attr}", @@ -571,11 +601,12 @@ class Character(): f.write(text) def to_pdf(self, filename, **kwargs): - char_file = filename.replace('pdf', 'py') - self.save(char_file, + if filename.endswith('.pdf'): + filename = filename.replace('pdf', 'py') + self.save(filename, template_file=kwargs.get('template_file', 'character_template.txt')) - subprocess.call(['makesheets', char_file]) + subprocess.call(['makesheets', filename) def read_character_file(filename): diff --git a/dungeonsheets/classes/__init__.py b/dungeonsheets/classes/__init__.py index 28e8f67..487c000 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') + 'Wizard', 'Revisedranger', 'available_classes') from .classes import CharClass from .barbarian import Barbarian @@ -15,3 +15,6 @@ 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] diff --git a/dungeonsheets/classes/classes.py b/dungeonsheets/classes/classes.py index 6d48b11..fb1de10 100644 --- a/dungeonsheets/classes/classes.py +++ b/dungeonsheets/classes/classes.py @@ -17,6 +17,8 @@ class CharClass(): num_skill_choices = 2 spellcasting_ability = None spell_slots_by_level = None + spells_known = () + spells_prepared = () subclass = None subclasses_available = () features_by_level = defaultdict(list) diff --git a/dungeonsheets/create_character.py b/dungeonsheets/create_character.py index 431410e..5b28800 100755 --- a/dungeonsheets/create_character.py +++ b/dungeonsheets/create_character.py @@ -23,26 +23,11 @@ def read_version(): return version -char_classes = { - 'Barbarian': classes.Barbarian, - 'Bard': classes.Bard, - 'Cleric': classes.Cleric, - 'Druid': classes.Druid, - 'Fighter': classes.Fighter, - 'Monk': classes.Monk, - 'Paladin': classes.Paladin, - 'Ranger': classes.Ranger, - 'Rogue': classes.Rogue, - 'Sorceror': classes.Sorceror, - 'Warlock': classes.Warlock, - 'Wizard': classes.Wizard -} +char_classes = {c.class_name: c for c in classes.available_classes} -races = race.race_dict +races = {r.name: r for r in race.available_races} - -backgrounds = background.available_backgrounds -backgrounds = {bg.name: bg for bg in backgrounds} +backgrounds = {b.name: b for b in background.available_backgrounds} class App(npyscreen.NPSAppManaged): @@ -53,10 +38,11 @@ class App(npyscreen.NPSAppManaged): def save_character(self): # Save the file filename = self.getForm("SAVE").filename.value - self.character.save(filename) + self.character.save(filename, template_file='empty_template.tex') # 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') subprocess.call(['makesheets', filename]) def update_max_hp(self): diff --git a/dungeonsheets/empty_template.tex b/dungeonsheets/empty_template.tex new file mode 100644 index 0000000..71ed65f --- /dev/null +++ b/dungeonsheets/empty_template.tex @@ -0,0 +1,90 @@ +"""This file describes the heroic adventurer {{ char.name }}. + +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 = "{{ char.dungeonsheets_version }}" + +name = "{{ char.name }}" +classes_levels = {{ char.classes_levels }} +player_name = "{{ char.player_name }}" +background = "{{ char.background.name }}" +race = "{{ char.race.name }}" +alignment = "{{ char.alignment }}" +xp = {{ char.xp }} +hp_max = {{ char.hp_max }} + +# Ability Scores +strength = {{ char.strength.value }} +dexterity = {{ char.dexterity.value }} +constitution = {{ char.constitution.value }} +intelligence = {{ char.intelligence.value }} +wisdom = {{ char.wisdom.value }} +charisma = {{ char.charisma.value }} + +# Select what skills you're proficient with +skill_proficiencies = {{ char.skill_proficiencies }} + +# 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 = """{{ char.languages }}""" + +# 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/features/backgrounds.py b/dungeonsheets/features/backgrounds.py index e69de29..0a6164e 100644 --- a/dungeonsheets/features/backgrounds.py +++ b/dungeonsheets/features/backgrounds.py @@ -0,0 +1,387 @@ +from .features import Feature + + +class ShelterOfTheFaithful(Feature): + """As an acolyte, you command the respect of those who share your faith, and + you can perform the religious ceremonies of your deity. You and your + adventuring companions can expect to receive free healing and care at a + temple, shrine, or other established presence of your faith, though you + must provide any material components needed for spells. Those who share + your religion will support you (but only you) at a modest lifestyle. + + You might also have ties to a specific temple dedicated to your chosen + deity or pantheon, and you have a residence there. This could be the temple + where you used to serve, if you remain on good terms with it, or a temple + where you have found a new home. While near your temple, you can call upon + the priests for assistance, provided the assistance you ask for is not + hazardous and you remain in good standing with your temple. + + """ + name = "Shelter of the Faithful" + source = "Background (Acolyte)" + + +class FalseIdentity(Feature): + """You have created a second identity that includes documentation, established + acquaintances, and disguises that allow you to assume that + persona. Additionally, you can forge documents including official papers + and personal letters, as long as you have seen an example of the kind of + document or the handwriting you are trying to copy. + + """ + name = "False Identity" + source = "Background (Charlattan)" + + +class CriminalContact(Feature): + """You have a reliable and trustworthy contact who acts as your liaison to a + network o f other criminals. You know how to get messages to and from your + contact, even over great distances; specifically, you know the local + messengers, corrupt caravan masters, and seedy sailors who can deliver + messages for you. + + """ + name = "Criminal Contact" + source = "Background (Criminal)" + + +class ByPopularDemand(Feature): + """You can always find a place to perform, usually in an inn or tavern but + possibly with a circus, at a theater, or even in a noble’s court. At such a + place, you receive free lodging and food of a modest or comfortable + standard (depending on the quality of the establishment), as long as you + perform each night. In addition, your performance makes you something of a + local figure. When strangers recognize you in a town where you have + performed, they typically take a liking to you. + + """ + name = "By Popular Demand" + source = "Background (Entertainer)" + + +class RusticHospitality(Feature): + """Since you come from the ranks of the common folk, you fit in among them + with ease. You can find a place to hide, rest, or recuperate among other + commoners, unless you have shown yourself to be a danger to them. They will + shield you from the law or anyone else searching for you, though they will + not risk their lives for you. + + """ + name = "Rustic Hospitality" + source = "Background (Folk Hero)" + + +class GuildMembership(Feature): + """As an established and respected member of a guild, you can rely on certain + benefits that membership provides. Your fellow guild members will provide + you with lodging and food if necessary, and pay for your funeral if + needed. In some cities and towns, a guildhall offers a central place to + meet other members of your profession, which can be a good place to meet + potential patrons, allies, or hirelings. + + Guilds often wield tremendous political power. If you are accused of a + crime, your guild will support you if a good case can be made for your + innocence or the crime is justifiable. You can also gain access to powerful + political figures through the guild, if you are a member in good + standing. Such connections might require the donation of money or magic + items to the guild’s coffers. + + You must pay dues of 5 gp per month to the guild. If you miss payments, you + must make up back dues to remain in the guild’s good graces. + + """ + name = "Guild Membership" + source = "Background (Guild Artisan)" + + +class Discovery(Feature): + """The quiet seclusion of your extended hermitage gave you access to a unique + and powerful discovery. The exact nature of this revelation depends on the + nature of your seclusion. It might be a great truth about the cosmos, the + deities, the powerful beings of the outer planes, or the forces of + nature. It could be a site that no one else has ever seen. You might have + uncovered a fact that has long been forgotten, or unearthed some relic of + the past that could rewrite history. It might be information that would be + damaging to the people who or consigned you to exile, and hence the reason + for your return to society. + + Work with your DM to determine the details of + your discovery and its impact on the campaign. + + """ + name = "Discovery" + source = "Background (Hermit)" + + +class PositionOfPrivilege(Feature): + """Thanks to your noble birth, people are inclined to think the best of + you. You are welcome in high society, and people assume you have the right + to be wherever you are. The common folk make every effort to accommodate + you and avoid your displeasure, and other people of high birth treat you as + a member of the same social sphere. You can secure an audience with a local + noble if you need to. + + """ + name = "Position of Privilege" + source = "Background (Noble)" + + +class Wanderer(Feature): + """You have an excellent memory for maps and geography, and you can always + recall the general layout of terrain, settlements, and other features + around you. In addition, you can find food and fresh water for yourself and + up to five other people each day, provided that the land offers berries, + small game, water, and so forth. + + """ + name = "Wanderer" + source = "Background (Outlander)" + + +class Researcher(Feature): + """When you attempt to learn or recall a piece of lore, if you do not know + that information, you often know where and from whom you can obtain + it. Usually, this information comes from a library, scriptorium, + university, or a sage or other learned person or creature. Your DM might + rule that the knowledge you seek is secreted away in an almost inaccessible + place, or that it simply cannot be found. Unearthing the deepest secrets of + the multiverse can require an adventure or even a whole campaign. + """ + name = "Researcher" + source = "Background (Sage)" + + +class ShipsPassage(Feature): + """When you need to, you can secure free passage on a sailing ship for + yourself and your adventuring companions. You might sail on the ship you + served on, or another ship you have good relations with (perhaps one + captained by a former crewmate). Because you’re calling in a favor, you + can’t be certain of a schedule or route that will meet your every + need. Your Dungeon Master will determine how long it takes to get where you + need to go. In return for your free passage, you and your companions are + expected to assist the crew during the voyage + + """ + name = "Ship's Passage" + source = "Background (Sailor)" + + +class MilitaryRank(Feature): + """You have a military rank from your career as a soldier. Soldiers loyal to + your former military organization still recognize your authority and + influence, and they defer to you if they are of a lower rank. You can + invoke your rank to exert influence over other soldiers and requisition + simple equipment or horses for temporary use. You can also usually gain + access to friendly military encampments and fortresses where your rank is + recognized. + + """ + name = "Military Rank" + source = "Background (Soldier)" + + +class CitySecrets(Feature): + """You know the secret patterns and flow to cities and can find passages + through the urban sprawl that others would miss. When you are not in + combat, you (and companions you lead) can travel between any two locations + in the city twice as fast as your speed would normally allow. + + """ + name = "City Secrets" + source = "Background (Urchin)" + + +# Swords Coast Adventurer's Guide +class AllEyesOnYou(Feature): + """Your accent, mannerisms, figures of speech, and per- haps 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 + of everyday folk who are eager to hear stories of your homeland. + + You can parley this attention into access to people and places you might + not otherwise have, for you and your traveling companions. Noble lords, + scholars, and merchant princes, to name a few, might be interested in + hearing about your distant homeland and people. + + """ + name = "All Eyes on You" + source = "Background (Far Traveler)" + + +class EarToTheGround(Feature): + """You are in frequent contact with people in the segment of society that your + chosen quarries move through. These people might be associated with the + criminal underworld, the rough-and-tumble folk of the streets, or members + of high society. This connection comes in the form of a contact in any city + you visit, a person who provides information about the people and places of + the local area. + + """ + name = "Ear to the Ground" + source = "Background (Urban Bounty Hunter)" + + +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 + 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. + + """ + name = "Watcher's Eye" + source = "Background (City Watch)" + + +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 + such a settlement might vie among themselves to determine who can offer you + (and possibly your compa- triots) the finest accommodations and assistance. + + """ + name = "Respect of the Stout Folk" + source = "Background (Clan Crafter)" + + +class LibraryAccess(Feature): + """Though others must often endure extensive interviews and significant fees to + gain access to even the most common archives in your library, you have free + and easy access to the majority of the library, though it might also have + repositories of lore that are too valuable, magical, or secret to permit + anyone immediate access. + + You have a working knowledge of your cloister's personnel and bureaucracy, + 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 + scholar. + + """ + name = "Library Access" + source = "Background (Cloistered Scholar)" + + +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 + 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. + + """ + name = "Court Functionary" + source = "Background (Courtier)" + + +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 + 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 + for you or risk revealing their true identities. + + """ + name = "Save Haven" + source = "Background (Faction Agent)" + + +class Inheritance(Feature): + """Choose or randomly determine your inheritance from among the possibilities + in the table in SCAG. Work with your Dungeon Master to come up with + details: Why is your inheritance so important, and what is its full story? + You might prefer for the DM to invent these details as part of the game, + allowing you to learn more about your inheritance as your character does. + + The Dungeon Master is free to use your inheritance as a story hook, sending + you on quests to learn more about its history or true nature, or + confronting you with foes who want to claim it for themselves or prevent + you from learning what you seek. The DM also determines the properties of + your inheritance and how they figure into the item's history and + importance. For instance, the object might be a minor magic item, or one + that begins with a modest ability and increases in potency with the passage + of time. Or, the true nature of your inheritance might not be apparent at + first and is revealed only when certain conditions are met. + + When you begin your adventuring career, you can decide whether to tell your + companions about your inheritance right away. Rather than attracting + attention to yourself, you might want to keep your inheritance a secret + until you learn more about what it means to you and what it can do for you. + + """ + name = "Inheritance" + source = "Background (Inheritor)" + + +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 + 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. + + 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 + he or she is being hunted unjustly. + + """ + name = "Knightly Regard" + source = "Background (Knight of the Order)" + + +class MercenaryLife(Feature): + """You know the mercenary life as only someone who has experienced it can. You + are able to identify mercenary companies by their emblems, and you know a + 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 + sufficient to maintain a comfortable lifestyle (see "Practicing a + Profession" under "Downtime Activities" in chapter 8 of the Player's + Handbook). + + """ + name = "Mercenary Life" + source = "Background (Mercenary Veteran)" + + +class UthgardtHeritage(Feature): + """You have an excellent knowledge of not only your tribe's territory, but also + the terrain and natural resources of the rest of the North. You are + familiar enough with any wilderness area that you find twice as much food + and water as you normally would when you forage there. + + Additionally, you can call upon the hospitality of your people, and those + folk allied with your tribe, often including members of druid circles, + tribes of nomadic elves, the Harpers, and the priesthoods devoted to the + gods of the First Circle. + + """ + name = "Uthgardt Heritage" + source = "Background (Uthgardt Tribe Member)" + + +class KeptInStyle(Feature): + """While you are in Waterdeep or elsewhere in the North your house sees to your + everyday needs. Your name and signet are sufficient to cover most of your + expenses; the inns, taverns, and festhalls you frequent are glad to re- + 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 + 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 + actual monetary reward. + + """ + name = "Kept in Style" + source = "Background (Waterdhavian Noble)" diff --git a/dungeonsheets/features/features.py b/dungeonsheets/features/features.py index 00f4709..db2f152 100644 --- a/dungeonsheets/features/features.py +++ b/dungeonsheets/features/features.py @@ -27,6 +27,8 @@ class Feature(): """ name = "Generic Feature" source = '' # race, class, background, etc. + spells_known = () + spells_prepared = () needs_implementation = False # Set to True if need to find way to compute stats def __eq__(self, other): diff --git a/dungeonsheets/features/races.py b/dungeonsheets/features/races.py index 67d26c5..888d567 100644 --- a/dungeonsheets/features/races.py +++ b/dungeonsheets/features/races.py @@ -125,6 +125,7 @@ class SunlightSensitivity(Feature): name = "Sunlight Sensitivity" source = "Race (Dark Elf)" + class DrowMagic(Feature): """You know the dancing lights cantrip. When you reach 3rd level, you can cast the faerie fire spell once per day. When you reach 5th level, you can @@ -259,7 +260,6 @@ class NaturalIllusionist(Feature): """ name = "Natural Illusionist" source = "Race (Forest Gnome)" - needs_implementation = True class SpeakWithSmallBeasts(Feature): @@ -357,6 +357,7 @@ class InfernalLegacy(Feature): """ name = "Infernal Legacy" source = "Race (Tiefling)" + needs_implementation = True # Aasimar @@ -385,7 +386,6 @@ class LightBearer(Feature): """ name = "Light Bearer" source = "Race (Aasimar)" - needs_implementation = True class RadiantSoul(Feature): diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index 9d9a50f..6860b7f 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -97,8 +97,7 @@ def create_latex_pdf(char, basename, template): f'{basename_}.log'] for filename in filenames: if os.path.exists(filename): - pass - #os.remove(filename) + os.remove(filename) # Compile the PDF pdf_file = f'{basename}.pdf' output_dir = os.path.abspath(os.path.dirname(pdf_file)) diff --git a/dungeonsheets/race.py b/dungeonsheets/race.py index 38fe7b3..f8bf9d4 100644 --- a/dungeonsheets/race.py +++ b/dungeonsheets/race.py @@ -1,4 +1,4 @@ -from . import weapons +from . import (weapons, spells) from . import features as feats from collections import defaultdict @@ -22,6 +22,8 @@ class Race(): wisdom_bonus = 0 charisma_bonus = 0 hit_point_bonus = 0 + spells_known = () + spells_prepared = () def __init__(self): self.features = tuple([f() for f in self.features]) @@ -99,6 +101,8 @@ class DarkElf(Elf): charisma_bonus = 1 features = (feats.SuperiorDarkvision, feats.FeyAncestry, feats.Trance, feats.SunlightSensitivity, feats.DrowMagic) + spells_known = (spells.DancingLights(),) + spells_prepared = (spells.DancingLights(),) # Halflings @@ -164,6 +168,8 @@ class ForestGnome(Gnome): dexterity_bonus = 1 features = Gnome.features + (feats.NaturalIllusionist, feats.SpeakWithSmallBeasts) + spells_known = (spells.MinorIllusion(),) + spells_prepared = (spells.MinorIllusion(),) class RockGnome(Gnome): @@ -230,6 +236,8 @@ class Aasimar(Race): languages = ("Common", "Celestial") features = (feats.Darkvision, feats.CelestialResistance, feats.HealingHands, feats.LightBearer) + spells_known = (spells.Light(),) + spells_prepared = (spells.Light(),) # Protector Aasimar @@ -334,8 +342,10 @@ class Triton(Race): features = (feats.Amphibious, feats.ControlAirAndWater, feats.EmissaryOfTheSea, feats.GuardiansOfTheDepths) languages = ("Common", "Primordial") + spells_known = (spells.FogCloud(),) + spells_prepared = (spells.FogCloud(),) + - # Aarakocra class Aarakocra(Race): name = 'Aarakocra' @@ -350,6 +360,7 @@ class Aarakocra(Race): # Genasi class Genasi(Race): + name = "Genasi" constitution_bonus = 2 size = 'medium' speed = 30 @@ -357,61 +368,44 @@ class Genasi(Race): class AirGenasi(Genasi): + name = "Air Genasi" dexterity_bonus = 1 features = (feats.UnendingBreath, feats.MingleWithTheWind) class EarthGenasi(Genasi): + name = "Earth Genasi" strength_bonus = 1 features = (feats.EarthWalk, feats.MergeWithStone) class FireGenasi(Genasi): + name = "Fire Genasi" intelligence_bonus = 1 features = (feats.Darkvision, feats.FireResistance, feats.ReachToTheBlaze) class WaterGenasi(Genasi): + name = "Water Genasi" wisdom_bonus = 1 speed = "30 (30 swim)" features = (feats.AcidResistance, feats.Amphibious, feats.CallToTheWave) -race_dict = { - "Hill Dwarf": HillDwarf, - 'Mountain Dwarf': MountainDwarf, - 'High Elf': HighElf, - 'Wood Elf': WoodElf, - 'Dark Elf': DarkElf, - 'Lightfoot Halfling': LightfootHalfling, - 'Stout Halfling': StoutHalfling, - 'Human': Human, - 'Dragonborn': Dragonborn, - 'Forest Gnome': ForestGnome, - 'Rock Gnome': RockGnome, - 'Deep Gnome': DeepGnome, - 'Half-Elf': HalfElf, - 'Half-Orc': HalfOrc, - 'Tiefling': Tiefling, - 'Fallen Aasimar': FallenAasimar, - 'Protector Aasimar': ProtectorAasimar, - 'Scourge Aasimar': ScourgeAasimar, - 'Firbolg': Firbolg, - 'Goliath': Goliath, - 'Lizardfolk': Lizardfolk, - 'Kenku': Kenku, - 'Tabaxi': Tabaxi, - 'Triton': Triton, - 'Aarakocra': Aarakocra, - 'Fire Genasi': FireGenasi, - 'Earth Genasi': EarthGenasi, - 'Water Genasi': WaterGenasi, - 'Air Genasi': AirGenasi, -} +PHB_races = [HillDwarf, MountainDwarf, HighElf, WoodElf, DarkElf, + LightfootHalfling, StoutHalfling, Human, Dragonborn, + ForestGnome, RockGnome, HalfElf, HalfOrc, Tiefling] -__all__ = tuple(race_dict.keys()) +VOLO_races = [ProtectorAasimar, ScourgeAasimar, FallenAasimar, + Firbolg, Goliath, Lizardfolk, Kenku, Tabaxi, Triton] +EE_races = [Aarakocra, DeepGnome, AirGenasi, FireGenasi, EarthGenasi, + WaterGenasi] +available_races = PHB_races + VOLO_races + EE_races + +__all__ = tuple([r.name for r in available_races]) + ( + 'available_races', 'PHB_races', 'VOLO_races', 'EE_races') diff --git a/dungeonsheets/spellbook_template.tex b/dungeonsheets/spellbook_template.tex index c0e6e98..7e2db7a 100644 --- a/dungeonsheets/spellbook_template.tex +++ b/dungeonsheets/spellbook_template.tex @@ -14,7 +14,7 @@ \maketitle [% for spl in character.spells %] - [% if spl.__class__ in character.spells_prepared %] + [% if spl in character.spells_prepared %] { [% elif spl.level == 0 %] { diff --git a/dungeonsheets/spells/spells.py b/dungeonsheets/spells/spells.py index cb6aae1..33933bf 100644 --- a/dungeonsheets/spells/spells.py +++ b/dungeonsheets/spells/spells.py @@ -47,6 +47,9 @@ class Spell(): def __eq__(self, other): return (self.name == other.name) and (self.level == other.level) + + def __hash__(self): + return 0 @property def component_string(self): diff --git a/dungeonsheets/spells/spells_a_d.py b/dungeonsheets/spells/spells_a_d.py index 9f3877c..f974597 100644 --- a/dungeonsheets/spells/spells_a_d.py +++ b/dungeonsheets/spells/spells_a_d.py @@ -1704,6 +1704,25 @@ class DetectMagic(Spell): classes = ('Bard', 'Cleric', 'Druid', 'Paladin', 'Ranger', 'Sorceror', 'Wizard', ) +class DetectPoisonAndDisease(Spell): + """For the duration, you can sense the presence and location of poisons, + poisonous creatures, and diseases within 30 feet of you. You also identify + the kind of poison, poisonous creature, or disease in each case. + + The spell can penetrate most barriers, but is blocked by 1 foot of stone, 1 + inch of common metal, a thin sheet of lead, or 3 feet of wood or dirt. + + """ + name = "Detect Poison and Disease" + level = 1 + casting_time = '1 action' + casting_range = "Self (30 feet)" + components = ("V", "S", "M") + materials = "a yew leaf" + magic_school = "Divination" + classes = ("Cleric", 'Druid', 'Paladin', 'Ranger') + + class DimensionDoor(Spell): """You teleport yourself from your current location to any other spot within range. You arrive at exactly the spot desired. It can be a diff --git a/dungeonsheets/spells/spells_e_i.py b/dungeonsheets/spells/spells_e_i.py index 106cb74..9f80315 100644 --- a/dungeonsheets/spells/spells_e_i.py +++ b/dungeonsheets/spells/spells_e_i.py @@ -174,7 +174,23 @@ class Etherealness(Spell): materials = "" duration = "Up to 8 hours" magic_school = "Transmutation" - classes = () + classes = ("Bard", 'Cleric', 'Sorceror', 'Warlock', 'Wizard') + + +class ExpeditiousRetreat(Spell): + """This spell allows you to move at an incredible pace. When you cast this + spell, and then as a bonus action on each of your turns until the spell + ends, you can take the Dash action. + + """ + name = "Expeditious Retreat" + level = 1 + casting_time = '1 bonus action' + components = ('V', 'S') + duration = "Concentration, up to 10 minutes" + casting_range = "self" + magic_school = "Transmutation" + classes = ("Sorceror", "Warlock", "Wizard") class Eyebite(Spell): @@ -468,6 +484,28 @@ class FindSteed(Spell): magic_school = "Conjuration" classes = ('Paladin', ) + +class FogCloud(Spell): + """You create a 20-foot-radius sphere of fog centered on a point within + range. The sphere spreads around corners, and its area is heavily obscured, + It lasts for the duration or until a wind of moderate or greater speed (at + least 10 miles per hour) disperses it. + + At Higher Level: + + When you cast this spell using a spell slot of 2nd level or higher, the + radius of the fog increases by 20 feet for each slot level above 1st. + + """ + name = "Fog Cloud" + level = 1 + casting_time = "1 action" + casting_range = "120 feet" + components = ("V", "S") + duration = "Concentration, up to 1 hour" + magic_school = "Conjuration" + classes = ('Druid', 'Ranger', 'Sorceror', 'Wizard') + class Foresight(Spell): """You touch a willing creature and bestow a limited ability to see @@ -681,6 +719,35 @@ class GuidingBolt(Spell): classes = () +class GustOfWind(Spell): + """A line of strong wind 60 feet long and 10 feet wide blasts from you in a + direction you choose for the spell’s duration. Each creature that starts + its turn in the line must succeed on a Strength saving throw or be pushed + 15 feet away from you in a direction following the line. + + Any creature in the line must spend 2 feet of movement for every 1 foot it + moves when moving closer to you. + + The gust disperses gas or vapor, and it extinguishes candles, torches, and + similar unprotected flames in the area. It causes protected flames, such as + those of lanterns, to dance wildly and has a 50 percent chance to + extinguish them. + + As a bonus action on each of your turns before the spell ends, you can + change the direction in which the line blasts from you. + + """ + name = "Gust of Wind" + level = 2 + casting_time = "1 action" + casting_range = "Self (60-foot line)" + components = ("V", 'S', 'M') + materials = "A legume seed" + duration = "Concentration, up to 1 minute" + magic_school = "Evocation" + classes = ("Druid", "Sorceror", 'Wizard') + + class Harm(Spell): """You unleash a virulent disease on a creature that you can see within range. The target must make a Constitution saving throw. On @@ -872,6 +939,32 @@ class HolyAura(Spell): classes = () +class HuntersMark(Spell): + """You choose a creature you can see within range and mystically mark it as + your quarry. Until the spell ends, you deal an extra 1d6 damage to the + target whenever you hit it with a weapon attack, and you have advantage on + any Wisdom (Perception) or Wisdom (Survival) check you make to find it. If + the target drops to 0 hit points before this spell ends, you can use a + bonus action on a subsequent turn of yours to mark a new creature. + + At Higher Level: + + When you cast this spell using a spell slot of 3rd or 4th level, you can + maintain your concentration on the spell for up to 8 hours. When you use a + spell slot of 5th level or higher, you can maintain your concentration on + the spell for up to 24 hours. + + """ + name = "Hunter's Mark" + level = 1 + casting_time = "1 bonus action" + casting_range = "90 feet" + components = ("V") + duration = "Concentration, up to 1 hour" + magic_school = "Diviniation" + classes = ("Ranger",) + + class IceStorm(Spell): """A hail of rock-hard ice pounds to the ground in a 20-foot-radius, 40-foot-high cylinder centered on a point within range. Each