mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-05 20:38:26 +02:00
Ran flake8 and black linters, and other cleanup-related fixes.
Project now passes flake8 and black linter (also including more rst cleanup). Moved latex related things to dedicated ``latex.py`` module, and removed the ``makesheets -dF`` call from travis.
This commit is contained in:
+310
-231
@@ -1,8 +1,4 @@
|
||||
"""Tools for describing a player character."""
|
||||
__all__ = ('Character',)
|
||||
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
@@ -11,13 +7,19 @@ from types import ModuleType
|
||||
|
||||
import jinja2
|
||||
|
||||
from dungeonsheets import (armor, background, classes, exceptions, features,
|
||||
infusions, magic_items, monsters, race, spells,
|
||||
weapons)
|
||||
from dungeonsheets.armor import Armor, NoArmor, NoShield, Shield
|
||||
from dungeonsheets.dice import read_dice_str
|
||||
from dungeonsheets.stats import (Ability, ArmorClass, Initiative, Skill, Speed,
|
||||
findattr)
|
||||
from dungeonsheets import (
|
||||
armor,
|
||||
background,
|
||||
classes,
|
||||
features,
|
||||
infusions,
|
||||
magic_items,
|
||||
monsters,
|
||||
race,
|
||||
spells,
|
||||
weapons,
|
||||
)
|
||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Skill, Speed, findattr
|
||||
from dungeonsheets.weapons import Weapon
|
||||
from dungeonsheets.readers import read_character_file
|
||||
|
||||
@@ -26,25 +28,39 @@ def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
|
||||
__version__ = read('../VERSION').strip()
|
||||
__version__ = read("../VERSION").strip()
|
||||
|
||||
|
||||
dice_re = re.compile('(\d+)d(\d+)')
|
||||
dice_re = re.compile(r"(\d+)d(\d+)")
|
||||
|
||||
__all__ = ('Artificer', 'Barbarian', 'Bard', 'Cleric', 'Druid', 'Fighter', 'Monk',
|
||||
'Paladin', 'Ranger', 'Rogue', 'Sorcerer', 'Warlock', 'Wizard', )
|
||||
__all__ = (
|
||||
"Artificer",
|
||||
"Barbarian",
|
||||
"Bard",
|
||||
"Cleric",
|
||||
"Character",
|
||||
"Druid",
|
||||
"Fighter",
|
||||
"Monk",
|
||||
"Paladin",
|
||||
"Ranger",
|
||||
"Rogue",
|
||||
"Sorceror",
|
||||
"Warlock",
|
||||
"Wizard",
|
||||
)
|
||||
|
||||
multiclass_spellslots_by_level = {
|
||||
# char_lvl: (cantrips, 1st, 2nd, 3rd, ...)
|
||||
1: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
2: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
3: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
|
||||
4: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
|
||||
5: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
|
||||
6: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
|
||||
7: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
|
||||
8: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
|
||||
9: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
|
||||
1: (0, 2, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
2: (0, 3, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
3: (0, 4, 2, 0, 0, 0, 0, 0, 0, 0),
|
||||
4: (0, 4, 3, 0, 0, 0, 0, 0, 0, 0),
|
||||
5: (0, 4, 3, 2, 0, 0, 0, 0, 0, 0),
|
||||
6: (0, 4, 3, 3, 0, 0, 0, 0, 0, 0),
|
||||
7: (0, 4, 3, 3, 1, 0, 0, 0, 0, 0),
|
||||
8: (0, 4, 3, 3, 2, 0, 0, 0, 0, 0),
|
||||
9: (0, 4, 3, 3, 3, 1, 0, 0, 0, 0),
|
||||
10: (0, 4, 3, 3, 3, 2, 0, 0, 0, 0),
|
||||
11: (0, 4, 3, 3, 3, 2, 1, 0, 0, 0),
|
||||
12: (0, 4, 3, 3, 3, 2, 1, 0, 0, 0),
|
||||
@@ -61,22 +77,22 @@ multiclass_spellslots_by_level = {
|
||||
|
||||
def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
||||
"""Take a raw entry in a character sheet and turn it into a usable object.
|
||||
|
||||
|
||||
Eg: spells can be defined in many ways. This function accepts all
|
||||
of those options and returns an actual *Spell* class that can be
|
||||
used by a character::
|
||||
|
||||
|
||||
>>> from dungeonsheets import spells
|
||||
>>> _resolve_mechanic("mage_hand", spells, None)
|
||||
>>> class MySpell(spells.Spell): pass
|
||||
>>> _resolve_mechanic(MySpell, None, spells.Spell)
|
||||
>>> _resolve_mechanic("hocus pocus", spells, None)
|
||||
|
||||
|
||||
The acceptable entries for *mechanic*, in priority order, are:
|
||||
1. A subclass of *SuperClass*
|
||||
2. A string with the name of a defined spell in *module*
|
||||
3. The name of an unknown spell (creates generic object using *factory*)
|
||||
|
||||
|
||||
Parameters
|
||||
==========
|
||||
mechanic : str, type
|
||||
@@ -93,16 +109,18 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
||||
argument *mechanic*) will be used for displaying a warning when an
|
||||
unknown mechanic is resolved. If omitted, no warning will be
|
||||
displayed.
|
||||
|
||||
|
||||
Returns
|
||||
=======
|
||||
Mechanic
|
||||
A class representing the resolved game mechanic. This will
|
||||
likely be a subclass of *SuperClass* if the other parameters are
|
||||
well behaved, but this is not enforced.
|
||||
|
||||
|
||||
"""
|
||||
is_already_resolved = isinstance(mechanic, type) and issubclass(mechanic, SuperClass)
|
||||
is_already_resolved = isinstance(mechanic, type) and issubclass(
|
||||
mechanic, SuperClass
|
||||
)
|
||||
if is_already_resolved:
|
||||
Mechanic = mechanic
|
||||
else:
|
||||
@@ -126,10 +144,9 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
||||
return Mechanic
|
||||
|
||||
|
||||
class Character():
|
||||
"""A generic player character.
|
||||
class Character:
|
||||
"""A generic player character."""
|
||||
|
||||
"""
|
||||
# General attirubtes
|
||||
name = ""
|
||||
player_name = ""
|
||||
@@ -158,27 +175,29 @@ class Character():
|
||||
skill_expertise = list()
|
||||
languages = ""
|
||||
# Skills
|
||||
acrobatics = Skill(ability='dexterity')
|
||||
animal_handling = Skill(ability='wisdom')
|
||||
arcana = Skill(ability='intelligence')
|
||||
athletics = Skill(ability='strength')
|
||||
deception = Skill(ability='charisma')
|
||||
history = Skill(ability='intelligence')
|
||||
insight = Skill(ability='wisdom')
|
||||
intimidation = Skill(ability='charisma')
|
||||
investigation = Skill(ability='intelligence')
|
||||
medicine = Skill(ability='wisdom')
|
||||
nature = Skill(ability='intelligence')
|
||||
perception = Skill(ability='wisdom')
|
||||
performance = Skill(ability='charisma')
|
||||
persuasion = Skill(ability='charisma')
|
||||
religion = Skill(ability='intelligence')
|
||||
sleight_of_hand = Skill(ability='dexterity')
|
||||
stealth = Skill(ability='dexterity')
|
||||
survival = Skill(ability='wisdom')
|
||||
acrobatics = Skill(ability="dexterity")
|
||||
animal_handling = Skill(ability="wisdom")
|
||||
arcana = Skill(ability="intelligence")
|
||||
athletics = Skill(ability="strength")
|
||||
deception = Skill(ability="charisma")
|
||||
history = Skill(ability="intelligence")
|
||||
insight = Skill(ability="wisdom")
|
||||
intimidation = Skill(ability="charisma")
|
||||
investigation = Skill(ability="intelligence")
|
||||
medicine = Skill(ability="wisdom")
|
||||
nature = Skill(ability="intelligence")
|
||||
perception = Skill(ability="wisdom")
|
||||
performance = Skill(ability="charisma")
|
||||
persuasion = Skill(ability="charisma")
|
||||
religion = Skill(ability="intelligence")
|
||||
sleight_of_hand = Skill(ability="dexterity")
|
||||
stealth = Skill(ability="dexterity")
|
||||
survival = Skill(ability="wisdom")
|
||||
# Characteristics
|
||||
attacks_and_spellcasting = ""
|
||||
personality_traits = "TODO: Describe how your character behaves, interacts with others"
|
||||
personality_traits = (
|
||||
"TODO: Describe how your character behaves, interacts with others"
|
||||
)
|
||||
ideals = "TODO: Describe what values your character believes in."
|
||||
bonds = "TODO: Describe your character's commitments or ongoing quests."
|
||||
flaws = "TODO: Describe your character's interesting flaws."
|
||||
@@ -208,30 +227,33 @@ class Character():
|
||||
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
||||
self.clear()
|
||||
# make sure class, race, background are set first
|
||||
my_classes = attrs.pop('classes', [])
|
||||
my_levels = attrs.pop('levels', [])
|
||||
my_subclasses = attrs.pop('subclasses', [])
|
||||
my_classes = attrs.pop("classes", [])
|
||||
my_levels = attrs.pop("levels", [])
|
||||
my_subclasses = attrs.pop("subclasses", [])
|
||||
# backwards compatability
|
||||
if (len(my_classes) == 0):
|
||||
if ('class' in attrs):
|
||||
my_classes = [attrs.pop('class')]
|
||||
my_levels = [attrs.pop('level', 1)]
|
||||
my_subclasses = [attrs.pop('subclass', None)]
|
||||
if len(my_classes) == 0:
|
||||
if "class" in attrs:
|
||||
my_classes = [attrs.pop("class")]
|
||||
my_levels = [attrs.pop("level", 1)]
|
||||
my_subclasses = [attrs.pop("subclass", None)]
|
||||
else: # if no classes or levels given, default to Lvl 1 Fighter
|
||||
my_classes = ['Fighter']
|
||||
my_classes = ["Fighter"]
|
||||
my_levels = [1]
|
||||
my_subclasses = [None]
|
||||
# Generate the list of class objects
|
||||
self.add_classes(
|
||||
my_classes, my_levels, my_subclasses,
|
||||
feature_choices=attrs.get('feature_choices', []))
|
||||
my_classes,
|
||||
my_levels,
|
||||
my_subclasses,
|
||||
feature_choices=attrs.get("feature_choices", []),
|
||||
)
|
||||
# parse race and background
|
||||
self.race = attrs.pop('race', None)
|
||||
self.background = attrs.pop('background', None)
|
||||
self.race = attrs.pop("race", None)
|
||||
self.background = attrs.pop("background", None)
|
||||
# parse all other attributes
|
||||
self.set_attrs(**attrs)
|
||||
self.__set_max_hp(attrs.get('hp_max', None))
|
||||
|
||||
self.__set_max_hp(attrs.get("hp_max", None))
|
||||
|
||||
def clear(self):
|
||||
# reset class-definied items
|
||||
self.class_list = list()
|
||||
@@ -247,52 +269,65 @@ class Character():
|
||||
self.infusions = list()
|
||||
self.custom_features = list()
|
||||
self.feature_choices = list()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.class_name}: {self.name}>"
|
||||
|
||||
def add_class(self, cls: (classes.CharClass, type, str), level: (int, str),
|
||||
subclass=None, feature_choices=[]):
|
||||
|
||||
def add_class(
|
||||
self,
|
||||
cls: (classes.CharClass, type, str),
|
||||
level: (int, str),
|
||||
subclass=None,
|
||||
feature_choices=[],
|
||||
):
|
||||
if isinstance(cls, str):
|
||||
cls = cls.strip().title().replace(' ', '')
|
||||
cls = cls.strip().title().replace(" ", "")
|
||||
try:
|
||||
cls = getattr(classes, cls)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
'class was not recognized from classes.py: {:s}'.format(cls))
|
||||
"class was not recognized from classes.py: {:s}".format(cls)
|
||||
)
|
||||
if isinstance(level, str):
|
||||
level = int(level)
|
||||
self.class_list.append(cls(level, owner=self,
|
||||
subclass=subclass,
|
||||
feature_choices=feature_choices))
|
||||
|
||||
def add_classes(self, classes_list=[], levels=[], subclasses=[],
|
||||
feature_choices=[]):
|
||||
self.class_list.append(
|
||||
cls(level, owner=self, subclass=subclass, feature_choices=feature_choices)
|
||||
)
|
||||
|
||||
def add_classes(
|
||||
self, classes_list=[], levels=[], subclasses=[], feature_choices=[]
|
||||
):
|
||||
if isinstance(classes_list, str):
|
||||
classes_list = [classes_list]
|
||||
if isinstance(levels, int) or isinstance(levels, float) or isinstance(levels, str):
|
||||
if (
|
||||
isinstance(levels, int)
|
||||
or isinstance(levels, float)
|
||||
or isinstance(levels, str)
|
||||
):
|
||||
levels = [levels]
|
||||
if len(levels) == 0:
|
||||
levels = [1]*len(classes_list)
|
||||
levels = [1] * len(classes_list)
|
||||
if isinstance(subclasses, str):
|
||||
subclasses = [subclasses]
|
||||
if len(subclasses) == 0:
|
||||
subclasses = [None]*len(classes_list)
|
||||
assert len(classes_list) == len(levels), (
|
||||
'the length of classes {:d} does not match length of '
|
||||
'levels {:d}'.format(len(classes), len(levels)))
|
||||
assert len(classes_list) == len(subclasses), (
|
||||
'the length of classes {:d} does not match length of '
|
||||
'subclasses {:d}'.format(len(classes_list), len(subclasses)))
|
||||
class_list = []
|
||||
subclasses = [None] * len(classes_list)
|
||||
assert len(classes_list) == len(
|
||||
levels
|
||||
), "the length of classes {:d} does not match length of levels {:d}".format(
|
||||
len(classes), len(levels)
|
||||
)
|
||||
assert len(classes_list) == len(
|
||||
subclasses
|
||||
), "the length of classes {:d} does not match length of subclasses {:d}".format(
|
||||
len(classes_list), len(subclasses)
|
||||
)
|
||||
for cls, lvl, sub in zip(classes_list, levels, subclasses):
|
||||
params = {}
|
||||
params['feature_choices'] = feature_choices
|
||||
self.add_class(cls=cls, level=lvl, subclass=sub,
|
||||
**params)
|
||||
params["feature_choices"] = feature_choices
|
||||
self.add_class(cls=cls, level=lvl, subclass=sub, **params)
|
||||
|
||||
@property
|
||||
def race(self):
|
||||
@@ -309,8 +344,7 @@ class Character():
|
||||
try:
|
||||
self._race = findattr(race, newrace)(owner=self)
|
||||
except AttributeError:
|
||||
msg = (f'Race "{newrace}" not defined. '
|
||||
f'Please add it to ``race.py``')
|
||||
msg = f'Race "{newrace}" not defined. Please add it to ``race.py``'
|
||||
self._race = race.Race(owner=self)
|
||||
warnings.warn(msg)
|
||||
elif newrace is None:
|
||||
@@ -331,8 +365,9 @@ class Character():
|
||||
try:
|
||||
self._background = findattr(background, bg)(owner=self)
|
||||
except AttributeError:
|
||||
msg = (f'Background "{bg}" not defined. '
|
||||
f'Please add it to ``background.py``')
|
||||
msg = (
|
||||
f'Background "{bg}" not defined. Please add it to ``background.py``'
|
||||
)
|
||||
self._background = background.Background(owner=self)
|
||||
warnings.warn(msg)
|
||||
|
||||
@@ -345,8 +380,7 @@ class Character():
|
||||
|
||||
@property
|
||||
def classes_and_levels(self):
|
||||
return ' / '.join([f'{c.name} {c.level}'
|
||||
for c in self.class_list])
|
||||
return " / ".join([f"{c.name} {c.level}" for c in self.class_list])
|
||||
|
||||
@property
|
||||
def class_names(self):
|
||||
@@ -358,7 +392,7 @@ class Character():
|
||||
|
||||
@property
|
||||
def subclasses(self):
|
||||
return list([c.subclass or '' for c in self.class_list])
|
||||
return list([c.subclass or "" for c in self.class_list])
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
@@ -368,8 +402,10 @@ class Character():
|
||||
def level(self, new_level):
|
||||
self.primary_class.level = new_level
|
||||
if self.num_classes > 1:
|
||||
warnings.warn("Unable to tell which level to set. Updating "
|
||||
"level of primary class {:s}".format(self.primary_class.name))
|
||||
warnings.warn(
|
||||
"Unable to tell which level to set. Updating "
|
||||
"level of primary class {:s}".format(self.primary_class.name)
|
||||
)
|
||||
|
||||
@property
|
||||
def num_classes(self):
|
||||
@@ -377,7 +413,7 @@ class Character():
|
||||
|
||||
@property
|
||||
def has_class(self):
|
||||
return (self.num_classes > 0)
|
||||
return self.num_classes > 0
|
||||
|
||||
@property
|
||||
def primary_class(self):
|
||||
@@ -399,7 +435,7 @@ class Character():
|
||||
level_one_hp = self.primary_class.hit_dice_faces + const_mod
|
||||
self.hp_max = level_one_hp
|
||||
for char_cls in self.class_list:
|
||||
hp_per_lvl = char_cls.hit_dice_faces/2 + 1 + const_mod
|
||||
hp_per_lvl = char_cls.hit_dice_faces / 2 + 1 + const_mod
|
||||
levels = char_cls.level
|
||||
if char_cls == self.primary_class:
|
||||
levels -= 1
|
||||
@@ -415,9 +451,9 @@ class Character():
|
||||
for c in self.class_list[1:]:
|
||||
wp |= set(c.multiclass_weapon_proficiencies)
|
||||
if self.race is not None:
|
||||
wp |= set(getattr(self.race, 'weapon_proficiencies', ()))
|
||||
wp |= set(getattr(self.race, "weapon_proficiencies", ()))
|
||||
if self.background is not None:
|
||||
wp |= set(getattr(self.background, 'weapon_proficiencies', ()))
|
||||
wp |= set(getattr(self.background, "weapon_proficiencies", ()))
|
||||
return tuple(wp)
|
||||
|
||||
@weapon_proficiencies.setter
|
||||
@@ -438,10 +474,10 @@ class Character():
|
||||
"Fighting Style (Dueling)",
|
||||
"Fighting Style (Great Weapon Fighting)",
|
||||
"Fighting Style (Protection)",
|
||||
"Fighting Style (Two-Weapon Fighting)"
|
||||
"Fighting Style (Two-Weapon Fighting)",
|
||||
}
|
||||
for temp_feature in fts:
|
||||
fighting_style_defined = (temp_feature.name in set_of_fighting_styles)
|
||||
fighting_style_defined = temp_feature.name in set_of_fighting_styles
|
||||
if fighting_style_defined:
|
||||
break
|
||||
|
||||
@@ -450,18 +486,21 @@ class Character():
|
||||
for c in self.class_list:
|
||||
fts |= set(c.features)
|
||||
for feature in fts:
|
||||
if fighting_style_defined and feature.name == 'Fighting Style (Select One)':
|
||||
if (
|
||||
fighting_style_defined
|
||||
and feature.name == "Fighting Style (Select One)"
|
||||
):
|
||||
temp_feature = feature
|
||||
fts.remove(temp_feature)
|
||||
break
|
||||
if self.race is not None:
|
||||
fts |= set(getattr(self.race, 'features', ()))
|
||||
fts |= set(getattr(self.race, "features", ()))
|
||||
# some races have level-based features (Ex: Aasimar)
|
||||
if hasattr(self.race, 'features_by_level'):
|
||||
for lvl in range(1, self.level+1):
|
||||
if hasattr(self.race, "features_by_level"):
|
||||
for lvl in range(1, self.level + 1):
|
||||
fts |= set(self.race.features_by_level[lvl])
|
||||
if self.background is not None:
|
||||
fts |= set(getattr(self.background, 'features', ()))
|
||||
fts |= set(getattr(self.background, "features", ()))
|
||||
|
||||
return sorted(tuple(fts), key=(lambda x: x.name))
|
||||
|
||||
@@ -477,8 +516,10 @@ class Character():
|
||||
if self.primary_class is None:
|
||||
return self._saving_throw_proficiencies
|
||||
else:
|
||||
return (self._saving_throw_proficiencies or
|
||||
self.primary_class.saving_throw_proficiencies)
|
||||
return (
|
||||
self._saving_throw_proficiencies
|
||||
or self.primary_class.saving_throw_proficiencies
|
||||
)
|
||||
|
||||
@saving_throw_proficiencies.setter
|
||||
def saving_throw_proficiencies(self, vals):
|
||||
@@ -494,7 +535,7 @@ class Character():
|
||||
|
||||
@property
|
||||
def is_spellcaster(self):
|
||||
return (len(self.spellcasting_classes) > 0)
|
||||
return len(self.spellcasting_classes) > 0
|
||||
|
||||
def spell_slots(self, spell_level):
|
||||
warlock_slots = 0
|
||||
@@ -504,17 +545,24 @@ class Character():
|
||||
if len(self.spellcasting_classes_excluding_warlock) == 0:
|
||||
return warlock_slots
|
||||
if len(self.spellcasting_classes_excluding_warlock) == 1:
|
||||
return self.spellcasting_classes_excluding_warlock[0].spell_slots(spell_level) + warlock_slots
|
||||
return (
|
||||
self.spellcasting_classes_excluding_warlock[0].spell_slots(spell_level)
|
||||
+ warlock_slots
|
||||
)
|
||||
else:
|
||||
if spell_level == 0:
|
||||
return sum([c.spell_slots(0)
|
||||
for c in self.spellcasting_classes])
|
||||
return sum([c.spell_slots(0) for c in self.spellcasting_classes])
|
||||
else:
|
||||
# compute effective level from PHB pg 164
|
||||
eff_level = 0
|
||||
for c in self.spellcasting_classes_excluding_warlock:
|
||||
if type(c) in [classes.Bard, classes.Cleric, classes.Druid,
|
||||
classes.Sorceror, classes.Wizard]:
|
||||
if type(c) in [
|
||||
classes.Bard,
|
||||
classes.Cleric,
|
||||
classes.Druid,
|
||||
classes.Sorceror,
|
||||
classes.Wizard,
|
||||
]:
|
||||
eff_level += c.level
|
||||
elif type(c) in [classes.Paladin, classes.Ranger]:
|
||||
eff_level += c.level // 2
|
||||
@@ -525,7 +573,10 @@ class Character():
|
||||
if eff_level == 0:
|
||||
return warlock_slots
|
||||
else:
|
||||
return multiclass_spellslots_by_level[eff_level][spell_level] + warlock_slots
|
||||
return (
|
||||
multiclass_spellslots_by_level[eff_level][spell_level]
|
||||
+ warlock_slots
|
||||
)
|
||||
|
||||
@property
|
||||
def spells(self):
|
||||
@@ -555,78 +606,93 @@ class Character():
|
||||
Useful for loading a character from a dictionary
|
||||
"""
|
||||
for attr, val in attrs.items():
|
||||
if attr == 'dungeonsheets_version':
|
||||
pass # Maybe we'll verify this later?
|
||||
elif attr == 'weapons':
|
||||
if attr == "dungeonsheets_version":
|
||||
pass # Maybe we'll verify this later?
|
||||
elif attr == "weapons":
|
||||
if isinstance(val, str):
|
||||
val = [val]
|
||||
# Treat weapons specially
|
||||
for weap in val:
|
||||
self.wield_weapon(weap)
|
||||
elif attr == 'magic_items':
|
||||
elif attr == "magic_items":
|
||||
if isinstance(val, str):
|
||||
val = [val]
|
||||
for mitem in val:
|
||||
msg = (f'Magic Item "{mitem}" not defined. '
|
||||
f'Please add it to ``magic_items.py``')
|
||||
ThisMagicItem = _resolve_mechanic(mechanic=mitem,
|
||||
module=magic_items,
|
||||
SuperClass=magic_items.MagicItem,
|
||||
warning_message=msg)
|
||||
msg = (
|
||||
f'Magic Item "{mitem}" not defined. '
|
||||
"Please add it to ``magic_items.py``"
|
||||
)
|
||||
ThisMagicItem = _resolve_mechanic(
|
||||
mechanic=mitem,
|
||||
module=magic_items,
|
||||
SuperClass=magic_items.MagicItem,
|
||||
warning_message=msg,
|
||||
)
|
||||
self.magic_items.append(ThisMagicItem(owner=self))
|
||||
elif attr == 'weapon_proficiencies':
|
||||
elif attr == "weapon_proficiencies":
|
||||
self.other_weapon_proficiencies = ()
|
||||
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
|
||||
wps = set([_resolve_mechanic(w, weapons, weapons.Weapon, msg) for w in val])
|
||||
wps = set(
|
||||
[_resolve_mechanic(w, weapons, weapons.Weapon, msg) for w in val]
|
||||
)
|
||||
wps -= set(self.weapon_proficiencies)
|
||||
self.other_weapon_proficiencies = list(wps)
|
||||
elif attr == 'armor':
|
||||
elif attr == "armor":
|
||||
self.wear_armor(val)
|
||||
elif attr == 'shield':
|
||||
elif attr == "shield":
|
||||
self.wield_shield(val)
|
||||
elif attr == 'circle':
|
||||
if hasattr(self, 'Druid'):
|
||||
elif attr == "circle":
|
||||
if hasattr(self, "Druid"):
|
||||
self.Druid.circle = val
|
||||
elif attr == 'features':
|
||||
elif attr == "features":
|
||||
if isinstance(val, str):
|
||||
val = [val]
|
||||
_features = []
|
||||
for f in val:
|
||||
msg = 'Feature "{}" not defined. Please add it to ``features.py``'
|
||||
ThisFeature = _resolve_mechanic(mechanic=f,
|
||||
module=features,
|
||||
SuperClass=features.Feature,
|
||||
warning_message=msg)
|
||||
ThisFeature = _resolve_mechanic(
|
||||
mechanic=f,
|
||||
module=features,
|
||||
SuperClass=features.Feature,
|
||||
warning_message=msg,
|
||||
)
|
||||
_features.append(ThisFeature)
|
||||
self.custom_features += tuple(F(owner=self) for F in _features)
|
||||
elif (attr == 'spells') or (attr == 'spells_prepared'):
|
||||
elif (attr == "spells") or (attr == "spells_prepared"):
|
||||
# Create a list of actual spell objects
|
||||
_spells = []
|
||||
for spell_name in val:
|
||||
msg = 'Spell "{}" not defined. Please add it to ``spells.py``'
|
||||
ThisSpell = _resolve_mechanic(mechanic=spell_name,
|
||||
module=spells,
|
||||
SuperClass=spells.Spell,
|
||||
warning_message=msg)
|
||||
ThisSpell = _resolve_mechanic(
|
||||
mechanic=spell_name,
|
||||
module=spells,
|
||||
SuperClass=spells.Spell,
|
||||
warning_message=msg,
|
||||
)
|
||||
_spells.append(ThisSpell)
|
||||
# Sort by name
|
||||
_spells.sort(key=lambda spell: spell.name)
|
||||
# Save list of spells to character atribute
|
||||
if attr == 'spells':
|
||||
if attr == "spells":
|
||||
# Instantiate them all for the spells list
|
||||
self._spells = tuple(S() for S in _spells)
|
||||
else:
|
||||
# Instantiate them all for the spells list
|
||||
self._spells_prepared = tuple(S() for S in _spells)
|
||||
elif attr == 'infusions':
|
||||
if hasattr(self, 'Artificer'):
|
||||
elif attr == "infusions":
|
||||
if hasattr(self, "Artificer"):
|
||||
_infusions = []
|
||||
for infusion_name in val:
|
||||
msg = 'Infusion "{}" not defined. Please add it to ``infusions.py``'
|
||||
ThisInfusion = _resolve_mechanic(mechanic=infusion_name,
|
||||
module=infusions,
|
||||
SuperClass=infusions.Infusion,
|
||||
warning_message=msg)
|
||||
msg = (
|
||||
"Infusion '{}' not defined. Please add it to"
|
||||
" ``infusions.py``"
|
||||
)
|
||||
ThisInfusion = _resolve_mechanic(
|
||||
mechanic=infusion_name,
|
||||
module=infusions,
|
||||
SuperClass=infusions.Infusion,
|
||||
warning_message=msg,
|
||||
)
|
||||
_infusions.append(ThisInfusion)
|
||||
_infusions.sort(key=lambda infusion: infusion.name)
|
||||
self.infusions = tuple(i() for i in _infusions)
|
||||
@@ -634,18 +700,19 @@ class Character():
|
||||
# Some other generic attribute
|
||||
is_unknown = not hasattr(self, attr) and not attr.startswith("_")
|
||||
if is_unknown:
|
||||
warnings.warn(f"Setting unknown character attribute {attr}",
|
||||
RuntimeWarning)
|
||||
warnings.warn(
|
||||
f"Setting unknown character attribute {attr}", RuntimeWarning
|
||||
)
|
||||
# Lookup general attributes
|
||||
setattr(self, attr, val)
|
||||
|
||||
|
||||
def spell_save_dc(self, class_type):
|
||||
ability_mod = getattr(self, class_type.spellcasting_ability).modifier
|
||||
return (8 + self.proficiency_bonus + ability_mod)
|
||||
|
||||
return 8 + self.proficiency_bonus + ability_mod
|
||||
|
||||
def spell_attack_bonus(self, class_type):
|
||||
ability_mod = getattr(self, class_type.spellcasting_ability).modifier
|
||||
return (self.proficiency_bonus + ability_mod)
|
||||
return self.proficiency_bonus + ability_mod
|
||||
|
||||
def is_proficient(self, weapon: Weapon):
|
||||
"""Is the character proficient with this item?
|
||||
@@ -690,24 +757,29 @@ class Character():
|
||||
# Add this item to the list text
|
||||
final_text += txt
|
||||
# Add a period at the end
|
||||
final_text += '.'
|
||||
final_text += "."
|
||||
return final_text
|
||||
|
||||
@property
|
||||
def features_text(self):
|
||||
s = '\n\n--'.join([f.name + ("**" if f.needs_implementation else "")
|
||||
for f in self.features])
|
||||
if s != '':
|
||||
s = '(See Features Page)\n\n--' + s
|
||||
s += '\n\n=================\n\n'
|
||||
s = "\n\n--".join(
|
||||
[f.name + ("**" if f.needs_implementation else "") for f in self.features]
|
||||
)
|
||||
if s != "":
|
||||
s = "(See Features Page)\n\n--" + s
|
||||
s += "\n\n=================\n\n"
|
||||
return s
|
||||
|
||||
@property
|
||||
def magic_items_text(self):
|
||||
s = ', '.join([f.name + ("**" if f.needs_implementation else "")
|
||||
for f in sorted(self.magic_items, key=(lambda x: x.name))])
|
||||
s = ", ".join(
|
||||
[
|
||||
f.name + ("**" if f.needs_implementation else "")
|
||||
for f in sorted(self.magic_items, key=(lambda x: x.name))
|
||||
]
|
||||
)
|
||||
if s:
|
||||
s += ', '
|
||||
s += ", "
|
||||
return s
|
||||
|
||||
def wear_armor(self, new_armor):
|
||||
@@ -720,15 +792,17 @@ class Character():
|
||||
directly.
|
||||
|
||||
"""
|
||||
if new_armor not in ('', 'None', None):
|
||||
if new_armor not in ("", "None", None):
|
||||
if isinstance(new_armor, armor.Armor):
|
||||
new_armor = new_armor
|
||||
else:
|
||||
msg = 'Unnown armor "{}". Please add it to ``armor.py``.'
|
||||
NewArmor = _resolve_mechanic(mechanic=new_armor,
|
||||
module=armor,
|
||||
SuperClass=armor.Armor,
|
||||
warning_message=msg)
|
||||
NewArmor = _resolve_mechanic(
|
||||
mechanic=new_armor,
|
||||
module=armor,
|
||||
SuperClass=armor.Armor,
|
||||
warning_message=msg,
|
||||
)
|
||||
new_armor = NewArmor()
|
||||
self.armor = new_armor
|
||||
|
||||
@@ -742,7 +816,7 @@ class Character():
|
||||
directly.
|
||||
|
||||
"""
|
||||
if shield not in ('', 'None', None):
|
||||
if shield not in ("", "None", None):
|
||||
try:
|
||||
NewShield = findattr(armor, shield)
|
||||
except AttributeError:
|
||||
@@ -752,22 +826,24 @@ class Character():
|
||||
|
||||
def wield_weapon(self, weapon):
|
||||
"""Accepts a string and adds it to the list of wielded weapons.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
weapon : str
|
||||
Case-insensitive string with a name of the weapon.
|
||||
|
||||
|
||||
"""
|
||||
# Retrieve the weapon class from the weapons module
|
||||
if isinstance(weapon, weapons.Weapon):
|
||||
ThisWeapon = type(weapon)
|
||||
else:
|
||||
msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.'
|
||||
ThisWeapon = _resolve_mechanic(mechanic=weapon,
|
||||
module=weapons,
|
||||
SuperClass=weapons.Weapon,
|
||||
warning_message=msg)
|
||||
ThisWeapon = _resolve_mechanic(
|
||||
mechanic=weapon,
|
||||
module=weapons,
|
||||
SuperClass=weapons.Weapon,
|
||||
warning_message=msg,
|
||||
)
|
||||
# Save it to the array
|
||||
self.weapons.append(ThisWeapon(wielder=self))
|
||||
|
||||
@@ -776,8 +852,7 @@ class Character():
|
||||
"""What type and how many dice to use for re-gaining hit points.
|
||||
|
||||
To change, set hit_dice_num and hit_dice_faces."""
|
||||
return ' + '.join([f'{c.level}d{c.hit_dice_faces}'
|
||||
for c in self.class_list])
|
||||
return " + ".join([f"{c.level}d{c.hit_dice_faces}" for c in self.class_list])
|
||||
|
||||
@property
|
||||
def hit_dice_faces(self):
|
||||
@@ -805,30 +880,30 @@ class Character():
|
||||
return prof
|
||||
|
||||
def can_assume_shape(self, shape: monsters.Monster):
|
||||
return hasattr(self, 'Druid') and self.Druid.can_assume_shape(shape)
|
||||
return hasattr(self, "Druid") and self.Druid.can_assume_shape(shape)
|
||||
|
||||
@property
|
||||
def all_wild_shapes(self):
|
||||
if hasattr(self, 'Druid'):
|
||||
if hasattr(self, "Druid"):
|
||||
return self.Druid.all_wild_shapes
|
||||
else:
|
||||
return ()
|
||||
|
||||
@property
|
||||
def wild_shapes(self):
|
||||
if hasattr(self, 'Druid'):
|
||||
if hasattr(self, "Druid"):
|
||||
return self.Druid.wild_shapes
|
||||
else:
|
||||
return ()
|
||||
|
||||
@wild_shapes.setter
|
||||
def wild_shapes(self, new_shapes):
|
||||
if hasattr(self, 'Druid'):
|
||||
if hasattr(self, "Druid"):
|
||||
self.Druid.wild_shapes = new_shapes
|
||||
|
||||
@property
|
||||
def infusions_text(self):
|
||||
if hasattr(self, 'Artificer'):
|
||||
if hasattr(self, "Artificer"):
|
||||
return tuple([i.name for i in self.infusions])
|
||||
else:
|
||||
return ()
|
||||
@@ -837,124 +912,128 @@ class Character():
|
||||
def load(cls, character_file):
|
||||
# Create a character from the character definition
|
||||
char_props = read_character_file(character_file)
|
||||
classes = char_props.get('classes', [])
|
||||
classes = char_props.get("classes", [])
|
||||
# backwards compatability
|
||||
if (len(classes) == 0) and ('character_class' in char_props):
|
||||
char_props['classes'] = [char_props.pop('character_class').lower().capitalize()]
|
||||
char_props['levels'] = [str(char_props.pop('level'))]
|
||||
if (len(classes) == 0) and ("character_class" in char_props):
|
||||
char_props["classes"] = [
|
||||
char_props.pop("character_class").lower().capitalize()
|
||||
]
|
||||
char_props["levels"] = [str(char_props.pop("level"))]
|
||||
# Create the character with loaded properties
|
||||
char = Character(**char_props)
|
||||
return char
|
||||
|
||||
def save(self, filename, template_file='character_template.txt'):
|
||||
def save(self, filename, template_file="character_template.txt"):
|
||||
# Create the template context
|
||||
context = dict(
|
||||
char=self,
|
||||
)
|
||||
# Render the template
|
||||
src_path = os.path.join(os.path.dirname(__file__), 'forms/')
|
||||
text = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(src_path or './')
|
||||
).get_template(template_file).render(context)
|
||||
src_path = os.path.join(os.path.dirname(__file__), "forms/")
|
||||
text = (
|
||||
jinja2.Environment(loader=jinja2.FileSystemLoader(src_path or "./"))
|
||||
.get_template(template_file)
|
||||
.render(context)
|
||||
)
|
||||
# Save the file
|
||||
with open(filename, mode='w') as f:
|
||||
with open(filename, mode="w") as f:
|
||||
f.write(text)
|
||||
|
||||
def to_pdf(self, filename, **kwargs):
|
||||
from dungeonsheets.make_sheets import make_sheet
|
||||
if filename.endswith('.pdf'):
|
||||
filename = filename.replace('pdf', 'py')
|
||||
make_sheet(filename, character=self,
|
||||
flatten=kwargs.get('flatten', True))
|
||||
|
||||
if filename.endswith(".pdf"):
|
||||
filename = filename.replace("pdf", "py")
|
||||
make_sheet(filename, character=self, flatten=kwargs.get("flatten", True))
|
||||
|
||||
|
||||
# Add backwards compatability for tests
|
||||
class Artificer(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Artificer']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Artificer"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Barbarian(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Barbarian']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Barbarian"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Bard(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Bard']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Bard"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Cleric(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Cleric']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Cleric"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Druid(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Druid']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Druid"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Fighter(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Fighter']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Fighter"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Monk(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Monk']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Monk"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Paladin(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Paladin']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Paladin"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Ranger(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Ranger']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Ranger"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Rogue(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Rogue']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Rogue"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Sorceror(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Sorceror']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Sorceror"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Warlock(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Warlock']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Warlock"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
|
||||
class Wizard(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
attrs['classes'] = ['Wizard']
|
||||
attrs['levels'] = [level]
|
||||
attrs["classes"] = ["Wizard"]
|
||||
attrs["levels"] = [level]
|
||||
super().__init__(**attrs)
|
||||
|
||||
Reference in New Issue
Block a user