mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 13:15:53 +02:00
Some more advanced characteristics, different player classes, and a CLI interface.
This commit is contained in:
+88
-10
@@ -16,15 +16,15 @@ class Character():
|
||||
name = ""
|
||||
class_name = ""
|
||||
player_name = ""
|
||||
background = ""
|
||||
level = 1
|
||||
alignment = 'true neutral'
|
||||
alignment = "Neutral"
|
||||
race = "Human"
|
||||
xp = 0
|
||||
armor_class = 10
|
||||
speed = 30 # In feet
|
||||
# Hit points
|
||||
hp_max = 10
|
||||
hit_dice_num = 1
|
||||
hit_dice_faces = 8
|
||||
hit_dice_faces = 2
|
||||
# Base stats (ability scores
|
||||
strength = Stat()
|
||||
dexterity = Stat()
|
||||
@@ -32,6 +32,12 @@ class Character():
|
||||
intelligence = Stat()
|
||||
wisdom = Stat()
|
||||
charisma = Stat()
|
||||
# Inventory
|
||||
cp = 0
|
||||
sp = 0
|
||||
ep = 0
|
||||
gp = 0
|
||||
pp = 0
|
||||
|
||||
def __init__(self, **attrs):
|
||||
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
||||
@@ -48,10 +54,82 @@ class Character():
|
||||
"""What type and how many dice to use for re-gaining hit points.
|
||||
|
||||
To change, set hit_dice_num and hit_dice_faces."""
|
||||
return f"{self.hit_dice_num}d{self.hit_dice_faces}"
|
||||
return f"{self.level}d{self.hit_dice_faces}"
|
||||
|
||||
@hit_dice.setter
|
||||
def hit_dice(self, val):
|
||||
dice = read_dice_str(val)
|
||||
self.hit_dice_faces = dice.faces
|
||||
self.hit_dice_num = dice.num
|
||||
@property
|
||||
def proficiency_bonus(self):
|
||||
if self.level < 5:
|
||||
prof = 2
|
||||
elif 5 <= self.level < 9:
|
||||
prof = 3
|
||||
elif 9 <= self.level < 13:
|
||||
prof = 4
|
||||
elif 13 <= self.level < 17:
|
||||
prof = 5
|
||||
elif 17 <= self.level:
|
||||
prof = 6
|
||||
return prof
|
||||
|
||||
@property
|
||||
def armor_class(self):
|
||||
"""Armor class, without items."""
|
||||
return 10 + self.dexterity.modifier
|
||||
|
||||
|
||||
class Barbarian(Character):
|
||||
class_name = 'Barbarian'
|
||||
hit_dice_faces = 12
|
||||
|
||||
|
||||
class Bard(Character):
|
||||
class_name = 'Bard'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Cleric(Character):
|
||||
class_name = 'Cleric'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Druid(Character):
|
||||
class_name = 'Druid'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Fighter(Character):
|
||||
class_name = 'Fighter'
|
||||
hit_dice_faces = 10
|
||||
|
||||
|
||||
class Monk(Character):
|
||||
class_name = 'Monk'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Paladin(Character):
|
||||
class_name = 'Paladin'
|
||||
hit_dice_faces = 10
|
||||
|
||||
|
||||
class Ranger(Character):
|
||||
class_name = 'Ranger'
|
||||
hit_dice_faces = 10
|
||||
|
||||
|
||||
class Rogue(Character):
|
||||
class_name = 'Rogue'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Sorceror(Character):
|
||||
class_name = 'Sorceror'
|
||||
hit_dice_faces = 6
|
||||
|
||||
class Warlock(Character):
|
||||
class_name = 'Warlock'
|
||||
hit_dice_faces = 8
|
||||
|
||||
|
||||
class Wizard(Character):
|
||||
class_name = 'Wizard'
|
||||
hit_dice_faces = 6
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from fdfgen import forge_fdf
|
||||
|
||||
from dungeonsheets.character import Character
|
||||
from dungeonsheets import character
|
||||
from dungeonsheets.stats import mod_str
|
||||
|
||||
"""Program to take character definitions and build a PDF of the
|
||||
character sheet."""
|
||||
@@ -53,22 +55,31 @@ def create_fdf(character, fdfname):
|
||||
('Alignment', character.alignment),
|
||||
('XP', character.xp),
|
||||
# Attributes
|
||||
('ProfBonus', mod_str(character.proficiency_bonus)),
|
||||
('STRmod', str(character.strength.value)),
|
||||
('STR', character.strength.modifier_string),
|
||||
('STR', mod_str(character.strength.modifier)),
|
||||
('DEXmod ', character.dexterity.value),
|
||||
('DEX', character.dexterity.modifier_string),
|
||||
('DEX', mod_str(character.dexterity.modifier)),
|
||||
('CONmod', character.constitution.value),
|
||||
('CON', character.constitution.modifier_string),
|
||||
('CON', mod_str(character.constitution.modifier)),
|
||||
('INTmod', character.intelligence.value),
|
||||
('INT', character.intelligence.modifier_string),
|
||||
('INT', mod_str(character.intelligence.modifier)),
|
||||
('WISmod', character.wisdom.value),
|
||||
('WIS', character.wisdom.modifier_string),
|
||||
('WIS', mod_str(character.wisdom.modifier)),
|
||||
('CHamod', character.charisma.value),
|
||||
('CHA', character.charisma.modifier_string),
|
||||
('CHA', mod_str(character.charisma.modifier)),
|
||||
('AC', character.armor_class),
|
||||
('Initiative', mod_str(character.dexterity.modifier)),
|
||||
('Speed', character.speed),
|
||||
# Hit points
|
||||
('HDTotal', character.hit_dice),
|
||||
('HPMax', character.hp_max),
|
||||
|
||||
# Inventory
|
||||
('CP', character.cp),
|
||||
('SP', character.sp),
|
||||
('EP', character.ep),
|
||||
('GP', character.gp),
|
||||
('PP', character.pp),
|
||||
]
|
||||
fdf = forge_fdf("", fields, [], [], [])
|
||||
fdf_file = open(fdfname, "wb")
|
||||
@@ -87,7 +98,9 @@ def make_sheet(character_file, flatten=False):
|
||||
"""
|
||||
# Create a character from the character definition
|
||||
char_props = load_character_file(character_file)
|
||||
char = Character(**char_props)
|
||||
class_name = char_props.pop('character_class').lower().capitalize()
|
||||
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)
|
||||
@@ -106,7 +119,14 @@ def make_sheet(character_file, flatten=False):
|
||||
|
||||
|
||||
def main():
|
||||
make_sheet('examples/rogue.py')
|
||||
# Prepare an argument parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Prepare Dungeons and Dragons character sheets as PDFs')
|
||||
parser.add_argument('filename', type=str, 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 file
|
||||
make_sheet(character_file=args.filename, flatten=args.flatten)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+11
-14
@@ -1,5 +1,16 @@
|
||||
import math
|
||||
|
||||
|
||||
def mod_str(modifier):
|
||||
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
||||
if modifier > 0:
|
||||
mod_str = '+' + str(modifier)
|
||||
else:
|
||||
mod_str = str(modifier)
|
||||
return mod_str
|
||||
|
||||
|
||||
|
||||
class Stat():
|
||||
value = 10
|
||||
|
||||
@@ -10,19 +21,5 @@ class Stat():
|
||||
def modifier(self):
|
||||
return math.floor((self.value - 10) / 2)
|
||||
|
||||
@property
|
||||
def modifier_string(self):
|
||||
"""Similar to ``modifier`` but as a string.
|
||||
|
||||
This also adds a '+' if necessary.
|
||||
|
||||
"""
|
||||
mod = self.modifier
|
||||
if mod > 0:
|
||||
mod_str = '+' + str(mod)
|
||||
else:
|
||||
mod_str = str(mod)
|
||||
return mod_str
|
||||
|
||||
def __set__(self, obj, val):
|
||||
self.value = val
|
||||
|
||||
Binary file not shown.
+9
-3
@@ -1,14 +1,13 @@
|
||||
name = 'Mr. Stabby'
|
||||
player_class = 'rogue'
|
||||
character_class = 'fighter'
|
||||
player_name = 'Mark'
|
||||
background = "Criminal"
|
||||
race = "Lightfoot halfling"
|
||||
level = 3
|
||||
alignment = "Neutral"
|
||||
class_name = "Rogue"
|
||||
xp = 1984
|
||||
hp_max = 19
|
||||
hit_dice = '3d10'
|
||||
speed = 25
|
||||
|
||||
# Ability Scores
|
||||
strength = 10
|
||||
@@ -17,3 +16,10 @@ constitution = 12
|
||||
intelligence = 13
|
||||
wisdom = 9
|
||||
charisma = 16
|
||||
|
||||
# Inventory
|
||||
cp = 950
|
||||
sp = 75
|
||||
ep = 50
|
||||
gp = 120
|
||||
pp = 0
|
||||
|
||||
@@ -9,4 +9,9 @@ setup(name='dungeonsheets',
|
||||
author_email='canismarko@gmail.com',
|
||||
url='',
|
||||
packages=['dungeonsheets'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'makesheets = dungeonsheets.make_sheets:main'
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
+24
-5
@@ -13,15 +13,34 @@ class TestCharacter(TestCase):
|
||||
def test_hit_dice(self):
|
||||
# Test the getter
|
||||
char = Character()
|
||||
char.level = 2
|
||||
char.hit_dice_faces = 10
|
||||
char.hit_dice_num = 2
|
||||
self.assertEqual(char.hit_dice, '2d10')
|
||||
# Test the setter
|
||||
char.hit_dice = '3d12'
|
||||
self.assertEqual(char.hit_dice_faces, 12)
|
||||
self.assertEqual(char.hit_dice_num, 3)
|
||||
|
||||
def test_set_attrs(self):
|
||||
char = Character()
|
||||
char.set_attrs(name='Inara')
|
||||
self.assertEqual(char.name, 'Inara')
|
||||
|
||||
def test_proficiency_bonus(self):
|
||||
char = Character()
|
||||
char.level = 1
|
||||
self.assertEqual(char.proficiency_bonus, 2)
|
||||
char.level = 4
|
||||
self.assertEqual(char.proficiency_bonus, 2)
|
||||
char.level = 5
|
||||
self.assertEqual(char.proficiency_bonus, 3)
|
||||
char.level = 8
|
||||
self.assertEqual(char.proficiency_bonus, 3)
|
||||
char.level = 9
|
||||
self.assertEqual(char.proficiency_bonus, 4)
|
||||
char.level = 12
|
||||
self.assertEqual(char.proficiency_bonus, 4)
|
||||
char.level = 13
|
||||
self.assertEqual(char.proficiency_bonus, 5)
|
||||
char.level = 16
|
||||
self.assertEqual(char.proficiency_bonus, 5)
|
||||
char.level = 17
|
||||
self.assertEqual(char.proficiency_bonus, 6)
|
||||
char.level = 20
|
||||
self.assertEqual(char.proficiency_bonus, 6)
|
||||
|
||||
@@ -3,14 +3,20 @@ import os
|
||||
|
||||
from dungeonsheets import make_sheets, character
|
||||
|
||||
EG_DIR = os.path.abspath(os.path.join(os.path.split(__file__)[0], '../examples/'))
|
||||
CHARFILE = os.path.join(EG_DIR, 'rogue.py')
|
||||
|
||||
class CharacterFileTestCase(unittest.TestCase):
|
||||
def test_load_character_file(self):
|
||||
charfile = 'examples/rogue.py'
|
||||
charfile = CHARFILE
|
||||
result = make_sheets.load_character_file(charfile)
|
||||
self.assertEqual(result['strength'], 10)
|
||||
|
||||
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()
|
||||
|
||||
+5
-9
@@ -4,6 +4,11 @@ from dungeonsheets import stats
|
||||
|
||||
class TestStats(TestCase):
|
||||
|
||||
def test_mod_str(self):
|
||||
self.assertEqual(stats.mod_str(-3), '-3')
|
||||
self.assertEqual(stats.mod_str(0), '0')
|
||||
self.assertEqual(stats.mod_str(2), '+2')
|
||||
|
||||
def test_modifier(self):
|
||||
ranges = [
|
||||
((1,), -5),
|
||||
@@ -31,15 +36,6 @@ class TestStats(TestCase):
|
||||
msg = f"Stat {value} doesn't produce modifier {target} ({stat.modifier})"
|
||||
self.assertEqual(stat.modifier, target, msg)
|
||||
|
||||
def test_modfifier_string(self):
|
||||
stat = stats.Stat()
|
||||
stat.value = 5
|
||||
self.assertEqual(stat.modifier_string, '-3')
|
||||
stat.value = 10
|
||||
self.assertEqual(stat.modifier_string, '0')
|
||||
stat.value = 15
|
||||
self.assertEqual(stat.modifier_string, '+2')
|
||||
|
||||
def test_setter(self):
|
||||
"""Verify that this class works as a data descriptor."""
|
||||
# Set up a dummy class
|
||||
|
||||
Reference in New Issue
Block a user