mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-19 04:33:26 +02:00
Added spell sheet for casters, but not spells yet.
This commit is contained in:
Binary file not shown.
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
@@ -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, I’ve 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 life’s work so far,
|
||||
and no vault is secure enough to keep it safe."""
|
||||
|
||||
flaws = """I’ll 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 can’t discern color in darkness, only shades
|
||||
of gray.
|
||||
|
||||
Fey Ancestry: You have advantage on saving throws against being
|
||||
charmed, and magic can’t put you to sleep.
|
||||
|
||||
Trance: Elves don’t 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 Oghma’s 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 won’t endanger them.""")
|
||||
@@ -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,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))
|
||||
|
||||
Reference in New Issue
Block a user