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 = ""
|
name = ""
|
||||||
class_name = ""
|
class_name = ""
|
||||||
player_name = ""
|
player_name = ""
|
||||||
|
background = ""
|
||||||
level = 1
|
level = 1
|
||||||
alignment = 'true neutral'
|
alignment = "Neutral"
|
||||||
|
race = "Human"
|
||||||
xp = 0
|
xp = 0
|
||||||
armor_class = 10
|
|
||||||
speed = 30 # In feet
|
speed = 30 # In feet
|
||||||
# Hit points
|
# Hit points
|
||||||
hp_max = 10
|
hp_max = 10
|
||||||
hit_dice_num = 1
|
hit_dice_faces = 2
|
||||||
hit_dice_faces = 8
|
|
||||||
# Base stats (ability scores
|
# Base stats (ability scores
|
||||||
strength = Stat()
|
strength = Stat()
|
||||||
dexterity = Stat()
|
dexterity = Stat()
|
||||||
@@ -32,6 +32,12 @@ class Character():
|
|||||||
intelligence = Stat()
|
intelligence = Stat()
|
||||||
wisdom = Stat()
|
wisdom = Stat()
|
||||||
charisma = Stat()
|
charisma = Stat()
|
||||||
|
# Inventory
|
||||||
|
cp = 0
|
||||||
|
sp = 0
|
||||||
|
ep = 0
|
||||||
|
gp = 0
|
||||||
|
pp = 0
|
||||||
|
|
||||||
def __init__(self, **attrs):
|
def __init__(self, **attrs):
|
||||||
"""Takes a bunch of attrs and passes them to ``set_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.
|
"""What type and how many dice to use for re-gaining hit points.
|
||||||
|
|
||||||
To change, set hit_dice_num and hit_dice_faces."""
|
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
|
@property
|
||||||
def hit_dice(self, val):
|
def proficiency_bonus(self):
|
||||||
dice = read_dice_str(val)
|
if self.level < 5:
|
||||||
self.hit_dice_faces = dice.faces
|
prof = 2
|
||||||
self.hit_dice_num = dice.num
|
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
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import argparse
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from fdfgen import forge_fdf
|
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
|
"""Program to take character definitions and build a PDF of the
|
||||||
character sheet."""
|
character sheet."""
|
||||||
@@ -53,22 +55,31 @@ def create_fdf(character, fdfname):
|
|||||||
('Alignment', character.alignment),
|
('Alignment', character.alignment),
|
||||||
('XP', character.xp),
|
('XP', character.xp),
|
||||||
# Attributes
|
# Attributes
|
||||||
|
('ProfBonus', mod_str(character.proficiency_bonus)),
|
||||||
('STRmod', str(character.strength.value)),
|
('STRmod', str(character.strength.value)),
|
||||||
('STR', character.strength.modifier_string),
|
('STR', mod_str(character.strength.modifier)),
|
||||||
('DEXmod ', character.dexterity.value),
|
('DEXmod ', character.dexterity.value),
|
||||||
('DEX', character.dexterity.modifier_string),
|
('DEX', mod_str(character.dexterity.modifier)),
|
||||||
('CONmod', character.constitution.value),
|
('CONmod', character.constitution.value),
|
||||||
('CON', character.constitution.modifier_string),
|
('CON', mod_str(character.constitution.modifier)),
|
||||||
('INTmod', character.intelligence.value),
|
('INTmod', character.intelligence.value),
|
||||||
('INT', character.intelligence.modifier_string),
|
('INT', mod_str(character.intelligence.modifier)),
|
||||||
('WISmod', character.wisdom.value),
|
('WISmod', character.wisdom.value),
|
||||||
('WIS', character.wisdom.modifier_string),
|
('WIS', mod_str(character.wisdom.modifier)),
|
||||||
('CHamod', character.charisma.value),
|
('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
|
# Hit points
|
||||||
('HDTotal', character.hit_dice),
|
('HDTotal', character.hit_dice),
|
||||||
('HPMax', character.hp_max),
|
('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 = forge_fdf("", fields, [], [], [])
|
||||||
fdf_file = open(fdfname, "wb")
|
fdf_file = open(fdfname, "wb")
|
||||||
@@ -87,7 +98,9 @@ def make_sheet(character_file, flatten=False):
|
|||||||
"""
|
"""
|
||||||
# Create a character from the character definition
|
# Create a character from the character definition
|
||||||
char_props = load_character_file(character_file)
|
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
|
# Set the fields in the FDF
|
||||||
fdfname = os.path.splitext(character_file)[0] + '.fdf'
|
fdfname = os.path.splitext(character_file)[0] + '.fdf'
|
||||||
create_fdf(character=char, fdfname=fdfname)
|
create_fdf(character=char, fdfname=fdfname)
|
||||||
@@ -106,7 +119,14 @@ def make_sheet(character_file, flatten=False):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+11
-14
@@ -1,5 +1,16 @@
|
|||||||
import math
|
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():
|
class Stat():
|
||||||
value = 10
|
value = 10
|
||||||
|
|
||||||
@@ -10,19 +21,5 @@ class Stat():
|
|||||||
def modifier(self):
|
def modifier(self):
|
||||||
return math.floor((self.value - 10) / 2)
|
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):
|
def __set__(self, obj, val):
|
||||||
self.value = val
|
self.value = val
|
||||||
|
|||||||
Binary file not shown.
+9
-3
@@ -1,14 +1,13 @@
|
|||||||
name = 'Mr. Stabby'
|
name = 'Mr. Stabby'
|
||||||
player_class = 'rogue'
|
character_class = 'fighter'
|
||||||
player_name = 'Mark'
|
player_name = 'Mark'
|
||||||
background = "Criminal"
|
background = "Criminal"
|
||||||
race = "Lightfoot halfling"
|
race = "Lightfoot halfling"
|
||||||
level = 3
|
level = 3
|
||||||
alignment = "Neutral"
|
alignment = "Neutral"
|
||||||
class_name = "Rogue"
|
|
||||||
xp = 1984
|
xp = 1984
|
||||||
hp_max = 19
|
hp_max = 19
|
||||||
hit_dice = '3d10'
|
speed = 25
|
||||||
|
|
||||||
# Ability Scores
|
# Ability Scores
|
||||||
strength = 10
|
strength = 10
|
||||||
@@ -17,3 +16,10 @@ constitution = 12
|
|||||||
intelligence = 13
|
intelligence = 13
|
||||||
wisdom = 9
|
wisdom = 9
|
||||||
charisma = 16
|
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',
|
author_email='canismarko@gmail.com',
|
||||||
url='',
|
url='',
|
||||||
packages=['dungeonsheets'],
|
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):
|
def test_hit_dice(self):
|
||||||
# Test the getter
|
# Test the getter
|
||||||
char = Character()
|
char = Character()
|
||||||
|
char.level = 2
|
||||||
char.hit_dice_faces = 10
|
char.hit_dice_faces = 10
|
||||||
char.hit_dice_num = 2
|
|
||||||
self.assertEqual(char.hit_dice, '2d10')
|
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):
|
def test_set_attrs(self):
|
||||||
char = Character()
|
char = Character()
|
||||||
char.set_attrs(name='Inara')
|
char.set_attrs(name='Inara')
|
||||||
self.assertEqual(char.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
|
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):
|
class CharacterFileTestCase(unittest.TestCase):
|
||||||
def test_load_character_file(self):
|
def test_load_character_file(self):
|
||||||
charfile = 'examples/rogue.py'
|
charfile = CHARFILE
|
||||||
result = make_sheets.load_character_file(charfile)
|
result = make_sheets.load_character_file(charfile)
|
||||||
self.assertEqual(result['strength'], 10)
|
self.assertEqual(result['strength'], 10)
|
||||||
|
|
||||||
class FDFTestCase(unittest.TestCase):
|
class FDFTestCase(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
if os.path.exists('temp.fdf'):
|
||||||
|
os.remove('temp.fdf')
|
||||||
|
|
||||||
def test_create_fdf(self):
|
def test_create_fdf(self):
|
||||||
fdfname = 'temp.fdf'
|
fdfname = 'temp.fdf'
|
||||||
char = character.Character()
|
char = character.Character()
|
||||||
|
|||||||
+5
-9
@@ -4,6 +4,11 @@ from dungeonsheets import stats
|
|||||||
|
|
||||||
class TestStats(TestCase):
|
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):
|
def test_modifier(self):
|
||||||
ranges = [
|
ranges = [
|
||||||
((1,), -5),
|
((1,), -5),
|
||||||
@@ -31,15 +36,6 @@ class TestStats(TestCase):
|
|||||||
msg = f"Stat {value} doesn't produce modifier {target} ({stat.modifier})"
|
msg = f"Stat {value} doesn't produce modifier {target} ({stat.modifier})"
|
||||||
self.assertEqual(stat.modifier, target, msg)
|
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):
|
def test_setter(self):
|
||||||
"""Verify that this class works as a data descriptor."""
|
"""Verify that this class works as a data descriptor."""
|
||||||
# Set up a dummy class
|
# Set up a dummy class
|
||||||
|
|||||||
Reference in New Issue
Block a user