diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 9ac3123..faf5009 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -12,7 +12,7 @@ import subprocess from .stats import Ability, Skill, findattr, ArmorClass, Speed from .dice import read_dice_str from . import (weapons, race, background, spells, armor, monsters, - exceptions, classes, features) + exceptions, classes, features, magic_items) from .weapons import Weapon from .armor import Armor, NoArmor, Shield, NoShield @@ -345,7 +345,7 @@ class Character(): fts |= set(self.race.features_by_level[lvl]) if self.background is not None: fts |= set(getattr(self.background, 'features', ())) - return tuple(fts) + return sorted(tuple(fts), key=(lambda x: x.name)) @property def custom_features_text(self): @@ -375,7 +375,6 @@ class Character(): return (len(self.spellcasting_classes) > 0) def spell_slots(self, spell_level): - # TODO: Update this for Multiclassing if len(self.spellcasting_classes) == 1: return self.spellcasting_classes[0].spell_slots(spell_level) else: @@ -407,7 +406,7 @@ class Character(): 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) + return sorted(tuple(spells), key=(lambda x: (x.level, x.name))) @property def spells_prepared(self): @@ -418,7 +417,7 @@ class Character(): spells |= set(c.spells_prepared) if self.race is not None: spells |= set(self.race.spells_prepared) - return tuple(spells) + return sorted(tuple(spells), key=(lambda x: (x.level, x.name))) def set_attrs(self, **attrs): """Bulk setting of attributes. Useful for loading a character from a @@ -436,7 +435,12 @@ class Character(): if isinstance(val, str): val = [val] for mitem in val: - self.magic_items.append(mitem(owner=self)) + try: + self.magic_items.append(findattr(magic_items, mitem)(owner=self)) + except (AttributeError): + msg = (f'Magic Item "{mitem}" not defined. ' + f'Please add it to ``magic_items.py``') + warnings.warn(msg) elif attr == 'weapon_proficiencies': self.other_weapon_proficiencies = () wps = set([findattr(weapons, w) for w in val]) @@ -511,7 +515,6 @@ class Character(): if self.has_feature(features.NaturalExplorerRevised): ini += '(A)' return ini - def is_proficient(self, weapon: Weapon): """Is the character proficient with this item? @@ -568,6 +571,14 @@ class Character(): s += '\n\n=================\n\n' return s + @property + def magic_items_text(self): + s = ', '.join([f.name + ("**" if f.needs_implementation else "") + for f in sorted(self.magic_items, key=(lambda x: x.name))]) + if s: + s += ', ' + return s + def wear_armor(self, new_armor): """Accepts a string or Armor class and replaces the current armor. diff --git a/dungeonsheets/forms/features_template.tex b/dungeonsheets/forms/features_template.tex index b5f2bec..d92ba83 100644 --- a/dungeonsheets/forms/features_template.tex +++ b/dungeonsheets/forms/features_template.tex @@ -13,17 +13,21 @@ \maketitle +\section*{Subclasses} + [% for sc in character.subclasses if sc not in ['', None, 'None', 'none']%] - \section*{Subclass: [[ sc.name ]]} + \subsection*{Subclass: [[ sc.name ]]} [[ sc.__doc__|rst_to_latex ]] [% endfor %] +\section*{Features} + [% for feat in character.features %] - \section*{[[ feat.name ]]} + \subsection*{[[ feat.name ]]} \noindent \textbf{Source:} [[ feat.source ]] \\ @@ -36,9 +40,11 @@ [% endfor %] +\section*{Magic Items} + [% for mitem in character.magic_items %] - \section*{[[ mitem.name ]]} + \subsection*{[[ mitem.name ]]} \noindent \textbf{Requires Attunement:} [[ mitem.requires_attunement ]] \\ diff --git a/dungeonsheets/magic_items.py b/dungeonsheets/magic_items.py index d4e00c4..22db4db 100644 --- a/dungeonsheets/magic_items.py +++ b/dungeonsheets/magic_items.py @@ -9,6 +9,15 @@ class MagicItem(): needs_implementation = False rarity = '' + def __init__(self, owner=None): + self.owner = owner + + def __str__(self): + return self.name + + def __repr__(self): + return '\"{:s}\"'.format(str(self)) + class RingOfProtection(MagicItem): """ @@ -19,3 +28,123 @@ class RingOfProtection(MagicItem): ac_bonus = 1 requires_attunement = True rarity = 'Rare' + + +class DecanterOfEndlessWater(MagicItem): + """This stoppered flask sloshes when shaken, as if it contains water. The + decanter weighs 2 pounds. + + You can use an action to remove the stopper and speak one of three command + words, whereupon an amount of fresh water or salt water (your choice) pours + out of the flask. The water stops pouring out at the start of your next + turn. Choose from the following options: + + --"Stream" produces 1 gallon of water. + + --"Fountain" produces 5 gallons of water. + + --"Geyser" produces 30 gallons of water that gushes forth in a geyser 30 + feet long and 1 foot wide. As a bonus action while holding the decanter, + you can aim the geyser at a creature you can see within 30 feet of you. The + target must succeed on a DC 13 Strength saving throw or take 1d4 + bludgeoning damage and fall prone. Instead of a creature, you can target an + object that isn't being worn or carried and that weighs no more than 200 + pounds. The object is either knocked over or pushed up to 15 feet away from + you. + + """ + name = "Decanter of Endless Water" + rarity = 'Uncommon' + + +class ToothOfAnimalFriendship(MagicItem): + """While holding this wolf's tooth, you can expend it's one charge to cast + Animal Friendship (DC 13) or Speak With Animals. + + The charge resets at the next Dawn. + """ + name = "Tooth of Animal Friendship" + rarity = 'Uncommon' + + +class CloakOfBillowing(MagicItem): + """While wearing this cloak, you can use a bonus action to make it billow + dramatically. + + """ + name = "Cloak of Billowing" + rarity = "Common" + + +class CapeOfTheMountebank(MagicItem): + """This cape smells faintly of brimstone. While wearing it, you can use it to + cast the Dimension Door spell as an action. This property of the cape can't + be used again until the next dawn. + + When you disappear, you leave behind a cloud of smoke, and you appear in a + similar cloud of smoke at your destination. The smoke lightly obscures the + space you left and the space you appear in, and it dissipates at the end of + your next turn. A light or stronger wind disperses the smoke. + + """ + name = "Cape of the Mountebank" + rarity = "Rare" + + +class EyesOfCharming(MagicItem): + """These Crystal lenses fit over the eyes. They have 3 Charges. While wearing + them, you can expend 1 charge as an action to cast the Charm Person spell + (save DC 13) on a humanoid within 30 feet of you, provided that you and the + target can see each other. The lenses regain all expended Charges daily at + dawn. + + """ + name = "Eyes of Charming" + rarity = "Uncommon" + requires_attunement = True + + +class CharlattansDie(MagicItem): + """Whenever you roll this six—sided die, you can control which number it + rolls. + + """ + name = "Charlattan's Die" + rarity = "Common" + + +class PipeOfSmokeMonsters(MagicItem): + """While smoking this pipe, you can use an action to ex- hale a puff of smoke + that takes the form of a single crea— ture, such as a dragon, a flumph, or + a froghemoth. The form must be small enough to fit in a 1-foot cube and + loses its shape after a few seconds, becoming an ordi- nary puff of smoke. + + """ + name = 'Pipe of Smoke Monsters' + rarity = "Common" + + +class CoinsOfCommunication(MagicItem): + """This set of multiple coins are virtually indistinguishable from regular Gold + Pieces, but are connected by magic. Once per day, a holder of any of any + coin can whisper a single word into it, after which all coins will + immediately vibrate and the word will replace a word in the traditional + Kings Message imprinted on the coin. This ability cannot be used again by + the holder of any of the coins until the following dawn. + + """ + name = "Coins of Communication" + rarity = "Uncommon" + + +class FlameTongue(MagicItem): + """You can use a Bonus Action to speak this magic sword's Command Word, causing + flames to erupt from the blade. These flames shed bright light in a 40-foot + radius and dim light for an additional 40 feet. While the sword is ablaze, + it deals an extra 2d6 fire damage to any target it hits. The flames last + until you use a Bonus Action to speak the Command Word again or until you + drop or sheathe the sword + + """ + name = "Flame Tongue" + rarity = "Rare" diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index e78b131..77cae12 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -260,7 +260,7 @@ def create_character_pdf(character, basename, flatten=False): 'EP': character.ep, 'GP': character.gp, 'PP': character.pp, - 'Equipment': text_box(character.equipment), + 'Equipment': text_box(character.magic_items_text + character.equipment), } # Check boxes for proficiencies ST_boxes = { diff --git a/dungeonsheets/weapons.py b/dungeonsheets/weapons.py index 09a8c4b..8ab4d8e 100644 --- a/dungeonsheets/weapons.py +++ b/dungeonsheets/weapons.py @@ -509,7 +509,15 @@ class Musket(Firearm): weight = 10 properties = "Ammunition (range 120/480), Two-Handed, Reload 1, Misfire 2" - + +# Magic Items +class FlameTongue(Greatsword): + name = "Flame Tongue +1" + magic_bonus = 1 + base_damage = "4d6" + damage_type = 'f' + + # Some lists of weapons for easy proficiency resolution simple_melee_weapons = (Club, Dagger, Greatclub, Handaxe, Javelin, LightHammer, Mace, Quarterstaff, Sickle, Spear)