added first version of multiclass options. Need to test

This commit is contained in:
Ben Cook
2018-12-18 22:30:40 -05:00
parent 274f34fe88
commit 6c634c0429
8 changed files with 774 additions and 424 deletions
+2
View File
@@ -0,0 +1,2 @@
Add multiclassing hit dice
Add Character.save() option to save to text file
+1 -1
View File
@@ -1,3 +1,3 @@
from . import weapons, character from . import weapons, character
__VERSION__ = "0.6.1" __VERSION__ = "0.7.0"
+3
View File
@@ -3,6 +3,9 @@ class Background():
skill_proficiencies = () skill_proficiencies = ()
languages = () languages = ()
def __str__(self):
return self.name
class Acolyte(Background): class Acolyte(Background):
name = "Acolyte" name = "Acolyte"
+119 -332
View File
@@ -1,37 +1,36 @@
"""Tools for describing a player character.""" """Tools for describing a player character."""
import re import re
import os
import warnings import warnings
import math from . import exceptions
import importlib.util
from .stats import Ability, Skill, findattr from .stats import Ability, Skill, findattr
from .dice import read_dice_str from .dice import read_dice_str
from . import weapons, race, spells, armor, monsters, exceptions from . import (weapons, race, background, spells, armor, monsters,
exceptions, classes)
from .weapons import Weapon from .weapons import Weapon
from .armor import Armor, NoArmor, Shield, NoShield from .armor import Armor, NoArmor, Shield, NoShield
dice_re = re.compile('(\d+)d(\d+)') dice_re = re.compile('(\d+)d(\d+)')
__all__ = ('Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk',
'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', 'Wizard', )
class Character(): class Character():
"""A generic player character. Intended to be subclasses by the """A generic player character.
various classes.
""" """
# General attirubtes # General attirubtes
name = "" name = ""
class_name = "" class_name = ""
player_name = "" player_name = ""
background = ""
level = 1
alignment = "Neutral" alignment = "Neutral"
class_list = []
race = None race = None
background = None
xp = 0 xp = 0
# Hit points # Hit points
hp_max = 10 hp_max = 10
hit_dice_faces = 2
# Base stats (ability scores) # Base stats (ability scores)
strength = Ability() strength = Ability()
dexterity = Ability() dexterity = Ability()
@@ -103,6 +102,31 @@ class Character():
def speed(self): def speed(self):
return getattr(self.race, 'speed', 30) return getattr(self.race, 'speed', 30)
@property
def level(self):
return sum(c.class_level for c in self.class_list)
@property
def primary_class(self):
# for now, assume first class given must be primary class
return self.class_list[0]
@property
def saving_throw_proficiencies(self):
return self.primary_class.saving_throw_proficiencies
@property
def spellcasting_classes(self):
return [c for c in self.class_list if c.is_spellcaster]
@property
def is_spellcaster(self):
return (len(self.spellcasting_classes) > 0)
def spell_slots(self, spell_level):
# TODO: Update this for Multiclassing
return self.spellcasting_classes[0].spell_slots(spell_level)
def set_attrs(self, **attrs): def set_attrs(self, **attrs):
"""Bulk setting of attributes. Useful for loading a character from a """Bulk setting of attributes. Useful for loading a character from a
dictionary.""" dictionary."""
@@ -116,6 +140,9 @@ class Character():
elif attr == 'race': elif attr == 'race':
MyRace = findattr(race, val) MyRace = findattr(race, val)
self.race = MyRace() self.race = MyRace()
elif attr == 'background':
MyBackground = findattr(background, val)
self.race = MyBackground()
elif attr == 'armor': elif attr == 'armor':
self.wear_armor(val) self.wear_armor(val)
elif attr == 'shield': elif attr == 'shield':
@@ -148,25 +175,14 @@ class Character():
# Lookup general attributes # Lookup general attributes
setattr(self, attr, val) setattr(self, attr, val)
@property def spell_save_dc(self, class_type):
def is_spellcaster(self): ability_mod = getattr(self, class_type.spellcasting_ability).modifier
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) return (8 + self.proficiency_bonus + ability_mod)
@property def spell_attack_bonus(self, class_type):
def spell_attack_bonus(self): ability_mod = getattr(self, class_type.spellcasting_ability).modifier
ability_mod = getattr(self, self.spellcasting_ability).modifier
return (self.proficiency_bonus + ability_mod) 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): def is_proficient(self, weapon: Weapon):
"""Is the character proficient with this item? """Is the character proficient with this item?
@@ -179,7 +195,8 @@ class Character():
""" """
all_proficiencies = tuple(self.weapon_proficiencies) all_proficiencies = tuple(self.weapon_proficiencies)
all_proficiencies += tuple(getattr(self.race, 'weapon_proficiencies', tuple())) all_proficiencies += tuple(getattr(self.race, 'weapon_proficiencies',
tuple()))
is_proficient = any((isinstance(weapon, W) for W in all_proficiencies)) is_proficient = any((isinstance(weapon, W) for W in all_proficiencies))
return is_proficient return is_proficient
@@ -292,6 +309,8 @@ class Character():
@property @property
def armor_class(self): def armor_class(self):
"""Armor class, including contributions from worn armor and shield.""" """Armor class, including contributions from worn armor and shield."""
# ## TODO:
# Implement AC functions by class
# Retrieve current armor (or a generic armor substitute) # Retrieve current armor (or a generic armor substitute)
armor = self.armor if self.armor is not None else NoArmor() armor = self.armor if self.armor is not None else NoArmor()
shield = self.shield if self.shield is not None else NoShield() shield = self.shield if self.shield is not None else NoShield()
@@ -304,318 +323,86 @@ class Character():
ac = armor.base_armor_class + shield.base_armor_class + modifier ac = armor.base_armor_class + shield.base_armor_class + modifier
return ac return ac
@classmethod
class Barbarian(Character): def load(cls, character_file):
class_name = 'Barbarian' # Create a character from the character definition
hit_dice_faces = 12 char_props = read_character_file(character_file)
saving_throw_proficiencies = ('strength', 'constitution') classes_levels = char_props.pop('classes_levels', [])
_proficiencies_text = ('light armor', 'medium armor', 'shields', if isinstance(classes_levels, str):
'simple weapons', 'martial weapons') classes_levels = [classes_levels]
weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons) subclasses = char_props.pop('subclasses', [])
class_skill_choices = ('Animal Handling', 'Athletics', if isinstance(subclasses, str):
'Intimidation', 'Nature', 'Perception', 'Survival') subclasses = [subclasses]
assert len(classes_levels) == len(subclasses), (
'the length of classes_levels {:d} does not match length of '
class Bard(Character): 'subclasses {:d}'.format(len(classes_levels), len(subclasses)))
class_name = 'Bard' class_list = []
hit_dice_faces = 8 for cl, sub in zip(classes_levels, subclasses):
saving_throw_proficiencies = ('dexterity', 'charisma')
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
class_skill_choices = ('Acrobatics', 'Animal Handling', 'Arcana',
'Athletics', 'Deception', 'History', 'Insight',
'Intimidation', 'Investigation', 'Medicine', 'Nature',
'Perception', 'Performance', 'Persuasion', 'Religion',
'Sleight of Hand', 'Stealth', 'Survival')
num_skill_choices = 3
class Cleric(Character):
class_name = 'Cleric'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficiencies = weapons.simple_weapons
class_skill_choices = ('History', 'Insight', 'Medicine',
'Persuasion', 'Religion')
class Druid(Character):
class_name = 'Druid'
circle = "" # Moon, land
_wild_shapes = ()
hit_dice_faces = 8
saving_throw_proficiencies = ('intelligence', 'wisdom')
spellcasting_ability = 'wisdom'
languages = 'Druidic'
_proficiencies_text = (
'Light armor', 'medium armor',
'shields (druids will not wear armor or use shields made of metal)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficiencies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace, weapons.Quarterstaff,
weapons.Scimitar, weapons.Sickle, weapons.Sling, weapons.Spear)
class_skill_choices = ('Arcana', 'Animal Handling', 'Insight',
'Medicine', 'Nature', 'Perception', 'Religion', 'Survival')
spell_slots_by_level = {
1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (3, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (3, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (4, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (4, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (4, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
@property
def all_wild_shapes(self):
"""Return all wild shapes, regardless of validity."""
return self._wild_shapes
@property
def wild_shapes(self):
"""Return a list of valid wild shapes for this Druid."""
valid_shapes = []
for shape in self._wild_shapes:
# Check if shape can be transformed into
if self.can_assume_shape(shape):
valid_shapes.append(shape)
return valid_shapes
@wild_shapes.setter
def wild_shapes(self, new_shapes):
actual_shapes = []
# Retrieve the actual monster classes if possible
for shape in new_shapes:
if isinstance(shape, monsters.Monster):
# Already a monster shape so just add it as is
new_shape = shape
else:
# Not already a monster so see if we can find one
try: try:
NewMonster = findattr(monsters, shape) c, lvl = cl.strip().split(' ') # " wizard 3 " => "wizard", "3"
new_shape = NewMonster() except ValueError:
raise ValueError(
'classes_levels not properly formatted. Each entry should '
'be formatted \"class level\", but got {:s}'.format(cl))
try:
this_class = getattr(classes, c)
this_level = int(lvl)
except AttributeError: except AttributeError:
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``' raise AttributeError(
raise exceptions.MonsterError(msg) 'class was not recognized from classes.py: {:s}'.format(c))
actual_shapes.append(new_shape) except ValueError:
# Save the updated list for later raise ValueError(
self._wild_shapes = actual_shapes 'level was not recognizable as an int: {:s}'.format(lvl))
class_list += [this_class(this_level, subclass=sub)]
# accept backwards compatability for single-class characters
if len(class_list) == 0:
class_name = char_props.pop('character_class').lower().capitalize()
class_level = char_props.pop('level')
CharClass = getattr(classes, class_name)
class_list = [CharClass(class_level)]
char_props['class_list'] = class_list
# Create the character with loaded properties
char = cls(**char_props)
return char
def can_assume_shape(self, shape: monsters.Monster)-> bool:
"""Determine if a given shape meets the requirements for transforming.
See Pg 66 of player's handbook. def read_character_file(filename):
"""Create a character object from the given definition file.
The definition file should be an importable python file, filled
with variables describing the character.
Parameters Parameters
========== ----------
shape filename : str
A monster that the Druid wishes to transform into. The path to the file that will be imported.
Returns
=======
can_assume
True if the monster meets the C/R, swim and flying speed
restrictions.
""" """
# Determine acceptable states based on druid level # Parse the file name
if self.level < 2: dir_, fname = os.path.split(os.path.abspath(filename))
max_cr = -1 module_name, ext = os.path.splitext(fname)
max_swim = 0 if ext != '.py':
max_fly = 0 raise ValueError(f"Character definition {filename} is not a python file.")
elif self.level < 4: # Check if this file contains the version string
max_cr = 1/4 version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
max_swim = 0 with open(filename, mode='r') as f:
max_fly = 0 version = None
elif self.level < 8: for line in f:
max_cr = 1/2 match = version_re.match(line)
max_swim = None if match:
max_fly = 0 version = match.group(1)
else: break
max_cr = 1 if version is None:
max_swim = None # Not a valid DND character file
max_fly = None raise exceptions.CharacterFileFormatError(
# Make adjustments for moon cirlce druids f"No ``dungeonsheets_version = `` entry in `{filename}`.")
if self.circle.lower() == "moon": # Import the module to extract the information
if 2 <= self.level < 6: spec = importlib.util.spec_from_file_location('module', filename)
max_cr = 1 module = importlib.util.module_from_spec(spec)
elif self.level >= 6: spec.loader.exec_module(module)
max_cr = math.floor(self.level / 3) # Prepare a list of properties for this character
# Check if the beast shape can be assumed char_props = {}
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr) for prop_name in dir(module):
valid_swim = (max_swim is None or shape.swim_speed <= max_swim) if prop_name[0:2] != '__':
valid_fly = (max_fly is None or shape.fly_speed <= max_fly) char_props[prop_name] = getattr(module, prop_name)
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly return char_props
return can_assume
@property
def spells(self):
return tuple(S() for S in self.spells_prepared)
@spells.setter
def spells(self, val):
if len(val) > 0:
warnings.warn("Druids cannot learn spells, "
"use ``spells_prepared`` instead.",
RuntimeWarning)
class Fighter(Character):
class_name = 'Fighter'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armar', 'shields', 'simple weapons', 'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Acrobatics', 'Animal Handling',
'Athletics', 'History', 'Insight', 'Intimidation', 'Perception',
'Survival')
class Monk(Character):
class_name = 'Monk'
hit_dice_faces = 8
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficiencies = (weapons.Shortsword,) + weapons.simple_weapons
class_skill_choices = ('Acrobatics', 'Athletics', 'History', 'Insight', 'Religion', 'Stealth')
class Paladin(Character):
class_name = 'Paladin'
hit_dice_faces = 10
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ("Athletics", 'Insight', 'Intimidation',
'Medicine', 'Persuasion', 'Religion')
class Ranger(Character):
class_name = 'Ranger'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Animal Handling', 'Athletics', 'Insight',
'Investigation', 'Nature', 'Perception', 'Stealth', 'Survival')
num_skill_choices = 3
class Rogue(Character):
class_name = 'Rogue'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'intelligence')
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficiencies = (weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) + weapons.simple_weapons
class_skill_choices = ('Acrobatics', 'Athletics', 'Deception',
'Insight', 'Intimidation', 'Investigation', 'Perception',
'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth')
class Sorceror(Character):
class_name = 'Sorceror'
hit_dice_faces = 6
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'Deception', 'Insight',
'Intimidation' ,'Persuasion', 'Religion')
class Warlock(Character):
class_name = 'Warlock'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
class_skill_choices = ('Arcana', 'Deception', 'History',
'Intimidation', 'Investigation', 'Nature', 'Religion')
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):
class_name = 'Wizard'
hit_dice_faces = 6
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'History', 'Investigation',
'Medicine', 'Religion')
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),
}
+475
View File
@@ -0,0 +1,475 @@
__all__ = ('Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk',
'Paladin', 'Ranger', 'Rogue', 'Sorceror', 'Warlock', 'Wizard', )
from .stats import findattr
from . import (weapons, monsters, exceptions)
import math
import warnings
class CharClass():
"""
A generic Character Class (not to be confused with builtin class)
"""
class_name = ""
class_level = 1
hit_dice_faces = None
_proficiencies_text = ()
weapon_proficiencies = ()
multiclass_weapon_proficiencies = ()
languages = ()
class_skill_choices = ()
num_skill_choices = 2
spellcasing_ability = None
spell_slots_by_level = None
subclass = None
class_features_by_level = {lvl: () for lvl in range(1, 21)}
def __init__(self, level, subclass=None, **params):
self.class_level = level
self.subclass = subclass
for k, v in params:
setattr(self, k, v)
@property
def class_features(self):
features = ()
for lvl in range(1, self.class_level+1):
features += tuple(self.class_features_by_level[lvl])
if self.subclass is not None:
features += tuple(self.subclass.features_by_level[lvl])
return features
@property
def is_spellcaster(self):
result = (self.spellcasting_ability is not None)
return result
def spell_slots(self, spell_level):
"""How many spells slots are available for this spell level."""
if self.spell_slots_by_level is None:
return 0
else:
return self.spell_slots_by_level[self.class_level][spell_level]
class Barbarian(CharClass):
class_name = 'Barbarian'
hit_dice_faces = 12
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'simple weapons', 'martial weapons')
weapon_proficiencies = (weapons.simple_weapons + weapons.martial_weapons)
class_skill_choices = ('Animal Handling', 'Athletics',
'Intimidation', 'Nature', 'Perception', 'Survival')
class Bard(CharClass):
class_name = 'Bard'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'charisma')
_proficiencies_text = (
'Light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', 'three musical instruments of your choice')
weapon_proficiencies = ((weapons.HandCrossbow, weapons.Longsword,
weapons.Rapier, weapons.Shortsword) +
weapons.simple_weapons)
class_skill_choices = ('Acrobatics', 'Animal Handling', 'Arcana',
'Athletics', 'Deception', 'History', 'Insight',
'Intimidation', 'Investigation', 'Medicine',
'Nature', 'Perception', 'Performance', 'Persuasion',
'Religion', 'Sleight of Hand', 'Stealth',
'Survival')
num_skill_choices = 3
spellcasting_ability = 'charisma'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (3, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (4, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (4, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (4, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (4, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
class Cleric(CharClass):
class_name = 'Cleric'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('light armor', 'medium armor', 'shields',
'all simple weapons')
weapon_proficiencies = weapons.simple_weapons
class_skill_choices = ('History', 'Insight', 'Medicine',
'Persuasion', 'Religion')
spellcasting_ability = 'wisdom'
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),
}
class Druid(CharClass):
class_name = 'Druid'
circle = "" # Moon, land
_wild_shapes = ()
hit_dice_faces = 8
saving_throw_proficiencies = ('intelligence', 'wisdom')
spellcasting_ability = 'wisdom'
languages = 'Druidic'
_proficiencies_text = (
'Light armor', 'medium armor',
'shields (druids will not wear armor or use shields made of metal)',
'clubs', 'daggers', 'darts', 'javelins', 'maces', 'quarterstaffs',
'scimitars', 'sickles', 'slings', 'spears')
weapon_proficiencies = (weapons.Club, weapons.Dagger, weapons.Dart,
weapons.Javelin, weapons.Mace,
weapons.Quarterstaff, weapons.Scimitar,
weapons.Sickle, weapons.Sling, weapons.Spear)
class_skill_choices = ('Arcana', 'Animal Handling', 'Insight',
'Medicine', 'Nature', 'Perception', 'Religion',
'Survival')
spell_slots_by_level = {
1: (2, 2, 0, 0, 0, 0, 0, 0, 0, 0),
2: (2, 3, 0, 0, 0, 0, 0, 0, 0, 0),
3: (2, 4, 2, 0, 0, 0, 0, 0, 0, 0),
4: (3, 4, 3, 0, 0, 0, 0, 0, 0, 0),
5: (3, 4, 3, 2, 0, 0, 0, 0, 0, 0),
6: (3, 4, 3, 3, 0, 0, 0, 0, 0, 0),
7: (3, 4, 3, 3, 1, 0, 0, 0, 0, 0),
8: (3, 4, 3, 3, 2, 0, 0, 0, 0, 0),
9: (3, 4, 3, 3, 3, 1, 0, 0, 0, 0),
10: (4, 4, 3, 3, 3, 2, 0, 0, 0, 0),
11: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
12: (4, 4, 3, 3, 3, 2, 1, 0, 0, 0),
13: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
14: (4, 4, 3, 3, 3, 2, 1, 1, 0, 0),
15: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
16: (4, 4, 3, 3, 3, 2, 1, 1, 1, 0),
17: (4, 4, 3, 3, 3, 2, 1, 1, 1, 1),
18: (4, 4, 3, 3, 3, 3, 1, 1, 1, 1),
19: (4, 4, 3, 3, 3, 3, 2, 1, 1, 1),
20: (4, 4, 3, 3, 3, 3, 2, 2, 1, 1),
}
@property
def all_wild_shapes(self):
"""Return all wild shapes, regardless of validity."""
return self._wild_shapes
@property
def wild_shapes(self):
"""Return a list of valid wild shapes for this Druid."""
valid_shapes = []
for shape in self._wild_shapes:
# Check if shape can be transformed into
if self.can_assume_shape(shape):
valid_shapes.append(shape)
return valid_shapes
@wild_shapes.setter
def wild_shapes(self, new_shapes):
actual_shapes = []
# Retrieve the actual monster classes if possible
for shape in new_shapes:
if isinstance(shape, monsters.Monster):
# Already a monster shape so just add it as is
new_shape = shape
else:
# Not already a monster so see if we can find one
try:
NewMonster = findattr(monsters, shape)
new_shape = NewMonster()
except AttributeError:
msg = f'Wild shape "{shape}" not found. Please add it to ``monsters.py``'
raise exceptions.MonsterError(msg)
actual_shapes.append(new_shape)
# Save the updated list for later
self._wild_shapes = actual_shapes
def can_assume_shape(self, shape: monsters.Monster)-> bool:
"""Determine if a given shape meets the requirements for transforming.
See Pg 66 of player's handbook.
Parameters
==========
shape
A monster that the Druid wishes to transform into.
Returns
=======
can_assume
True if the monster meets the C/R, swim and flying speed
restrictions.
"""
# Determine acceptable states based on druid level
if self.class_level < 2:
max_cr = -1
max_swim = 0
max_fly = 0
elif self.class_level < 4:
max_cr = 1/4
max_swim = 0
max_fly = 0
elif self.class_level < 8:
max_cr = 1/2
max_swim = None
max_fly = 0
else:
max_cr = 1
max_swim = None
max_fly = None
# Make adjustments for moon cirlce druids
if self.circle.lower() == "moon":
if 2 <= self.class_level < 6:
max_cr = 1
elif self.class_level >= 6:
max_cr = math.floor(self.class_level / 3)
# Check if the beast shape can be assumed
valid_cr = (max_cr is None or shape.challenge_rating <= max_cr)
valid_swim = (max_swim is None or shape.swim_speed <= max_swim)
valid_fly = (max_fly is None or shape.fly_speed <= max_fly)
can_assume = shape.is_beast and valid_cr and valid_swim and valid_fly
return can_assume
@property
def spells(self):
return tuple(S() for S in self.spells_prepared)
@spells.setter
def spells(self, val):
if len(val) > 0:
warnings.warn("Druids cannot learn spells, "
"use ``spells_prepared`` instead.",
RuntimeWarning)
class Fighter(CharClass):
class_name = 'Fighter'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'constitution')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Acrobatics', 'Animal Handling',
'Athletics', 'History', 'Insight', 'Intimidation',
'Perception', 'Survival')
class Monk(CharClass):
class_name = 'Monk'
hit_dice_faces = 8
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = (
'simple weapons', 'shortswords',
"one type of artisan's tools or one musical instrument")
weapon_proficiencies = (weapons.Shortsword,) + weapons.simple_weapons
class_skill_choices = ('Acrobatics', 'Athletics', 'History', 'Insight',
'Religion', 'Stealth')
class Paladin(CharClass):
class_name = 'Paladin'
hit_dice_faces = 10
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ('All armor', 'shields', 'simple weapons',
'martial weapons')
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ("Athletics", 'Insight', 'Intimidation',
'Medicine', 'Persuasion', 'Religion')
spellcasting_ability = 'charisma'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
2: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
4: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
5: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
6: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
7: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
8: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
9: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
10: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
11: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
13: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
14: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
15: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
16: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
17: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
18: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
19: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
}
class Ranger(CharClass):
class_name = 'Ranger'
hit_dice_faces = 10
saving_throw_proficiencies = ('strength', 'dexterity')
_proficiencies_text = ("light armor", "medium armor", "shields",
"simple weapons", "martial weapons")
weapon_proficiencies = weapons.simple_weapons + weapons.martial_weapons
class_skill_choices = ('Animal Handling', 'Athletics', 'Insight',
'Investigation', 'Nature', 'Perception', 'Stealth',
'Survival')
num_skill_choices = 3
spellcasting_ability = 'wisdom'
spell_slots_by_level = {
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
1: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
2: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
3: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
4: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
5: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
6: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
7: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
8: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
9: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
10: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
11: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
12: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
13: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
14: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
15: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
16: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
17: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
18: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
19: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
20: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
}
class Rogue(CharClass):
class_name = 'Rogue'
hit_dice_faces = 8
saving_throw_proficiencies = ('dexterity', 'intelligence')
_proficiencies_text = (
'light armor', 'simple weapons', 'hand crossbows', 'longswords',
'rapiers', 'shortswords', "thieves' tools")
weapon_proficiencies = weapons.simple_weapons + (
weapons.HandCrossbow, weapons.Longsword, weapons.Rapier,
weapons.Shortsword)
class_skill_choices = ('Acrobatics', 'Athletics', 'Deception',
'Insight', 'Intimidation', 'Investigation',
'Perception', 'Performance', 'Persuasion',
'Sleight of Hand', 'Stealth')
class Sorceror(CharClass):
class_name = 'Sorceror'
hit_dice_faces = 6
saving_throw_proficiencies = ('constitution', 'charisma')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'Deception', 'Insight',
'Intimidation', 'Persuasion', 'Religion')
class Warlock(CharClass):
class_name = 'Warlock'
hit_dice_faces = 8
saving_throw_proficiencies = ('wisdom', 'charisma')
_proficiencies_text = ("light Armor", "simple weapons")
class_skill_choices = ('Arcana', 'Deception', 'History',
'Intimidation', 'Investigation', 'Nature',
'Religion')
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(CharClass):
class_name = 'Wizard'
hit_dice_faces = 6
saving_throw_proficiencies = ('intelligence', 'wisdom')
_proficiencies_text = ('daggers', 'darts', 'slings',
'quarterstaffs', 'light crossbows')
weapon_proficiencies = (weapons.Dagger, weapons.Dart,
weapons.Sling, weapons.Quarterstaff,
weapons.LightCrossbow)
class_skill_choices = ('Arcana', 'History', 'Investigation',
'Medicine', 'Religion')
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),
}
+4
View File
@@ -0,0 +1,4 @@
__all__ = ['load_character_file']
+53 -83
View File
@@ -14,8 +14,8 @@ from fdfgen import forge_fdf
import pdfrw import pdfrw
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from dungeonsheets import character, exceptions from . import character, exceptions, classes
from dungeonsheets.stats import mod_str from .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
@@ -67,64 +67,23 @@ def text_box(string):
return new_string return new_string
def load_character_file(filename): def create_druid_shapes_pdf(char, basename):
"""Create a character object from the given definition file.
The definition file should be an importable python file, filled
with variables describing the character.
Parameters
----------
filename : str
The path to the file that will be imported.
"""
# Parse the file name
dir_, fname = os.path.split(os.path.abspath(filename))
module_name, ext = os.path.splitext(fname)
if ext != '.py':
raise ValueError(f"Character definition {filename} is not a python file.")
# Check if this file contains the version string
version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
with open(filename, mode='r') as f:
version = None
for line in f:
match = version_re.match(line)
if match:
version = match.group(1)
break
if version is None:
# Not a valid DND character file
raise exceptions.CharacterFileFormatError(
f"No ``dungeonsheets_version = `` entry in `{filename}`.")
# Import the module to extract the information
spec = importlib.util.spec_from_file_location('module', filename)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Prepare a list of properties for this character
char_props = {}
for prop_name in dir(module):
if prop_name[0:2] != '__':
char_props[prop_name] = getattr(module, prop_name)
return char_props
def create_druid_shapes_pdf(character, basename):
template = jinja_env.get_template('druid_shapes_template.tex') template = jinja_env.get_template('druid_shapes_template.tex')
return create_latex_pdf(character, basename, template) return create_latex_pdf(char, basename, template)
def create_spellbook_pdf(character, basename): def create_spellbook_pdf(char, basename):
template = jinja_env.get_template('spellbook_template.tex') template = jinja_env.get_template('spellbook_template.tex')
return create_latex_pdf(character, basename, template) return create_latex_pdf(char, basename, template)
def create_latex_pdf(character, basename, template): def create_latex_pdf(char, basename, template):
tex = template.render(character=character) tex = template.render(character=char)
# Create tex document # Create tex document
tex_file = f'{basename}.tex' tex_file = f'{basename}.tex'
with open(tex_file, mode='w') as f: with open(tex_file, mode='w') as f:
f.write(tex) f.write(tex)
# Convenience function for removing temporary files # Convenience function for removing temporary files
def remove_temp_files(basename_): def remove_temp_files(basename_):
filenames = [f'{basename_}.tex', f'{basename_}.aux', filenames = [f'{basename_}.tex', f'{basename_}.aux',
@@ -150,28 +109,28 @@ def create_latex_pdf(character, basename, template):
raise exceptions.LatexError(f'Processing of {basename}.tex failed.') raise exceptions.LatexError(f'Processing of {basename}.tex failed.')
def create_spells_pdf(character, basename, flatten=False): def create_spells_pdf(char, spell_class, basename, flatten=False):
class_level = (character.class_name + ' ' + str(character.level)) class_level = (spell_class.class_name + ' ' + str(spell_class.level))
spell_level = lambda x : (x or '') spell_level = lambda x : (x or '')
fields = { fields = {
'Spellcasting Class 2': class_level, 'Spellcasting Class 2': class_level,
'SpellcastingAbility 2': character.spellcasting_ability.capitalize(), 'SpellcastingAbility 2': spell_class.spellcasting_ability.capitalize(),
'SpellSaveDC 2': character.spell_save_dc, 'SpellSaveDC 2': char.spell_save_dc(spell_class),
'SpellAtkBonus 2': mod_str(character.spell_attack_bonus), 'SpellAtkBonus 2': mod_str(char.spell_attack_bonus(spell_class)),
# Number of spell slots # Number of spell slots
'SlotsTotal 19': spell_level(character.spell_slots(1)), 'SlotsTotal 19': spell_level(char.spell_slots(1)),
'SlotsTotal 20': spell_level(character.spell_slots(2)), 'SlotsTotal 20': spell_level(char.spell_slots(2)),
'SlotsTotal 21': spell_level(character.spell_slots(3)), 'SlotsTotal 21': spell_level(char.spell_slots(3)),
'SlotsTotal 22': spell_level(character.spell_slots(4)), 'SlotsTotal 22': spell_level(char.spell_slots(4)),
'SlotsTotal 23': spell_level(character.spell_slots(5)), 'SlotsTotal 23': spell_level(char.spell_slots(5)),
'SlotsTotal 24': spell_level(character.spell_slots(6)), 'SlotsTotal 24': spell_level(char.spell_slots(6)),
'SlotsTotal 25': spell_level(character.spell_slots(7)), 'SlotsTotal 25': spell_level(char.spell_slots(7)),
'SlotsTotal 26': spell_level(character.spell_slots(8)), 'SlotsTotal 26': spell_level(char.spell_slots(8)),
'SlotsTotal 27': spell_level(character.spell_slots(9)), 'SlotsTotal 27': spell_level(char.spell_slots(9)),
} }
# Cantrips # Cantrips
cantrip_fields = (f'Spells 10{i}' for i in (14, 16, 17, 18, 19, 20, 21, 22)) cantrip_fields = (f'Spells 10{i}' for i in (14, 16, 17, 18, 19, 20, 21, 22))
cantrips = (spl for spl in character.spells if spl.level == 0) cantrips = (spl for spl in char.spells if spl.level == 0)
for spell, field_name in zip(cantrips, cantrip_fields): for spell, field_name in zip(cantrips, cantrip_fields):
fields[field_name] = str(spell) fields[field_name] = str(spell)
# Spells for each level # Spells for each level
@@ -198,12 +157,13 @@ def create_spells_pdf(character, basename, flatten=False):
9: (327, 326, 3079, 3080, 3081, 3082, 3083, ), 9: (327, 326, 3079, 3080, 3081, 3082, 3083, ),
} }
for level in field_numbers.keys(): for level in field_numbers.keys():
spells = tuple(spl for spl in character.spells if spl.level == level) spells = tuple(spl for spl in char.spells if spl.level == level)
field_names = tuple(f'Spells {i}' for i in field_numbers[level]) field_names = tuple(f'Spells {i}' for i in field_numbers[level])
prep_names = tuple(f'Check Box {i}' for i in prep_numbers[level]) prep_names = tuple(f'Check Box {i}' for i in prep_numbers[level])
for spell, field, chk_field in zip(spells, field_names, prep_names): for spell, field, chk_field in zip(spells, field_names, prep_names):
fields[field] = str(spell) fields[field] = str(spell)
is_prepared = any([isinstance(spell, Spl) for Spl in character.spells_prepared]) is_prepared = any([isinstance(spell, Spl)
for Spl in char.spells_prepared])
fields[chk_field] = CHECKBOX_ON if is_prepared else CHECKBOX_OFF fields[chk_field] = CHECKBOX_ON if is_prepared else CHECKBOX_OFF
# # Uncomment to post field names instead: # # Uncomment to post field names instead:
# for field in field_names: # for field in field_names:
@@ -214,14 +174,15 @@ def create_spells_pdf(character, basename, flatten=False):
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
def create_character_pdf(character, basename, flatten=False): def create_character_pdf(char, basename, flatten=False):
# Prepare the list of fields # Prepare the list of fields
class_level = f"{character.class_name} {character.level}" class_level = ' / '.join([f'{c.class_name} {c.class_level}'
for c in char.class_list])
fields = { fields = {
# Character description # Character description
'CharacterName': character.name, 'CharacterName': character.name,
'ClassLevel': class_level, 'ClassLevel': class_level,
'Background': character.background, 'Background': str(character.background),
'PlayerName': character.player_name, 'PlayerName': character.player_name,
'Race ': str(character.race), 'Race ': str(character.race),
'Alignment': character.alignment, 'Alignment': character.alignment,
@@ -346,7 +307,8 @@ def create_character_pdf(character, basename, flatten=False):
# Prepare the actual PDF # Prepare the actual PDF
dirname = os.path.dirname(os.path.abspath(__file__)) dirname = os.path.dirname(os.path.abspath(__file__))
src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf') src_pdf = os.path.join(dirname, 'blank-character-sheet-default.pdf')
return make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten) return make_pdf(fields, src_pdf=src_pdf, basename=basename,
flatten=flatten)
def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool=False): def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool=False):
@@ -445,20 +407,21 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
os.remove(fdfname) os.remove(fdfname)
def make_sheet(character_file, flatten=False): def make_sheet(character_file, char=None, flatten=False):
"""Prepare a PDF character sheet from the given character file. """Prepare a PDF character sheet from the given character file.
Parameters Parameters
---------- ----------
character_file : str
File (.py) to load character from. Will save PDF using same name
char : Character, optional
If provided, will not load from the character file, just use file
for PDF name
flatten : bool, optional flatten : bool, optional
If true, the resulting PDF will not be a fillable form. If true, the resulting PDF will look better and won't be fillable form.
""" """
# Create a character from the character definition if char is None:
char_props = load_character_file(character_file) char = character.Character.load(character_file)
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
char_base = os.path.splitext(character_file)[0] + '_char' char_base = os.path.splitext(character_file)[0] + '_char'
sheets = [char_base + '.pdf'] sheets = [char_base + '.pdf']
@@ -466,11 +429,17 @@ def make_sheet(character_file, flatten=False):
char_pdf = create_character_pdf(character=char, basename=char_base, char_pdf = create_character_pdf(character=char, basename=char_base,
flatten=flatten) flatten=flatten)
pages.append(char_pdf) pages.append(char_pdf)
if char.is_spellcaster: for spell_class in char.spellcasting_classes:
# Create spell sheet # Create spell sheet (one for each class)
spell_base = os.path.splitext(character_file)[0] + '_spells' # Even though each sheet will include same spells,
create_spells_pdf(character=char, basename=spell_base, flatten=flatten) # this will list all modifiers
spell_base = '{:s}_{:s}_spells'.format(
os.path.splitext(character_file)[0],
spell_class.class_name)
create_spells_pdf(char=char, spell_class=spell_class,
basename=spell_base, flatten=flatten)
sheets.append(spell_base + '.pdf') sheets.append(spell_base + '.pdf')
if char.is_spellcaster:
# Create spell book # Create spell book
spellbook_base = os.path.splitext(character_file)[0] + '_spellbook' spellbook_base = os.path.splitext(character_file)[0] + '_spellbook'
try: try:
@@ -495,6 +464,7 @@ def make_sheet(character_file, flatten=False):
final_pdf = os.path.splitext(character_file)[0] + '.pdf' final_pdf = os.path.splitext(character_file)[0] + '.pdf'
merge_pdfs(sheets, final_pdf, clean_up=True) merge_pdfs(sheets, final_pdf, clean_up=True)
def merge_pdfs(src_filenames, dest_filename, clean_up=False): def merge_pdfs(src_filenames, dest_filename, clean_up=False):
"""Merge several PDF files into a single final file. """Merge several PDF files into a single final file.
+109
View File
@@ -0,0 +1,109 @@
dungeonsheets_version = "0.4.2"
# Basic information
name = 'Inara Serradon'
# classes_levels: specify class and level or, if multiclass, specify as list
# example:
classes_levels = ['figher 2'] # 2nd level fighter
subclasses = [None]
# also accepted, as long as only one class
# classes_levels = 'fighter 2'
# subclasses = None
# multiclass example
# classes_levels = ['wizard 3', 'fighter 1', 'rogue 1'] # 5th level total
subclasses = ['necromancer', None, None]
player_name = 'Mark'
background = "Acolyte"
race = "High-Elf"
# level = 3 # no longer used
alignment = "Chaotic good"
xp = 2190
hp_max = 16
# Ability Scores
strength = 10
dexterity = 15
constitution = 14
intelligence = 16
wisdom = 12
charisma = 8
# Proficiencies and languages
skill_proficiencies = [
'arcana',
'insight',
'investigation',
'perception',
'religion',
]
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.""")
# List of known spells
spells = ('blindness deafness', 'burning hands', 'detect magic',
'false life', 'mage armor', 'mage hand', 'magic missile',
'prestidigitation', 'ray of frost', 'ray of sickness', 'shield',
'shocking grasp', 'sleep',)
# Which spells have been prepared (not including cantrips)
spells_prepared = ('blindness deafness', 'false life', 'mage armor',
'ray of sickness', 'shield', 'sleep',)
# 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.""")