mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-03 19:38:30 +02:00
Monsters now have a spells attribute that will eventually hold a list of spells.
This commit is contained in:
+22
-97
@@ -69,83 +69,6 @@ multiclass_spellslots_by_level = {
|
||||
}
|
||||
|
||||
|
||||
def _resolve_mechanic(mechanic, 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::
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
||||
|
||||
>>> from dungeonsheets import spells
|
||||
>>> class MySpell(spells.Spell): pass
|
||||
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
||||
|
||||
The acceptable entries for *mechanic*, in priority order, are:
|
||||
1. A subclass of *SuperClass*
|
||||
2. A string with the name of defined content
|
||||
3. The name of an unknown spell (creates generic object using *factory*)
|
||||
|
||||
*SuperClass* can be ``None`` to match any class, however this will
|
||||
raise an exception if more than one content type has this
|
||||
name. For example, "shield" can refer to both the armor or the
|
||||
spell, so ``_resolve_mechanic("shield")`` will raise an exception.
|
||||
|
||||
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.
|
||||
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
|
||||
valid_classes = [SuperClass] if SuperClass is not None else []
|
||||
Mechanic = find_content(mechanic, valid_classes=valid_classes)
|
||||
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(Entity):
|
||||
"""A generic player character."""
|
||||
|
||||
@@ -628,7 +551,7 @@ class Character(Entity):
|
||||
f'Magic Item "{mitem}" not defined. '
|
||||
"Please add it to ``magic_items.py``"
|
||||
)
|
||||
ThisMagicItem = _resolve_mechanic(
|
||||
ThisMagicItem = self._resolve_mechanic(
|
||||
mechanic=mitem,
|
||||
SuperClass=magic_items.MagicItem,
|
||||
warning_message=msg,
|
||||
@@ -639,7 +562,7 @@ class Character(Entity):
|
||||
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
|
||||
wps = set(
|
||||
[
|
||||
_resolve_mechanic(
|
||||
self._resolve_mechanic(
|
||||
w, SuperClass=weapons.Weapon, warning_message=msg
|
||||
)
|
||||
for w in val
|
||||
@@ -660,7 +583,7 @@ class Character(Entity):
|
||||
_features = []
|
||||
for f in val:
|
||||
msg = 'Feature "{}" not defined. Please add it to ``features.py``'
|
||||
ThisFeature = _resolve_mechanic(
|
||||
ThisFeature = self._resolve_mechanic(
|
||||
mechanic=f,
|
||||
SuperClass=features.Feature,
|
||||
warning_message=msg,
|
||||
@@ -672,7 +595,7 @@ class Character(Entity):
|
||||
_spells = []
|
||||
for spell_name in val:
|
||||
msg = 'Spell "{}" not defined. Please add it to ``spells.py``'
|
||||
ThisSpell = _resolve_mechanic(
|
||||
ThisSpell = self._resolve_mechanic(
|
||||
mechanic=spell_name,
|
||||
SuperClass=spells.Spell,
|
||||
warning_message=msg,
|
||||
@@ -695,7 +618,7 @@ class Character(Entity):
|
||||
"Infusion '{}' not defined. Please add it to"
|
||||
" ``infusions.py``"
|
||||
)
|
||||
ThisInfusion = _resolve_mechanic(
|
||||
ThisInfusion = self._resolve_mechanic(
|
||||
mechanic=infusion_name,
|
||||
SuperClass=infusions.Infusion,
|
||||
warning_message=msg,
|
||||
@@ -767,6 +690,14 @@ class Character(Entity):
|
||||
final_text += "."
|
||||
return final_text
|
||||
|
||||
@proficiencies_text.setter
|
||||
def proficiencies_text(self, val):
|
||||
try:
|
||||
profs = profiencies_text.split(",")
|
||||
except AttributeError:
|
||||
profs = proficiencies_text
|
||||
self._proficiencies_text = profs
|
||||
|
||||
@property
|
||||
def features_text(self):
|
||||
s = "\n\n--".join(
|
||||
@@ -804,7 +735,7 @@ class Character(Entity):
|
||||
new_armor = new_armor
|
||||
else:
|
||||
msg = 'Unnown armor "{}". Please add it to ``armor.py``.'
|
||||
NewArmor = _resolve_mechanic(
|
||||
NewArmor = self._resolve_mechanic(
|
||||
mechanic=new_armor,
|
||||
SuperClass=armor.Armor,
|
||||
warning_message=msg,
|
||||
@@ -823,11 +754,8 @@ class Character(Entity):
|
||||
|
||||
"""
|
||||
if shield not in ("", "None", None):
|
||||
try:
|
||||
NewShield = find_content(shield, valid_classes=[armor.Shield])
|
||||
except AttributeError:
|
||||
# Not a string, so just treat it as Armor
|
||||
NewShield = shield
|
||||
msg = 'Unknown shield "{}". Please ad it to ``shields.py``.'
|
||||
NewShield = self._resolve_mechanic(shield, SuperClass=armor.Shield, warning_message=msg)
|
||||
self.shield = NewShield()
|
||||
|
||||
def wield_weapon(self, weapon):
|
||||
@@ -840,15 +768,12 @@ class Character(Entity):
|
||||
|
||||
"""
|
||||
# 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,
|
||||
SuperClass=weapons.Weapon,
|
||||
warning_message=msg,
|
||||
)
|
||||
msg = 'Unknown weapon "{}". Please add it to ``weapons.py``.'
|
||||
ThisWeapon = self._resolve_mechanic(
|
||||
mechanic=weapon,
|
||||
SuperClass=weapons.Weapon,
|
||||
warning_message=msg,
|
||||
)
|
||||
# Save it to the array
|
||||
self._weapons.append(ThisWeapon(wielder=self))
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ class Druid(CharClass):
|
||||
try:
|
||||
NewMonster = find_content(shape, valid_classes=[monsters.Monster])
|
||||
new_shape = NewMonster()
|
||||
except AttributeError:
|
||||
except exceptions.ContentNotFound:
|
||||
msg = (
|
||||
f"Wild shape '{shape}' not found. Please add it to"
|
||||
" ``monsters.py``"
|
||||
|
||||
@@ -13,6 +13,7 @@ from dungeonsheets import (
|
||||
infusions,
|
||||
magic_items,
|
||||
features,
|
||||
exceptions,
|
||||
)
|
||||
|
||||
|
||||
@@ -38,7 +39,11 @@ class ContentRegistry:
|
||||
|
||||
"""
|
||||
# Come up with several options
|
||||
name = name.strip()
|
||||
try:
|
||||
name = name.strip()
|
||||
except AttributeError as e:
|
||||
# Probably not a string
|
||||
raise ValueError('content "%s" is not a valid identifier string.' % name)
|
||||
# check for +X weapons, armor, shields
|
||||
bonus = 0
|
||||
for i in range(1, 11):
|
||||
@@ -72,9 +77,9 @@ class ContentRegistry:
|
||||
found_attrs = [attr for attr, v in zip(found_attrs, is_valid) if v]
|
||||
# Check that we found a valid, unique attribute
|
||||
if len(found_attrs) == 0:
|
||||
raise AttributeError(f"Modules {self.modules} have no attribute {name}")
|
||||
raise exceptions.ContentNotFound(f"Modules {self.modules} have no attribute {name}")
|
||||
elif len(found_attrs) > 1:
|
||||
raise RuntimeError(f"Found multiple content entries for {name}")
|
||||
raise exceptions.AmbiguousContent(f"Found multiple content entries for {name}")
|
||||
else:
|
||||
attr = found_attrs[0]
|
||||
# Apply weapon/etc. bonuses
|
||||
@@ -100,7 +105,7 @@ default_content_registry.add_module(magic_items)
|
||||
default_content_registry.add_module(features)
|
||||
|
||||
|
||||
def find_content(name: str, valid_classes: Optional[List]):
|
||||
def find_content(name: str, valid_classes: Optional[List] = None):
|
||||
"""Find content from a previously registered module.
|
||||
|
||||
Parameters
|
||||
|
||||
+83
-1
@@ -1,6 +1,9 @@
|
||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
|
||||
from abc import ABC
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
def read(fname):
|
||||
@@ -109,4 +112,83 @@ class Entity(ABC):
|
||||
self.medicine, self.nature, self.perception,
|
||||
self.performance, self.persuasion, self.religion,
|
||||
self.sleight_of_hand, self.stealth, self.survival,]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mechanic(mechanic, 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::
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
||||
|
||||
>>> from dungeonsheets import spells
|
||||
>>> class MySpell(spells.Spell): pass
|
||||
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
||||
|
||||
The acceptable entries for *mechanic*, in priority order, are:
|
||||
1. A subclass of *SuperClass*
|
||||
2. A string with the name of defined content
|
||||
3. The name of an unknown spell (creates generic object using *factory*)
|
||||
|
||||
*SuperClass* can be ``None`` to match any class, however this will
|
||||
raise an exception if more than one content type has this
|
||||
name. For example, "shield" can refer to both the armor or the
|
||||
spell, so ``_resolve_mechanic("shield")`` will raise an exception.
|
||||
|
||||
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.
|
||||
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
|
||||
elif SuperClass is not None and isinstance(mechanic, SuperClass):
|
||||
# Has been instantiated for some reason
|
||||
Mechanic = type(Mechanic)
|
||||
else:
|
||||
try:
|
||||
# Retrieve pre-defined mechanic
|
||||
valid_classes = [SuperClass] if SuperClass is not None else []
|
||||
Mechanic = find_content(mechanic, valid_classes=valid_classes)
|
||||
except ValueError:
|
||||
# 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
|
||||
|
||||
@@ -28,3 +28,10 @@ class UnknownFileType(RuntimeError):
|
||||
|
||||
class UnknownOutputFormat(RuntimeError):
|
||||
"""The output format requested is not one of the known outputs."""
|
||||
|
||||
|
||||
class ContentNotFound(ValueError):
|
||||
"""The requested content could not be resolved."""
|
||||
|
||||
class AmbiguousContent(ValueError):
|
||||
"""Multiple valid content entries were found."""
|
||||
|
||||
@@ -96,8 +96,8 @@
|
||||
<dd>[[ spl.component_string ]]</dd>
|
||||
</dl>
|
||||
|
||||
<block class="spell-description">
|
||||
<div class="spell-description">
|
||||
[[ spl.__doc__ | rst_to_html(top_heading_level=1) ]]
|
||||
</block>
|
||||
</div>
|
||||
|
||||
[% endfor %]
|
||||
|
||||
@@ -232,7 +232,7 @@ def make_gm_sheet(
|
||||
else:
|
||||
try:
|
||||
MyMonster = find_content(monster, valid_classes=[monsters.Monster])
|
||||
except AttributeError:
|
||||
except exceptions.ContentNotFound:
|
||||
msg = f"Monster '{monster}' not found. Please add it to ``monsters.py``"
|
||||
warnings.warn(msg)
|
||||
continue
|
||||
|
||||
@@ -3,11 +3,32 @@ shape forms.
|
||||
|
||||
"""
|
||||
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
from dungeonsheets.entity import Entity
|
||||
from dungeonsheets.spells import Spell
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
class Monster(Entity):
|
||||
class SpellFactory(ABCMeta):
|
||||
"""Meta class to resolve spell strings into the ``spells.Spell``.
|
||||
|
||||
For classes using this metaclass, the *spell* attribute, if
|
||||
present, should be a list of spells that the entity knows. For
|
||||
each entry on that list, anything that is not already a spell
|
||||
class (so probably a string) will be resolved into the
|
||||
corresponding ``spells.Spell`` class.
|
||||
|
||||
"""
|
||||
def __init__(self, name, bases, attrs):
|
||||
for idx, spell in enumerate(self.spells):
|
||||
TheSpell = self._resolve_mechanic(spell, SuperClass=Spell)
|
||||
self.spells[idx] = TheSpell
|
||||
|
||||
|
||||
class Monster(Entity, metaclass=SpellFactory):
|
||||
|
||||
"""A monster that may be encountered when adventuring."""
|
||||
|
||||
name = "Generic Monster"
|
||||
@@ -28,6 +49,7 @@ class Monster(Entity):
|
||||
burrow_speed = 0
|
||||
hp_max = 10
|
||||
hit_dice = "1d6"
|
||||
spells = []
|
||||
|
||||
def __init__(self):
|
||||
super(Monster, self).__init__()
|
||||
|
||||
@@ -362,25 +362,28 @@ class Pony(Monster):
|
||||
|
||||
|
||||
class Priest(Monster):
|
||||
"""Divine Eminence
|
||||
As a bonus action, the priest can expend a spell slot to cause
|
||||
its melee weapon attacks to magically deal an extra 10 (3d6)
|
||||
radiant damage to a target on a hit. This benefit lasts until
|
||||
the end of the turn. If the priest expends a spell slot of 2nd
|
||||
level or higher, the extra damage increases by 1d6 for each
|
||||
level above 1st.
|
||||
Spellcasting
|
||||
The priest is a 5th-level spellcaster. Its spellcasting ability
|
||||
is Wisdom (spell save DC 13, +5 to hit with spell attacks). The
|
||||
priest has the following cleric spells prepared:
|
||||
|
||||
- Cantrips (at will): light, sacred flame, thaumaturgy
|
||||
- 1st level (4 slots): cure wounds, guiding bolt, sanctuary
|
||||
- 2nd level (3 slots): lesser restoration, spiritual weapon
|
||||
- 3rd level (2 slots): dispel magic, spirit guardians
|
||||
|
||||
Mace
|
||||
Melee Weapon Attack: +2 to hit, reach 5 ft., one target. Hit: 3
|
||||
(1d6) bludgeoning damage.
|
||||
|
||||
"""
|
||||
Divine Eminence.
|
||||
|
||||
As a bonus action, the priest can expend a spell slot to cause its melee weapon attacks to magically deal an extra 10 (3d6) radiant damage to a target on a hit. This benefit lasts until the end of the turn. If the priest expends a spell slot of 2nd level or higher, the extra damage increases by 1d6 for each level above 1st.
|
||||
|
||||
Spellcasting.
|
||||
|
||||
The priest is a 5th-level spellcaster. Its spellcasting ability is Wisdom (spell save DC 13, +5 to hit with spell attacks). The priest has the following cleric spells prepared:
|
||||
|
||||
- Cantrips (at will): light, sacred flame, thaumaturgy
|
||||
- 1st level (4 slots): cure wounds, guiding bolt, sanctuary
|
||||
- 2nd level (3 slots): lesser restoration, spiritual weapon
|
||||
- 3rd level (2 slots): dispel magic, spirit guardians
|
||||
|
||||
Mace.
|
||||
|
||||
Melee Weapon Attack: +2 to hit, reach 5 ft., one target. Hit: 3 (1d6) bludgeoning damage.
|
||||
"""
|
||||
|
||||
name = "Priest"
|
||||
description = "Medium humanoid, any alignment"
|
||||
challenge_rating = 2
|
||||
|
||||
@@ -74,7 +74,6 @@ class BaseCharacterReader:
|
||||
The path to the file that will be imported.
|
||||
|
||||
"""
|
||||
print(filename)
|
||||
self.filename = filename
|
||||
|
||||
def __call__(self, filename):
|
||||
@@ -285,7 +284,7 @@ class Roll20CharacterReader(JSONCharacterReader):
|
||||
match = prof_re.match(obj["name"])
|
||||
if match:
|
||||
tool_profs.append(self.get_attrib(match.group(0)))
|
||||
char_props["_proficiencies_text"] = tool_profs
|
||||
char_props["proficiencies_text"] = tool_profs
|
||||
# Combat stats
|
||||
char_props["hp_max"] = self.as_int(self.get_attrib("hp", which="max"))
|
||||
# Equipment
|
||||
@@ -531,7 +530,7 @@ class FoundryCharacterReader(JSONCharacterReader):
|
||||
tool_profs = [tool_labels[prof] for prof in tool_profs]
|
||||
custom_tool_profs = json_data["data"]["traits"]["toolProf"]["custom"]
|
||||
tool_profs.extend([s.strip() for s in custom_tool_profs.split(";")])
|
||||
char_props["_proficiencies_text"] = tool_profs
|
||||
char_props["proficiencies_text"] = tool_profs
|
||||
# Combat stats
|
||||
char_props["hp_max"] = self.as_int(json_data["data"]["attributes"]["hp"]["max"])
|
||||
# Equipment
|
||||
|
||||
Reference in New Issue
Block a user