Files
dungeon-sheets/dungeonsheets/content.py
T

219 lines
7.3 KiB
Python

"""Base classes for the various D&D 5e content types."""
import warnings
from abc import ABC
from pathlib import Path
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
from dungeonsheets.content_registry import find_content
def read(fname):
return open((Path(__file__).parent / fname).resolve()).read()
__version__ = read("../VERSION").strip()
class Content(ABC):
"""A base class for all D&D 5e content types.
Every piece of content (e.g. class feature, spell, monster) should
have this base class in its inheritance tree.
"""
dungeonsheets_version = __version__
name = "Generic content"
@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
class Creature(Content):
"""A thing with stats. Use Monster or Character, not this class
directly!
"""
# General attributes
alignment = "Neutral"
_race = None
name = "Generic creature"
# Hit points
hp_max = None
# Base stats (ability scores)
strength = Ability()
dexterity = Ability()
constitution = Ability()
intelligence = Ability()
wisdom = Ability()
charisma = Ability()
# Numerical things
armor_class = ArmorClass()
initiative = Initiative()
speed = Speed()
# Proficiencies and Languages
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
other_weapon_proficiencies = tuple() # add to class/race proficiencies
skill_proficiencies = list()
skill_expertise = list()
languages = ""
senses = ""
# 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")
# Inventory
cp = 0
sp = 0
ep = 0
gp = 0
pp = 0
equipment = ""
_weapons = list()
magic_items = list()
armor = None
shield = None
# Magic
spellcasting_ability = None
_spells = list()
_spells_prepared = list()
infusions = list()
# Features IN MAJOR DEVELOPMENT
custom_features = list()
feature_choices = list()
def __init__(self):
pass
@property
def weapons(self):
return self._weapons.copy()
@property
def passive_wisdom(self):
return self.perception.modifier + 10
@property
def passive_perception(self):
"""Just a wrapper around passive wisdom."""
return self.passive_wisdom
@property
def passive_insight(self):
return self.insight.modifier + 10
@property
def passive_investigation(self):
return self.investigation.modifier + 10
@property
def abilities(self):
return [self.strength, self.dexterity, self.constitution,
self.intelligence, self.wisdom, self.charisma]
@property
def skills(self):
return [self.acrobatics, self.animal_handling, self.arcana,
self.athletics, self.deception, self.history,
self.insight, self.intimidation, self.investigation,
self.medicine, self.nature, self.perception,
self.performance, self.persuasion, self.religion,
self.sleight_of_hand, self.stealth, self.survival]
@property
def is_spellcaster(self):
raise NotImplementedError