Added a way to include homebrew in a character file.

This commit is contained in:
Mark Wolfman
2021-02-09 22:26:23 -06:00
parent f5a1d84c60
commit 75c77aa750
8 changed files with 346 additions and 71 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
__all__ = ('__version__', 'Character', 'weapons', 'features',
'character', 'race', 'background', 'spells')
from dungeonsheets import background, features, race, spells, weapons
from dungeonsheets import background, features, race, spells, weapons, mechanics
from dungeonsheets.character import Character
import os
+120 -58
View File
@@ -7,6 +7,7 @@ import os
import re
import warnings
import math
from types import ModuleType
import jinja2
@@ -58,6 +59,73 @@ 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
The thing to be resolved, either a string with the name of the
mechanic, or a subclass of *ParentClass* describing the
mechanic.
module : module
A python module in which to look for the defined string in *name*.
SuperClass : type
Class to determine whether *mechanic* should just be allowed
through as is.
error_message : str, optional
A string whose ``str.format()`` method (receiving one positional
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)
if is_already_resolved:
Mechanic = mechanic
else:
try:
# Retrieve pre-defined mechanic
Mechanic = findattr(module, mechanic)
except AttributeError:
# No pre-defined mechanic available
if warning_message is not None:
# Emit the warning
msg = warning_message.format(mechanic)
warnings.warn(msg)
else:
# Create a generic message so we can make a docstring later.
msg = f'Mechanic "{mechanic}" not defined. Please add it.'
# Create generic mechanic from the factory
class_name = "".join([s.title() for s in mechanic.split("_")])
mechanic_name = mechanic.replace("_", " ").title()
attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"}
Mechanic = type(class_name, (SuperClass,), attrs)
return Mechanic
class Character():
"""A generic player character.
@@ -163,7 +231,7 @@ class Character():
# parse all other attributes
self.set_attrs(**attrs)
self.__set_max_hp(attrs.get('hp_max', None))
def clear(self):
# reset class-definied items
self.class_list = list()
@@ -179,13 +247,13 @@ 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=[]):
if isinstance(cls, str):
@@ -200,7 +268,7 @@ class Character():
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):
@@ -499,15 +567,17 @@ class Character():
if isinstance(val, str):
val = [val]
for mitem in val:
try:
self.magic_items.append(findattr(magic_items, mitem)(owner=self))
except (AttributeError):
msg = (f'Magic Item "{mitem}" not defined. '
f'Please add it to ``magic_items.py``')
warnings.warn(msg)
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)
self.magic_items.append(ThisMagicItem(owner=self))
elif attr == 'weapon_proficiencies':
self.other_weapon_proficiencies = ()
wps = set([findattr(weapons, w) for w in val])
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(self.weapon_proficiencies)
self.other_weapon_proficiencies = list(wps)
elif attr == 'armor':
@@ -522,30 +592,23 @@ class Character():
val = [val]
_features = []
for f in val:
try:
_features.append(findattr(features, f))
except AttributeError:
msg = (f'Feature "{f}" not defined. '
f'Please add it to ``features.py``')
# create temporary feature
_features.append(features.create_feature(
name=f, source='Unknown',
__doc__="""Unknown Feature. Add to features.py"""))
warnings.warn(msg)
msg = 'Feature "{}" not defined. Please add it to ``features.py``'
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'):
# Create a list of actual spell objects
_spells = []
for spell_name in val:
try:
_spells.append(findattr(spells, spell_name))
except AttributeError:
msg = (f'Spell "{spell_name}" not defined. '
f'Please add it to ``spells.py``')
warnings.warn(msg)
# Create temporary spell
_spells.append(spells.create_spell(name=spell_name, level=9))
# raise AttributeError(msg)
msg = 'Spell "{}" not defined. Please add it to ``spells.py``'
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
@@ -559,25 +622,27 @@ class Character():
if hasattr(self, 'Artificer'):
_infusions = []
for infusion_name in val:
try:
_infusions.append(findattr(infusions, infusion_name))
except AttributeError:
msg = (f'Infusion "{infusion_name}" not defined. '
f'Please add it to ``infusions.py``')
warnings.warn(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)
else:
if not hasattr(self, attr):
elif type(val) not in (type, ModuleType):
# 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)
# 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)
def spell_attack_bonus(self, class_type):
ability_mod = getattr(self, class_type.spellcasting_ability).modifier
return (self.proficiency_bonus + ability_mod)
@@ -659,7 +724,11 @@ class Character():
if isinstance(new_armor, armor.Armor):
new_armor = new_armor
else:
NewArmor = findattr(armor, new_armor)
msg = 'Unnown armor "{}". Please add it to ``armor.py``.'
NewArmor = _resolve_mechanic(mechanic=new_armor,
module=armor,
SuperClass=armor.Armor,
warning_message=msg)
new_armor = NewArmor()
self.armor = new_armor
@@ -683,31 +752,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):
weapon_ = type(weapon)(wielder=self)
elif isinstance(weapon, str):
try:
NewWeapon = findattr(weapons, weapon)
except AttributeError:
warnings.warn(f"Unknown weapon '{weapon}'. Please add it to ``weapons.py`` "
"or submit an issue: https://github.com/canismarko/dungeon-sheets/issues",
RuntimeWarning)
return
weapon_ = NewWeapon(wielder=self)
elif issubclass(weapon, weapons.Weapon):
weapon_ = weapon(wielder=self)
ThisWeapon = type(weapon)
else:
raise AttributeError(f'Weapon "{weapon}" is not defined')
msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.'
ThisWeapon = _resolve_mechanic(mechanic=weapon,
module=weapons,
SuperClass=weapons.Weapon,
warning_message=msg)
# Save it to the array
self.weapons.append(weapon_)
self.weapons.append(ThisWeapon(wielder=self))
@property
def hit_dice(self):
+1 -1
View File
@@ -27,7 +27,7 @@ class Feature():
"""
name = "Generic Feature"
owner = None
source = '' # race, class, background, etc.
source = 'Unknown' # race, class, background, etc.
spells_known = ()
spells_prepared = ()
needs_implementation = False # Set to True if need to find way to compute stats
+1 -1
View File
@@ -43,7 +43,7 @@
\item [Casting Time:] [[ spl.casting_time ]] \\
\item [Duration:] [[ spl.duration ]] \\
\item [Range:] [[ spl.casting_range ]] \\
\item [Components:] [[ spl.component_string ]] \\
\item [Components:] [[ spl.component_string ]]
\end{description}
\vspace{\zerosep}
+9
View File
@@ -0,0 +1,9 @@
"""Convenience module holding base classes for the various kinds of
game mechanics."""
from dungeonsheets.spells import Spell
from dungeonsheets.features import Feature
from dungeonsheets.infusions import Infusion
from dungeonsheets.weapons import Weapon
from dungeonsheets.armor import Armor, Shield
from dungeonsheets.magic_items import MagicItem
+10 -8
View File
@@ -184,14 +184,16 @@ class MagicJar(Spell):
class MagicMissile(Spell):
"""You create three glowing darts of magical force. Each dart hits a creature of
your choice that you can see within range. A dart deals 1d4 + 1 force damage to
its target. The darts all strike simultaneously and you can direct them to hit
one creature or several.
At Higher Levels: When you cast this spell using a
spell slot of 2nd level or higher, the spell creates one more dart for each slot
level above 1st.
"""You create three glowing darts of magical force. Each dart hits a
creature of your choice that you can see within range. A dart
deals 1d4 + 1 force damage to its target. The darts all strike
simultaneously and you can direct them to hit one creature or
several.
At Higher Levels: When you cast this spell using a spell slot of
2nd level or higher, the spell creates one more dart for each slot
level above 1st.
"""
name = "Magic Missile"
level = 1