Added spell sheet for casters, but not spells yet.

This commit is contained in:
Mark Wolfman
2018-03-28 23:56:11 -05:00
parent cafde3465d
commit 8a53b7c2be
9 changed files with 231 additions and 28 deletions
Binary file not shown.
+68
View File
@@ -74,6 +74,8 @@ class Character():
equipment = ""
weapons = [] # Replaced in __init__ constructor
_proficiencies_text = tuple()
# Magic
spellcasting_ability = None
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
@@ -108,6 +110,25 @@ class Character():
# Lookup general attributes
setattr(self, attr, val)
@property
def is_spellcaster(self):
result = (self.spellcasting_ability is not None)
return result
@property
def spell_save_dc(self):
ability_mod = getattr(self, self.spellcasting_ability).modifier
return (8 + self.proficiency_bonus + ability_mod)
@property
def spell_attack_bonus(self):
ability_mod = getattr(self, self.spellcasting_ability).modifier
return (self.proficiency_bonus + ability_mod)
def spell_slots(self, spell_level):
"""How many spells slots are available for this spell level."""
return self.spell_slots_by_level[self.level][spell_level]
def is_proficient(self, weapon: Weapon):
"""Is the character proficient with this item?
@@ -308,6 +329,29 @@ class Warlock(Character):
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
weapon_proficiencies = weapons.simple_weapons
spellcasting_ability = 'charisma'
spell_slots_by_level = {
1: (2, 1, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 0, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 0, 2, 0, 0, 0, 0, 0, 0, 0),
5: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
6: (3, 0, 0, 3, 0, 0, 0, 0, 0, 0),
7: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
8: (3, 0, 0, 0, 2, 0, 0, 0, 0, 0),
9: (3, 0, 0, 0, 0, 2, 0, 0, 0, 0),
10: (4, 0, 0, 0, 0, 2, 0, 0, 0, 0),
11: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
12: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
13: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
14: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
15: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
16: (4, 0, 0, 0, 0, 3, 0, 0, 0, 0),
17: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
18: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
19: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
20: (4, 0, 0, 0, 0, 4, 0, 0, 0, 0),
}
class Wizard(Character):
@@ -319,3 +363,27 @@ class Wizard(Character):
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
spellcasting_ability = 'intelligence'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (3, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (3, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (3, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (4, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (4, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (4, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (4, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (4, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (4, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (5, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (5, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (5, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (5, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (5, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (5, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (5, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (5, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
+61 -16
View File
@@ -52,7 +52,32 @@ def load_character_file(filename):
return char_props
def create_fdf(character, fdfname):
def create_spells_pdf(character, basename, flatten=False):
class_level = (character.class_name + ' ' + str(character.level))
spell_level = lambda x : (x or '')
fields = (
('Spellcasting Class 2', class_level),
("SpellcastingAbility 2", character.spellcasting_ability.capitalize()),
('SpellSaveDC 2', character.spell_save_dc),
('SpellAtkBonus 2', mod_str(character.spell_attack_bonus)),
# Number of spell slots
('SlotsTotal 19', spell_level(character.spell_slots(1))),
('SlotsTotal 20', spell_level(character.spell_slots(2))),
('SlotsTotal 21', spell_level(character.spell_slots(3))),
('SlotsTotal 22', spell_level(character.spell_slots(4))),
('SlotsTotal 23', spell_level(character.spell_slots(5))),
('SlotsTotal 24', spell_level(character.spell_slots(6))),
('SlotsTotal 25', spell_level(character.spell_slots(7))),
('SlotsTotal 26', spell_level(character.spell_slots(8))),
('SlotsTotal 27', spell_level(character.spell_slots(9))),
)
# Make the actual pdf
dirname = os.path.dirname(os.path.abspath(__file__))
src_pdf = os.path.join(dirname, 'blank-spell-sheet-default.pdf')
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def create_character_pdf(character, basename, flatten=False):
# Prepare the list of fields
class_level = (character.class_name + ' ' + str(character.level))
fields = [
@@ -173,11 +198,29 @@ def create_fdf(character, fdfname):
prof_text = "Proficiencies:\n" + text_box(character.proficiencies_text)
prof_text += "\n\nLanguages:\n" + text_box(character.languages)
fields.append(('ProficienciesLang', prof_text))
# Prepare the actual PDF
dirname = os.path.dirname(os.path.abspath(__file__))
src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf')
return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def make_pdf(fields, src_pdf, basename, flatten=False):
# Create the actual FDF file
fdfname = basename + '.fdf'
fdf = forge_fdf("", fields, [], [], [])
fdf_file = open(fdfname, "wb")
fdf_file.write(fdf)
fdf_file.close()
# Build the final flattened PDF documents
dest_pdf = basename + '.pdf'
popenargs = [
'pdftk', src_pdf, 'fill_form', fdfname, 'output', dest_pdf,
]
if flatten:
popenargs.append('flatten')
subprocess.call(popenargs)
# Clean up temporary files
os.remove(fdfname)
def make_sheet(character_file, flatten=False):
@@ -195,28 +238,30 @@ def make_sheet(character_file, flatten=False):
CharClass = getattr(character, class_name)
char = CharClass(**char_props)
# Set the fields in the FDF
fdfname = os.path.splitext(character_file)[0] + '.fdf'
create_fdf(character=char, fdfname=fdfname)
# Build the final flattened PDF document
dirname = os.path.dirname(os.path.abspath(__file__))
src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf')
dest_pdf = os.path.splitext(character_file)[0] + '.pdf'
popenargs = [
'pdftk', src_pdf, 'fill_form', fdfname, 'output', dest_pdf,
]
if flatten:
popenargs.append('flatten')
char_base = os.path.splitext(character_file)[0] + '_char'
sheets = [char_base + '.pdf']
create_character_pdf(character=char, basename=char_base, flatten=flatten)
if char.is_spellcaster:
spell_base = os.path.splitext(character_file)[0] + '_spells'
create_spells_pdf(character=char, basename=spell_base, flatten=flatten)
sheets.append(spell_base + '.pdf')
# Combine sheets into final pdf
final_pdf = os.path.splitext(character_file)[0] + '.pdf'
popenargs = ('pdftk', *sheets, 'cat', 'output', final_pdf)
subprocess.call(popenargs)
# Clean up temporary files
os.remove(fdfname)
# Remove temporary files
for sheet in sheets:
os.remove(sheet)
def main():
# Prepare an argument parser
parser = argparse.ArgumentParser(
description='Prepare Dungeons and Dragons character sheets as PDFs')
parser.add_argument('filename', type=str, nargs="?", help="Python file with character definition")
parser.add_argument('--flatten', '-F', action="store_true", help="Remove the PDF fields once processed.")
parser.add_argument('filename', type=str, nargs="?",
help="Python file with character definition")
parser.add_argument('--flatten', '-F', action="store_true",
help="Remove the PDF fields once processed.")
args = parser.parse_args()
# Process the requested files
if args.filename is None:
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
name = 'Mr. Stabby'
character_class = 'rogue'
player_name = 'Mark'
player_name = 'Mike'
background = "Criminal"
race = "Lightfoot halfling"
level = 3
Binary file not shown.
+88
View File
@@ -0,0 +1,88 @@
name = 'Inara Serradon'
character_class = 'wizard'
player_name = 'Mark'
background = "Acolyte"
race = "High-Elf"
level = 3
alignment = "Chaotic good"
xp = 2190
hp_max = 16
# Ability Scores
strength = 10
dexterity = 15
constitution = 14
intelligence = 16
wisdom = 12
charisma = 8
skill_proficiencies = [
'arcana',
'insight',
'investigation',
'perception',
'religion',
]
# Proficiencies and languages
languages = "Common, Elvish, Draconic, Dwarvish, Goblin."
# Inventory
cp = 316
sp = 283
ep = 28
gp = 125
pp = 0
weapons = ('shortsword', 'longsword')
equipment = (
"""Gallon of ale, red cloak, shortsword, longsword, jar of salt, vodka
(500mL), potion of vitality, wand of magic missiles (7/7),
component pouch, spellbook, backpack, bottle of ink, ink pen, 10
sheets of parchment, small knife, tome of historical lore, holy
symbol, prayer book, set of common clothes, pouch.""")
# Backstory
personality_traits = """I use polysyllabic words that convey the impression of
erudition. Also, Ive spent so long in the temple that I have little
experience dealing with people on a casual basis."""
ideals = """Knowledge. The path to power and self- improvement is through
knowledge."""
bonds = """The tome I carry with me is the record of my lifes work so far,
and no vault is secure enough to keep it safe."""
flaws = """Ill do just about anything to uncover historical secrets that
would add to my research."""
features_and_traits = (
"""Spellcasting Ability: Intelligence is your spellcasting ability for
your spells. The saving throw DC to resist a spell you cast is
13. Your attack bonus when you make an attack with a spell is
+5. See the rulebook for rules on casting your spells.
Arcane Recovery: You can regain some of your magical energy by
studying your spellbook. Once per day during a short rest, you can
choose to recover expended spell slots with a combined level equal
to or less than half your wizard level (rounded up).
Darkvision: You see in dim light within a 60-foot radius of you as
if it were bright light, and in darkness in that radius as if it
were dim light. You cant discern color in darkness, only shades
of gray.
Fey Ancestry: You have advantage on saving throws against being
charmed, and magic cant put you to sleep.
Trance: Elves dont need to sleep. They meditate deeply, remaining
semiconscious, for 4 hours a day and gain the same benefit a human
does from 8 hours of sleep.
Shelter of the Faithful: As a servant of Oghma, you command the
respect of those who share your faith, and you can perform the
rites of Oghma. You and your companions can expect to receive free
healing and care at a temple, shrine, or other established
presence of Oghmas faith. Those who share your religion will
support you (and only you) at a modest lifestyle. You also have
ties to the temple of Oghma in Neverwinter, where you have a
residence. When you are in Neverwinter, you can call upon the
priests there for assistance that wont endanger them.""")
+13
View File
@@ -112,3 +112,16 @@ class TestCharacter(TestCase):
self.assertEqual(char.proficiency_bonus, 6)
char.level = 20
self.assertEqual(char.proficiency_bonus, 6)
def test_spell_slots(self):
char = Wizard()
# Wizard level 1
char.level = 1
self.assertEqual(char.spell_slots(spell_level=0), 3)
self.assertEqual(char.spell_slots(spell_level=1), 2)
self.assertEqual(char.spell_slots(spell_level=2), 0)
# Wizard level 2
char.level = 2
self.assertEqual(char.spell_slots(spell_level=0), 3)
self.assertEqual(char.spell_slots(spell_level=1), 3)
self.assertEqual(char.spell_slots(spell_level=2), 0)
-11
View File
@@ -11,14 +11,3 @@ class CharacterFileTestCase(unittest.TestCase):
charfile = CHARFILE
result = make_sheets.load_character_file(charfile)
self.assertEqual(result['strength'], 8)
class FDFTestCase(unittest.TestCase):
def tearDown(self):
if os.path.exists('temp.fdf'):
os.remove('temp.fdf')
def test_create_fdf(self):
fdfname = 'temp.fdf'
char = character.Character()
make_sheets.create_fdf(char, fdfname=fdfname)
self.assertTrue(os.path.exists(fdfname))