mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
219 lines
7.3 KiB
Python
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
|