diff --git a/VERSION b/VERSION index 0548fb4..7092c7c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.14.0 \ No newline at end of file +0.15.0 \ No newline at end of file diff --git a/docs/advanced_features.rst b/docs/advanced_features.rst new file mode 100644 index 0000000..ca6c1f8 --- /dev/null +++ b/docs/advanced_features.rst @@ -0,0 +1,122 @@ +=================== + Advanced Features +=================== + +.. warning:: + + Character and GM files are python modules that are imported when + parsed. **NEVER parse a character file without inspecting it** to + verify that there are no unexpected consequences, especially a file + from someone you do not trust. + +Homebrew +======== + +Dungeonsheets provides mechanisms for including items and abilities +outside of the standard rules ("homebrew"). This can be done in one of +two ways. + +1. As subclasses (recommended) +2. As strings + +Subclasses (Recommended) +------------------------ + +The best option is to define your homebrew item directly in the +character file as a subclass of one of the basic mechanics: + +- :py:class:`dungeonsheets.spells.Spell` +- :py:class:`dungeonsheets.features.Feature` +- :py:class:`dungeonsheets.infusions.Infusion` +- :py:class:`dungeonsheets.weapons.Weapon` +- :py:class:`dungeonsheets.armor.Armor` +- :py:class:`dungeonsheets.armor.Shield` +- :py:class:`dungeonsheets.magic_items.MagicItem` + +For convenience, these are all available in the +:py:mod:`dungeonsheets.mechanics` module. With this approach, a +homebrew weapon can be specified in the character file. See the +relevant super class for relevant attributes. + +.. code:: python + + from dungeonsheets import mechanics + + class DullSword(mechanics.Weapon): + """Bonk things with it.""" + name = "Dullsword" + base_damage = "10d6" + + weapons = ['shortsword', DullSword] + +These homebrew definitions can also be stored in a separate file +(e.g. *my_homebrew.py*), then imported and used in multiple character +files: + +.. code:: python + + from dungeonsheets import import_homebrew + + + my_homebrew = import_homebrew("my_campaign.py") + + weapons = ["shortsword", my_homebrew.DullSword] + +See the :ref:`homebrew example` example for more examples. + +Strings +------- + +If a mechanic is listed in a character file, but not built into +dungeonsheets, it will still be listed on the character sheet with +generic attributes. This should be viewed as a fallback to the +recommended subclass method above, so that attributes and descriptions +can be given. + + +Roll20 (VTTES) and Foundry JSON Files +===================================== + +Dungeonsheets has partial support for reading JSON files exported +either from roll20.net using the `VTTES browser extension`_, or +directly from `Foundry VTT`_ by choosing *export data* from the +actor's right-click menu. This allows character sheets to be exported +from roll20.net and foundry, and then rendered into full character +sheets. + +.. _VTTES browser extension: https://wiki.5e.tools/index.php/R20es_Install_Guide + +.. _Foundry VTT: https://foundryvtt.com/article/actors/ + + +Cascading Sheets +================ + +Character and GM sheet files can **inherit from other character and GM +files**. This has two primary use cases: + +1. A parent GM sheet can be made for a campaign, and then child sheets + can provide only the specific details needed for each session. +2. When importing JSON files from roll20 or Foundry VTT, missing + features (e.g. Druid wild shapes) can be added manually. + +Sheet cascading is activated by using the ``parent_sheets`` attribute +in a python sheet file, which should be a list of paths to other +sheets (either ``.py`` or ``.json``): + + + +.. code-block:: python + :caption: gm_session1_notes.py + + dungeonsheets_version = "0.15.0" + monsters = ['giant eagle', 'wolf', 'goblin'] + parent_sheets = ['gm_generic_notes.py'] + + +.. code-block:: python + :caption: gm_generic_notes.py + + dungeonsheets_version = "0.15.0" + party = ["rogue1.py", "paladin2.py", ...] + diff --git a/docs/character_files.rst b/docs/character_files.rst index fcab000..710e449 100644 --- a/docs/character_files.rst +++ b/docs/character_files.rst @@ -263,86 +263,6 @@ attribute of the character file: infusions = ["enhanced_arcane_focus", "repulsion_shield"] - -Homebrew -======== - -Dungeonsheets provides mechanisms for including items and abilities -outside of the standard rules ("homebrew"). This can be done in one of -two ways. - -1. As subclasses (recommended) -2. As strings - -Subclasses (Recommended) ------------------------- - -The best option is to define your homebrew item directly in the -character file as a subclass of one of the basic mechanics: - -- :py:class:`dungeonsheets.spells.Spell` -- :py:class:`dungeonsheets.features.Feature` -- :py:class:`dungeonsheets.infusions.Infusion` -- :py:class:`dungeonsheets.weapons.Weapon` -- :py:class:`dungeonsheets.armor.Armor` -- :py:class:`dungeonsheets.armor.Shield` -- :py:class:`dungeonsheets.magic_items.MagicItem` - -For convenience, these are all available in the -:py:mod:`dungeonsheets.mechanics` module. With this approach, a -homebrew weapon can be specified in the character file. See the -relevant super class for relevant attributes. - -.. code:: python - - from dungeonsheets import mechanics - - class DullSword(mechanics.Weapon): - """Bonk things with it.""" - name = "Dullsword" - base_damage = "10d6" - - weapons = ['shortsword', DullSword] - -These homebrew definitions can also be stored in a separate file -(e.g. *my_homebrew.py*), then imported and used in multiple character -files: - -.. code:: python - - from dungeonsheets import import_homebrew - - - my_homebrew = import_homebrew("my_campaign.py") - - weapons = ["shortsword", my_homebrew.DullSword] - -See the :ref:`homebrew example` example for more examples. - -Strings -------- - -If a mechanic is listed in a character file, but not built into -dungeonsheets, it will still be listed on the character sheet with -generic attributes. This should be viewed as a fallback to the -recommended subclass method above, so that attributes and descriptions -can be given. - - -Roll20 (VTTES) and Foundry JSON Files -===================================== - -Dungeonsheets has partial support for reading JSON files exported -either from roll20.net using the `VTTES browser extension`_, or -directly from `Foundry VTT`_ by choosing *export data* from the -actor's right-click menu. This allows character sheets to be exported -from roll20.net and foundry, and then rendered into full character -sheets. - .. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook .. _issue: https://github.com/canismarko/dungeon-sheets/issues - -.. _VTTES browser extension: https://wiki.5e.tools/index.php/R20es_Install_Guide - -.. _Foundry VTT: https://foundryvtt.com/article/actors/ diff --git a/docs/index.rst b/docs/index.rst index 08d7556..411c144 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,7 @@ Welcome to Dungeonsheets's documentation! character_files gm_notes + advanced_features examples Indices and tables diff --git a/dungeonsheets/readers.py b/dungeonsheets/readers.py index 11cd59f..3090ffd 100644 --- a/dungeonsheets/readers.py +++ b/dungeonsheets/readers.py @@ -4,6 +4,7 @@ import json import re from functools import lru_cache import logging +from typing import Union from pathlib import Path @@ -12,18 +13,26 @@ from dungeonsheets import exceptions log = logging.getLogger(__file__) -def read_sheet_file(filename: str): +def read_sheet_file(filename: Union[str, Path]) -> dict: """Create a character object from the given definition file. The definition file should be an importable python file or a JSON file following one of the supported formats, filled with variables describing the character. + This function also resolves any *parent_sheets* attributes in the + given sheet, loading parent sheets and updating those attributes. + Parameters ---------- filename The path to the file that will be imported. + Returns + ------- + char_props + Dictionary with the import character properties. + """ filename = Path(filename) # Parse the file name @@ -33,8 +42,17 @@ def read_sheet_file(filename: str): except KeyError: raise ValueError(f"Character definition {filename} is not a known file type.") else: - new_char = reader() - return new_char + these_props = reader() + # Resolve parent_sheets + char_props = {} + parent_sheets = these_props.pop('parent_sheets', []) + for parent_sheet in parent_sheets: + parent_sheet = Path(parent_sheet) + if parent_sheet != filename: + parent_props = read_sheet_file(parent_sheet) + char_props.update(parent_props) + char_props.update(these_props) + return char_props class BaseCharacterReader: diff --git a/tests/test_readers.py b/tests/test_readers.py index 9f3c8ba..d1ac3bf 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -2,6 +2,7 @@ import warnings from pathlib import Path import unittest import types +from contextlib import contextmanager from dungeonsheets import exceptions from dungeonsheets.readers import read_sheet_file @@ -15,6 +16,35 @@ SPELLCASTER_JSON_FILE = EG_DIR / "artificer2.json" class PythonReaderTests(unittest.TestCase): + @contextmanager + def inherited_sheets(self): + """Create some cascading sheets to be inherited.""" + child = Path("child_sheet.py") + parent = Path("parent_sheet.py") + # Write inheritance files + with open(parent, mode="w") as fp: + fp.writelines("\n".join([ + "dungeonsheets_version = '0.15.0'", + "background = 'entertainer'", + ]) + "\n") + with open(child, mode="w") as fp: + fp.writelines("\n".join([ + "dungeonsheets_version = '0.15.0'", + "name = 'Douglass Adams'", + f"parent_sheets = ['{str(parent)}']", + ]) + "\n") + # Drop back into the calling code + yield child, parent + # Remove temporary files + child.unlink() + parent.unlink() + + def test_cascading_sheets(self): + with self.inherited_sheets() as (child, parent): + char_props = read_sheet_file(child) + self.assertEqual(char_props["name"], "Douglass Adams") + self.assertEqual(char_props["background"], "entertainer") + def test_load_python_gm_sheet(self): gmfile = GM_PYTHON_FILE result = read_sheet_file(gmfile)