mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-03 19:38:30 +02:00
Added a content registry, so homebrew content can be referenced by name.
This commit is contained in:
@@ -12,7 +12,7 @@ __all__ = (
|
||||
|
||||
from dungeonsheets import background, features, race, spells, weapons, mechanics
|
||||
from dungeonsheets.character import Character
|
||||
from dungeonsheets.homebrew import import_homebrew
|
||||
from dungeonsheets.content_registry import import_homebrew
|
||||
|
||||
import os
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from dungeonsheets import (
|
||||
spells,
|
||||
weapons,
|
||||
)
|
||||
from dungeonsheets.stats import findattr
|
||||
from dungeonsheets.content_registry import find_content
|
||||
from dungeonsheets.weapons import Weapon
|
||||
from dungeonsheets.entity import Entity
|
||||
|
||||
@@ -120,7 +120,7 @@ def _resolve_mechanic(mechanic, module, SuperClass, warning_message=None):
|
||||
else:
|
||||
try:
|
||||
# Retrieve pre-defined mechanic
|
||||
Mechanic = findattr(module, mechanic)
|
||||
Mechanic = find_content(mechanic, valid_classes=[SuperClass])
|
||||
except AttributeError:
|
||||
# No pre-defined mechanic available
|
||||
if warning_message is not None:
|
||||
@@ -336,7 +336,7 @@ class Character(Entity):
|
||||
self._race = newrace(owner=self)
|
||||
elif isinstance(newrace, str):
|
||||
try:
|
||||
self._race = findattr(race, newrace)(owner=self)
|
||||
self._race = find_content(newrace, valid_classes=[race.Race])(owner=self)
|
||||
except AttributeError:
|
||||
msg = f'Race "{newrace}" not defined. Please add it to ``race.py``'
|
||||
self._race = race.Race(owner=self)
|
||||
@@ -357,7 +357,7 @@ class Character(Entity):
|
||||
self._background = bg(owner=self)
|
||||
elif isinstance(bg, str):
|
||||
try:
|
||||
self._background = findattr(background, bg)(owner=self)
|
||||
self._background = find_content(bg, valid_classes=[background.Background])(owner=self)
|
||||
except AttributeError:
|
||||
msg = (
|
||||
f'Background "{bg}" not defined. Please add it to ``background.py``'
|
||||
@@ -812,7 +812,7 @@ class Character(Entity):
|
||||
"""
|
||||
if shield not in ("", "None", None):
|
||||
try:
|
||||
NewShield = findattr(armor, shield)
|
||||
NewShield = find_content(shield, valid_classes=[armor.Shield])
|
||||
except AttributeError:
|
||||
# Not a string, so just treat it as Armor
|
||||
NewShield = shield
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections import defaultdict
|
||||
|
||||
from dungeonsheets import exceptions, features, monsters, weapons
|
||||
from dungeonsheets.classes.classes import CharClass, SubClass
|
||||
from dungeonsheets.stats import findattr
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
# PHB
|
||||
@@ -272,7 +272,7 @@ class Druid(CharClass):
|
||||
else:
|
||||
# Not already a monster so see if we can find one
|
||||
try:
|
||||
NewMonster = findattr(monsters, shape)
|
||||
NewMonster = find_content(shape, valid_classes=[monsters.Monster])
|
||||
new_shape = NewMonster()
|
||||
except AttributeError:
|
||||
msg = (
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
import importlib.util
|
||||
from typing import Union, List, Optional
|
||||
|
||||
from dungeonsheets import weapons, monsters, race, background, armor, spells, infusions, magic_items
|
||||
|
||||
|
||||
class ContentRegistry():
|
||||
modules = None
|
||||
|
||||
def __init__(self):
|
||||
self.modules = []
|
||||
|
||||
def add_module(self, new_module):
|
||||
if new_module not in self.modules:
|
||||
self.modules.append(new_module)
|
||||
|
||||
def findattr(self, name, valid_classes=[]):
|
||||
"""Resolve the name of a piece of content to the corresponding Class.
|
||||
|
||||
Similar to builtin getattr(obj, name) but more forgiving to
|
||||
whitespace and capitalization.
|
||||
|
||||
valid_classes
|
||||
If given, only subclasses of classes in this list will be
|
||||
returned.
|
||||
|
||||
"""
|
||||
# Come up with several options
|
||||
name = name.strip()
|
||||
# check for +X weapons, armor, shields
|
||||
bonus = 0
|
||||
for i in range(1, 11):
|
||||
if (f"+{i}" in name) or (f"+ {i}" in name):
|
||||
bonus = i
|
||||
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
|
||||
break
|
||||
py_name = name.replace("-", "_").replace(" ", "_").replace("'", "").replace("/", "")
|
||||
camel_case = "".join([s.capitalize() for s in py_name.split("_")])
|
||||
# Check each module in the registry
|
||||
found_attrs = []
|
||||
for module in self.modules:
|
||||
if hasattr(module, py_name):
|
||||
# Direct lookup
|
||||
found_attrs.append(getattr(module, py_name))
|
||||
elif hasattr(module, camel_case):
|
||||
# CamelCase lookup
|
||||
found_attrs.append(getattr(module, camel_case))
|
||||
# Filter by valid classes
|
||||
if len(valid_classes) > 0:
|
||||
is_valid = [False for attr in found_attrs]
|
||||
for cls in valid_classes:
|
||||
is_valid = [v or isinstance(attr, cls) or (isinstance(attr, type) and issubclass(attr, cls)) for v, attr in zip(is_valid, found_attrs)]
|
||||
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}")
|
||||
elif len(found_attrs) > 1:
|
||||
raise RuntimeError(f"Found multiple content entries for {name}")
|
||||
else:
|
||||
attr = found_attrs[0]
|
||||
# Apply weapon/etc. bonuses
|
||||
if bonus > 0:
|
||||
if (
|
||||
issubclass(attr, weapons.Weapon)
|
||||
or issubclass(attr, armor.Shield)
|
||||
or issubclass(attr, armor.Armor)
|
||||
):
|
||||
attr = attr.improved_version(bonus)
|
||||
return attr
|
||||
|
||||
|
||||
default_content_registry = ContentRegistry()
|
||||
default_content_registry.add_module(weapons)
|
||||
default_content_registry.add_module(monsters)
|
||||
default_content_registry.add_module(race)
|
||||
default_content_registry.add_module(background)
|
||||
default_content_registry.add_module(armor)
|
||||
default_content_registry.add_module(spells)
|
||||
default_content_registry.add_module(infusions)
|
||||
default_content_registry.add_module(magic_items)
|
||||
|
||||
|
||||
def find_content(name: str, valid_classes: Optional[List]):
|
||||
"""Find content from a previously registered module.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
name
|
||||
The name of the item to find. Can be "CamelCase", "under_case",
|
||||
or "lower case".
|
||||
valid_classes
|
||||
A list of parent classes to look for. If ``None`` or ``[]``, all
|
||||
classes will be considered valid.
|
||||
|
||||
"""
|
||||
if valid_classes is None:
|
||||
valid_classes = []
|
||||
return default_content_registry.findattr(name, valid_classes=valid_classes)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def import_homebrew(filepath: Union[str, Path]):
|
||||
"""Import a module file containing homebrew content.
|
||||
|
||||
This is intended to be used in a character/GM sheet to load in
|
||||
homebrew content defined in an external file.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
filepath
|
||||
The location of the python file containing the homebrew content.
|
||||
|
||||
Returns
|
||||
=======
|
||||
mod
|
||||
The imported module of homebrew content.
|
||||
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location("module.name", filepath)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
default_content_registry.add_module(mod)
|
||||
return mod
|
||||
@@ -156,7 +156,7 @@ def create_character_pdf_template(character, basename, flatten=False):
|
||||
fields[atk_field] = "{:+d}".format(weapon.attack_modifier)
|
||||
fields[dmg_field] = f"{weapon.damage}/{weapon.damage_type}"
|
||||
# Additional attacks beyond 3
|
||||
attack = [f"{w.name}: Atk {weapon.attack_modifier:+d}, Dam {weapon.damage}/{weapon.damage_type}"
|
||||
attack = [f"{w.name}: Atk {w.attack_modifier:+d}, Dam {w.damage}/{w.damage_type}"
|
||||
for w in character.weapons[len(weapon_fields):]]
|
||||
# Other attack information
|
||||
if character.armor:
|
||||
|
||||
@@ -1,28 +1,2 @@
|
||||
"""Tools useful for defining homebrew content."""
|
||||
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
from typing import Union
|
||||
|
||||
|
||||
def import_homebrew(filepath: Union[str, Path]):
|
||||
"""Import a module file containing homebrew content.
|
||||
|
||||
This is intended to be used in a character/GM sheet to load in
|
||||
homebrew content defined in an external file.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
filepath
|
||||
The location of the python file containing the homebrew content.
|
||||
|
||||
Returns
|
||||
=======
|
||||
mod
|
||||
The imported module of homebrew content.
|
||||
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location("module.name", filepath)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
@@ -14,7 +14,8 @@ from typing import Union, Sequence, Optional
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
from dungeonsheets import character as _char, exceptions, readers, latex, monsters
|
||||
from dungeonsheets.stats import mod_str, findattr
|
||||
from dungeonsheets.stats import mod_str
|
||||
from dungeonsheets.content_registry import find_content
|
||||
from dungeonsheets.fill_pdf_template import (
|
||||
create_character_pdf_template,
|
||||
create_personality_pdf_template,
|
||||
@@ -219,7 +220,7 @@ def make_gm_sheet(
|
||||
new_monster = monster
|
||||
else:
|
||||
try:
|
||||
MyMonster = findattr(monsters, monster)
|
||||
MyMonster = find_content(monster, valid_classes=[monsters.Monster])
|
||||
except AttributeError:
|
||||
msg = (
|
||||
f"Monster '{monster}' not found. Please add it to"
|
||||
|
||||
@@ -34,7 +34,7 @@ def read_sheet_file(filename: Union[str, Path]) -> dict:
|
||||
Dictionary with the import character properties.
|
||||
|
||||
"""
|
||||
filename = Path(filename)
|
||||
filename = Path(filename).resolve()
|
||||
# Parse the file name
|
||||
ext = filename.suffix
|
||||
try:
|
||||
@@ -52,6 +52,9 @@ def read_sheet_file(filename: Union[str, Path]) -> dict:
|
||||
parent_props = read_sheet_file(parent_sheet)
|
||||
char_props.update(parent_props)
|
||||
char_props.update(these_props)
|
||||
# Remove imported dungeonsheets modules
|
||||
char_props.pop("import_homebrew", None)
|
||||
char_props.pop("mechanics", None)
|
||||
return char_props
|
||||
|
||||
|
||||
|
||||
@@ -27,40 +27,6 @@ from dungeonsheets.features import (
|
||||
from dungeonsheets.weapons import Weapon
|
||||
|
||||
|
||||
def findattr(obj, name):
|
||||
"""Similar to builtin getattr(obj, name) but more forgiving to
|
||||
whitespace and capitalization.
|
||||
|
||||
"""
|
||||
# Come up with several options
|
||||
name = name.strip()
|
||||
# check for +X weapons, armor, shields
|
||||
bonus = 0
|
||||
for i in range(1, 11):
|
||||
if (f"+{i}" in name) or (f"+ {i}" in name):
|
||||
bonus = i
|
||||
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
|
||||
break
|
||||
py_name = name.replace("-", "_").replace(" ", "_").replace("'", "").replace("/", "")
|
||||
camel_case = "".join([s.capitalize() for s in py_name.split("_")])
|
||||
if hasattr(obj, py_name):
|
||||
# Direct lookup
|
||||
attr = getattr(obj, py_name)
|
||||
elif hasattr(obj, camel_case):
|
||||
# CamelCase lookup
|
||||
attr = getattr(obj, camel_case)
|
||||
else:
|
||||
raise AttributeError(f"{obj} has no attribute {name}")
|
||||
if bonus > 0:
|
||||
if (
|
||||
issubclass(attr, Weapon)
|
||||
or issubclass(attr, Shield)
|
||||
or issubclass(attr, Armor)
|
||||
):
|
||||
attr = attr.improved_version(bonus)
|
||||
return attr
|
||||
|
||||
|
||||
def mod_str(modifier):
|
||||
"""Converts a modifier to a string, eg 2 -> '+2'."""
|
||||
return "{:+d}".format(modifier)
|
||||
|
||||
Reference in New Issue
Block a user