From bb9e44bc818346719cfd5d0deb0356ff007800a0 Mon Sep 17 00:00:00 2001 From: Ben Cook Date: Fri, 21 Dec 2018 13:45:06 -0500 Subject: [PATCH] Added invocations from PHB to Warlock --- dungeonsheets/character.py | 2 +- dungeonsheets/classes/classes.py | 10 +- dungeonsheets/classes/paladin.py | 2 +- dungeonsheets/features/features.py | 14 +- dungeonsheets/features/warlock.py | 335 +++++++++++++++++++++ dungeonsheets/forms/character_template.txt | 2 +- 6 files changed, 355 insertions(+), 10 deletions(-) diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index d638915..ca96930 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -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 = [] diff --git a/dungeonsheets/classes/classes.py b/dungeonsheets/classes/classes.py index 6ffb4a3..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,12 +24,19 @@ 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 # 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(): diff --git a/dungeonsheets/classes/paladin.py b/dungeonsheets/classes/paladin.py index 33f68ac..76db64a 100644 --- a/dungeonsheets/classes/paladin.py +++ b/dungeonsheets/classes/paladin.py @@ -248,7 +248,7 @@ class Paladin(CharClass): 9: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0), 10: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0), 11: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0), -p 12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0), + 12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0), 13: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0), 14: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0), 15: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0), diff --git a/dungeonsheets/features/features.py b/dungeonsheets/features/features.py index eb1971a..c3bc5eb 100644 --- a/dungeonsheets/features/features.py +++ b/dungeonsheets/features/features.py @@ -89,11 +89,13 @@ class FeatureSelector(Feature): """ options = dict() - def __init__(self, selection=None): + def __init__(self, feature_choices=[]): keep_source = self.source - # Transform into selected feature - if selection.lower() in self.options(): - new_feat = self.options[selection.lower()] - self.__dict__.update(new_feat.__dict__) - new_feat.__init__(self) + # 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/warlock.py b/dungeonsheets/features/warlock.py index e69de29..1a04ad9 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 'V' in s.components: + c = list(s.components) + c.remove('V') + 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.MistyVisions,) + + +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/forms/character_template.txt b/dungeonsheets/forms/character_template.txt index c992dad..1d287f0 100644 --- a/dungeonsheets/forms/character_template.txt +++ b/dungeonsheets/forms/character_template.txt @@ -29,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 }}