mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 13:15:53 +02:00
added first version of multiclass options. Need to test
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
Add multiclassing hit dice
|
||||||
|
Add Character.save() option to save to text file
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
from . import weapons, character
|
from . import weapons, character
|
||||||
|
|
||||||
__VERSION__ = "0.6.1"
|
__VERSION__ = "0.7.0"
|
||||||
|
|||||||
@@ -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
@@ -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),
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
__all__ = ['load_character_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.
|
||||||
|
|
||||||
|
|||||||
@@ -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, 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.""")
|
||||||
Reference in New Issue
Block a user