mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-06-07 05:03:31 +02:00
Added ability to parse JSON files exported from Foundry.
This commit is contained in:
@@ -326,16 +326,20 @@ recommended subclass method above, so that attributes and descriptions
|
|||||||
can be given.
|
can be given.
|
||||||
|
|
||||||
|
|
||||||
VTTES JSON Files
|
Roll20 (VTTES) and Foundry JSON Files
|
||||||
================
|
=====================================
|
||||||
|
|
||||||
Dungeonsheets has partial support for reading JSON files exporting
|
Dungeonsheets has partial support for reading JSON files exported
|
||||||
using the `VTTES browser extension`_. This allows character sheets to
|
either from roll20.net using the `VTTES browser extension`_, or
|
||||||
be exported from systems like Roll20.net, and then rendered into full
|
directly from `Foundry VTT`_ by choosing *export data* from the
|
||||||
character sheets.
|
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
|
.. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
|
||||||
|
|
||||||
.. _issue: https://github.com/canismarko/dungeon-sheets/issues
|
.. _issue: https://github.com/canismarko/dungeon-sheets/issues
|
||||||
|
|
||||||
.. _VTTES browser extension: https://wiki.5e.tools/index.php/R20es_Install_Guide
|
.. _VTTES browser extension: https://wiki.5e.tools/index.php/R20es_Install_Guide
|
||||||
|
|
||||||
|
.. _Foundry VTT: https://foundryvtt.com/article/actors/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import re
|
|||||||
import warnings
|
import warnings
|
||||||
import math
|
import math
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
@@ -223,13 +224,38 @@ class Character:
|
|||||||
custom_features = list()
|
custom_features = list()
|
||||||
feature_choices = list()
|
feature_choices = list()
|
||||||
|
|
||||||
def __init__(self, **attrs):
|
def __init__(
|
||||||
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
|
self,
|
||||||
|
classes: Sequence = [],
|
||||||
|
levels: Sequence[int] = [],
|
||||||
|
subclasses: Sequence = [],
|
||||||
|
**attrs,
|
||||||
|
):
|
||||||
|
"""Create a new character from attributes *attrs*.
|
||||||
|
|
||||||
|
**Multiclassing** can be accomplished by a list of class names
|
||||||
|
*classes*, and a list of class levels *levels*.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
==========
|
||||||
|
classes
|
||||||
|
Strings with class names, or character class definitions
|
||||||
|
representing the characters various D&D classes.
|
||||||
|
levels
|
||||||
|
The class levels for each corresponding class entry in
|
||||||
|
*classes*.
|
||||||
|
subclasses
|
||||||
|
Subclasses that apply for this character.
|
||||||
|
**attrs
|
||||||
|
Additional keyword parameters to set as attributes for this
|
||||||
|
character.
|
||||||
|
|
||||||
|
"""
|
||||||
self.clear()
|
self.clear()
|
||||||
# make sure class, race, background are set first
|
# make sure class, race, background are set first
|
||||||
my_classes = attrs.pop("classes", [])
|
my_classes = classes
|
||||||
my_levels = attrs.pop("levels", [])
|
my_levels = levels
|
||||||
my_subclasses = attrs.pop("subclasses", [])
|
my_subclasses = subclasses
|
||||||
# backwards compatability
|
# backwards compatability
|
||||||
if len(my_classes) == 0:
|
if len(my_classes) == 0:
|
||||||
if "class" in attrs:
|
if "class" in attrs:
|
||||||
@@ -281,8 +307,9 @@ class Character:
|
|||||||
cls: (classes.CharClass, type, str),
|
cls: (classes.CharClass, type, str),
|
||||||
level: (int, str),
|
level: (int, str),
|
||||||
subclass=None,
|
subclass=None,
|
||||||
feature_choices=[],
|
feature_choices: Sequence = [],
|
||||||
):
|
):
|
||||||
|
"""Add a class, level, and subclass the character has attained."""
|
||||||
if isinstance(cls, str):
|
if isinstance(cls, str):
|
||||||
cls = cls.strip().title().replace(" ", "")
|
cls = cls.strip().title().replace(" ", "")
|
||||||
try:
|
try:
|
||||||
@@ -298,8 +325,18 @@ class Character:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def add_classes(
|
def add_classes(
|
||||||
self, classes_list=[], levels=[], subclasses=[], feature_choices=[]
|
self,
|
||||||
|
classes_list: Sequence[Union[str, classes.CharClass]] = [],
|
||||||
|
levels: Sequence[Union[int, float, str]] = [],
|
||||||
|
subclasses: Sequence = [],
|
||||||
|
feature_choices: Sequence = [],
|
||||||
):
|
):
|
||||||
|
"""Add several classes, levels, etc.
|
||||||
|
|
||||||
|
The lists can also be single values for a single class
|
||||||
|
character.
|
||||||
|
|
||||||
|
"""
|
||||||
if isinstance(classes_list, str):
|
if isinstance(classes_list, str):
|
||||||
classes_list = [classes_list]
|
classes_list = [classes_list]
|
||||||
if (
|
if (
|
||||||
@@ -909,7 +946,7 @@ class Character:
|
|||||||
return ()
|
return ()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, character_file):
|
def load(Cls, character_file):
|
||||||
# Create a character from the character definition
|
# Create a character from the character definition
|
||||||
char_props = read_character_file(character_file)
|
char_props = read_character_file(character_file)
|
||||||
classes = char_props.get("classes", [])
|
classes = char_props.get("classes", [])
|
||||||
@@ -920,7 +957,7 @@ class Character:
|
|||||||
]
|
]
|
||||||
char_props["levels"] = [str(char_props.pop("level"))]
|
char_props["levels"] = [str(char_props.pop("level"))]
|
||||||
# Create the character with loaded properties
|
# Create the character with loaded properties
|
||||||
char = Character(**char_props)
|
char = Cls(**char_props)
|
||||||
return char
|
return char
|
||||||
|
|
||||||
def save(self, filename, template_file="character_template.txt"):
|
def save(self, filename, template_file="character_template.txt"):
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ def text_box(string):
|
|||||||
)
|
)
|
||||||
return new_string
|
return new_string
|
||||||
|
|
||||||
|
|
||||||
def create_character_pdf_template(character, basename, flatten=False):
|
def create_character_pdf_template(character, basename, flatten=False):
|
||||||
# Prepare the list of fields
|
# Prepare the list of fields
|
||||||
fields = {
|
fields = {
|
||||||
@@ -445,6 +446,7 @@ def create_spells_pdf_template(character, basename, flatten=False):
|
|||||||
src_pdf = os.path.join(dirname, "blank-spell-sheet-default.pdf")
|
src_pdf = os.path.join(dirname, "blank-spell-sheet-default.pdf")
|
||||||
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
|
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
|
||||||
|
|
||||||
|
|
||||||
def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False):
|
def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False):
|
||||||
"""Create a new PDF by applying fields to a src PDF document.
|
"""Create a new PDF by applying fields to a src PDF document.
|
||||||
|
|
||||||
@@ -556,4 +558,4 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
|
|||||||
popenargs.append("flatten")
|
popenargs.append("flatten")
|
||||||
subprocess.call(popenargs)
|
subprocess.call(popenargs)
|
||||||
# Clean up temporary files
|
# Clean up temporary files
|
||||||
os.remove(fdfname)
|
os.remove(fdfname)
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ def _remove_temp_files(basename_):
|
|||||||
filename.unlink()
|
filename.unlink()
|
||||||
|
|
||||||
|
|
||||||
def create_latex_pdf(
|
def create_latex_pdf(tex, basename, keep_temp_files=False, use_dnd_decorations=False):
|
||||||
tex, basename, keep_temp_files=False, use_dnd_decorations=False
|
|
||||||
):
|
|
||||||
# Create tex document
|
# Create tex document
|
||||||
tex_file = f"{basename}.tex"
|
tex_file = f"{basename}.tex"
|
||||||
with open(tex_file, mode="w", encoding="utf-8") as f:
|
with open(tex_file, mode="w", encoding="utf-8") as f:
|
||||||
|
|||||||
+57
-104
@@ -21,105 +21,25 @@ from dungeonsheets.fill_pdf_template import (
|
|||||||
)
|
)
|
||||||
from dungeonsheets.character import Character
|
from dungeonsheets.character import Character
|
||||||
|
|
||||||
|
"""Program to take character definitions and build a PDF of the
|
||||||
|
character sheet."""
|
||||||
|
|
||||||
PDFTK_CMD = "pdftk"
|
PDFTK_CMD = "pdftk"
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
ORDINALS = {
|
ORDINALS = {
|
||||||
1: "1st",
|
1: "1st",
|
||||||
2: "2nd",
|
2: "2nd",
|
||||||
3: "3rd",
|
3: "3rd",
|
||||||
4: "4th",
|
4: "4th",
|
||||||
5: "5th",
|
5: "5th",
|
||||||
6: "6th",
|
6: "6th",
|
||||||
7: "7th",
|
7: "7th",
|
||||||
8: "8th",
|
8: "8th",
|
||||||
9: "9th"
|
9: "9th",
|
||||||
}
|
}
|
||||||
|
|
||||||
"""Program to take character definitions and build a PDF of the
|
|
||||||
character sheet."""
|
|
||||||
|
|
||||||
bold_re = re.compile(r"\*\*([^*]+)\*\*")
|
|
||||||
it_re = re.compile(r"\*([^*]+)\*")
|
|
||||||
verb_re = re.compile(r"``([^`]+)``")
|
|
||||||
heading_re = re.compile(r"^[ \t\r\f\v]*(.+)\n\s*([-=\^]+)$", flags=re.MULTILINE)
|
|
||||||
# What defines a list in reST:
|
|
||||||
# - a blank line
|
|
||||||
# - one or more of the following
|
|
||||||
# - "- [a-z]"
|
|
||||||
# - an additional line of text (if multi-line bullets)
|
|
||||||
# - a blank line
|
|
||||||
# - a blank line
|
|
||||||
# - a non-list line or end of file
|
|
||||||
# list_re = re.compile('^[ \t\r\f\v]*\n((?:\s*[-*+]\s+[^\n]*\n)+)', flags=re.MULTILINE)
|
|
||||||
list_re = re.compile(
|
|
||||||
r"^[ \t\r\f\v]*\n" # A blank line
|
|
||||||
r"((?:\s*[-*+]\s+[^\n]*\n)+)" # The first line of each list item
|
|
||||||
"",
|
|
||||||
flags=re.MULTILINE,
|
|
||||||
)
|
|
||||||
# What defines a list item in reST:
|
|
||||||
# - a line starting with "- " then some text
|
|
||||||
# - zero or more lines starting with anything other than "- "
|
|
||||||
list_item_re = re.compile(r"^\s*[-*+]\s+", flags=re.MULTILINE)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_rst_lists(rst):
|
|
||||||
"""Read lists in reST and iterate.
|
|
||||||
|
|
||||||
Yields
|
|
||||||
======
|
|
||||||
list_rst : str
|
|
||||||
The matching reST list found in the input text
|
|
||||||
list_items : list
|
|
||||||
A python list of the items found in the reST list.
|
|
||||||
|
|
||||||
"""
|
|
||||||
for match in list_re.finditer(rst):
|
|
||||||
list_rst = match.group(0)
|
|
||||||
# Separate the list items
|
|
||||||
list_items = list_item_re.split(match.group(1))
|
|
||||||
# Clean up separated list items
|
|
||||||
list_items = list_items[1:] # First item is an empty string
|
|
||||||
list_items = [item.replace("\n", " ").strip() for item in list_items]
|
|
||||||
yield list_rst, list_items
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_rst_headings(rst):
|
|
||||||
"""Read headings in reST and iterate.
|
|
||||||
|
|
||||||
Yields
|
|
||||||
======
|
|
||||||
heading_rst : str
|
|
||||||
The matching reST heading found in the input text.
|
|
||||||
heading : str
|
|
||||||
The text of the heading with underlining removed.
|
|
||||||
level : int
|
|
||||||
How deep the heading is: 0 is top-level, 1 is next level down,
|
|
||||||
etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
heading_levels = {
|
|
||||||
"=": 0,
|
|
||||||
"-": 1,
|
|
||||||
"^": 2,
|
|
||||||
}
|
|
||||||
for match in heading_re.finditer(rst):
|
|
||||||
heading_rst = match.group(0)
|
|
||||||
heading, underline = match.groups()
|
|
||||||
# Check for valid heading
|
|
||||||
if len(underline) < len(heading):
|
|
||||||
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
|
|
||||||
continue
|
|
||||||
if len(set(underline)) > 1:
|
|
||||||
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
|
|
||||||
continue
|
|
||||||
# Valid heading, so determine how many levels deep it is
|
|
||||||
level = heading_levels[underline[0]]
|
|
||||||
yield heading_rst, heading, level
|
|
||||||
|
|
||||||
|
|
||||||
jinja_env = Environment(
|
jinja_env = Environment(
|
||||||
loader=PackageLoader("dungeonsheets", "forms"),
|
loader=PackageLoader("dungeonsheets", "forms"),
|
||||||
block_start_string="[%",
|
block_start_string="[%",
|
||||||
@@ -141,6 +61,7 @@ def create_subclasses_tex(
|
|||||||
template = jinja_env.get_template("subclasses_template.tex")
|
template = jinja_env.get_template("subclasses_template.tex")
|
||||||
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
||||||
|
|
||||||
|
|
||||||
def create_features_tex(
|
def create_features_tex(
|
||||||
character: Character,
|
character: Character,
|
||||||
use_dnd_decorations: bool = False,
|
use_dnd_decorations: bool = False,
|
||||||
@@ -148,6 +69,7 @@ def create_features_tex(
|
|||||||
template = jinja_env.get_template("features_template.tex")
|
template = jinja_env.get_template("features_template.tex")
|
||||||
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
||||||
|
|
||||||
|
|
||||||
def create_magic_items_tex(
|
def create_magic_items_tex(
|
||||||
character: Character,
|
character: Character,
|
||||||
use_dnd_decorations: bool = False,
|
use_dnd_decorations: bool = False,
|
||||||
@@ -155,12 +77,16 @@ def create_magic_items_tex(
|
|||||||
template = jinja_env.get_template("magic_items_template.tex")
|
template = jinja_env.get_template("magic_items_template.tex")
|
||||||
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
||||||
|
|
||||||
|
|
||||||
def create_spellbook_tex(
|
def create_spellbook_tex(
|
||||||
character: Character,
|
character: Character,
|
||||||
use_dnd_decorations: bool = False,
|
use_dnd_decorations: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
template = jinja_env.get_template("spellbook_template.tex")
|
template = jinja_env.get_template("spellbook_template.tex")
|
||||||
return template.render(character=character, ordinals=ORDINALS, use_dnd_decorations=use_dnd_decorations)
|
return template.render(
|
||||||
|
character=character, ordinals=ORDINALS, use_dnd_decorations=use_dnd_decorations
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_infusions_tex(
|
def create_infusions_tex(
|
||||||
character: Character,
|
character: Character,
|
||||||
@@ -169,6 +95,7 @@ def create_infusions_tex(
|
|||||||
template = jinja_env.get_template("infusions_template.tex")
|
template = jinja_env.get_template("infusions_template.tex")
|
||||||
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
|
||||||
|
|
||||||
|
|
||||||
def create_druid_shapes_tex(
|
def create_druid_shapes_tex(
|
||||||
character: Character,
|
character: Character,
|
||||||
use_dnd_decorations: bool = False,
|
use_dnd_decorations: bool = False,
|
||||||
@@ -211,7 +138,11 @@ def make_sheet(
|
|||||||
char_base = os.path.splitext(character_file)[0] + "_char"
|
char_base = os.path.splitext(character_file)[0] + "_char"
|
||||||
sheets = [char_base + ".pdf"]
|
sheets = [char_base + ".pdf"]
|
||||||
pages = []
|
pages = []
|
||||||
tex = [jinja_env.get_template("preamble.tex").render(use_dnd_decorations=fancy_decorations)]
|
tex = [
|
||||||
|
jinja_env.get_template("preamble.tex").render(
|
||||||
|
use_dnd_decorations=fancy_decorations
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
# Start of PDF gen
|
# Start of PDF gen
|
||||||
char_pdf = create_character_pdf_template(
|
char_pdf = create_character_pdf_template(
|
||||||
@@ -230,33 +161,54 @@ def make_sheet(
|
|||||||
features_base = "{:s}_features".format(os.path.splitext(character_file)[0])
|
features_base = "{:s}_features".format(os.path.splitext(character_file)[0])
|
||||||
# Create a list of subcasses
|
# Create a list of subcasses
|
||||||
if character.subclasses:
|
if character.subclasses:
|
||||||
tex.append(create_subclasses_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_subclasses_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
# Create a list of features
|
# Create a list of features
|
||||||
if character.features:
|
if character.features:
|
||||||
tex.append(create_features_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_features_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
if character.magic_items:
|
if character.magic_items:
|
||||||
tex.append(create_magic_items_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_magic_items_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
# Create a list of spells
|
# Create a list of spells
|
||||||
if character.is_spellcaster:
|
if character.is_spellcaster:
|
||||||
tex.append(create_spellbook_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_spellbook_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
# Create a list of Artificer infusions
|
# Create a list of Artificer infusions
|
||||||
if getattr(character, "infusions", []):
|
if getattr(character, "infusions", []):
|
||||||
tex.append(create_infusions_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_infusions_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
|
)
|
||||||
|
|
||||||
# Create a list of Druid wild_shapes
|
# Create a list of Druid wild_shapes
|
||||||
if getattr(character, "wild_shapes", []):
|
if getattr(character, "wild_shapes", []):
|
||||||
tex.append(create_druid_shapes_tex(character, use_dnd_decorations=fancy_decorations))
|
tex.append(
|
||||||
|
create_druid_shapes_tex(character, use_dnd_decorations=fancy_decorations)
|
||||||
tex.append(jinja_env.get_template("postamble.tex").render(use_dnd_decorations=fancy_decorations))
|
)
|
||||||
|
|
||||||
|
tex.append(
|
||||||
|
jinja_env.get_template("postamble.tex").render(
|
||||||
|
use_dnd_decorations=fancy_decorations
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Typeset combined LaTeX file
|
# Typeset combined LaTeX file
|
||||||
try:
|
try:
|
||||||
if len(tex) > 2:
|
if len(tex) > 2:
|
||||||
latex.create_latex_pdf("".join(tex), features_base, keep_temp_files=debug, use_dnd_decorations=fancy_decorations)
|
latex.create_latex_pdf(
|
||||||
|
"".join(tex),
|
||||||
|
features_base,
|
||||||
|
keep_temp_files=debug,
|
||||||
|
use_dnd_decorations=fancy_decorations,
|
||||||
|
)
|
||||||
sheets.append(features_base + ".pdf")
|
sheets.append(features_base + ".pdf")
|
||||||
final_pdf = os.path.splitext(character_file)[0] + ".pdf"
|
final_pdf = os.path.splitext(character_file)[0] + ".pdf"
|
||||||
merge_pdfs(sheets, final_pdf, clean_up=True)
|
merge_pdfs(sheets, final_pdf, clean_up=True)
|
||||||
@@ -293,7 +245,8 @@ def merge_pdfs(src_filenames, dest_filename, clean_up=False):
|
|||||||
os.remove(sheet)
|
os.remove(sheet)
|
||||||
|
|
||||||
|
|
||||||
load_character_file = readers.read_character_file
|
# # Deprecated:
|
||||||
|
# load_character_file = readers.read_character_file
|
||||||
|
|
||||||
|
|
||||||
def _build(filename, args) -> int:
|
def _build(filename, args) -> int:
|
||||||
|
|||||||
+305
-44
@@ -7,7 +7,6 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from dungeonsheets import exceptions
|
from dungeonsheets import exceptions
|
||||||
|
|
||||||
log = logging.getLogger(__file__)
|
log = logging.getLogger(__file__)
|
||||||
@@ -30,17 +29,28 @@ def read_character_file(filename: str):
|
|||||||
# Parse the file name
|
# Parse the file name
|
||||||
ext = filename.suffix
|
ext = filename.suffix
|
||||||
try:
|
try:
|
||||||
reader = readers_by_extension[ext]()
|
reader = readers_by_extension[ext](filename=filename)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"Character definition {filename} is not a known file type.")
|
raise ValueError(f"Character definition {filename} is not a known file type.")
|
||||||
else:
|
else:
|
||||||
new_char = reader(filename=filename)
|
new_char = reader()
|
||||||
return new_char
|
return new_char
|
||||||
|
|
||||||
|
|
||||||
class BaseCharacterReader:
|
class BaseCharacterReader:
|
||||||
"""Callable to parse a generic character file. Meant to be subclassed."""
|
"""Callable to parse a generic character file. Meant to be subclassed."""
|
||||||
|
|
||||||
|
def __init__(self, filename: str):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename
|
||||||
|
The path to the file that will be imported.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
def __call__(self, filename):
|
def __call__(self, filename):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
@@ -51,17 +61,24 @@ class BaseCharacterReader:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def json_character_reader(filename: str):
|
||||||
|
"""Factory to create a JSON reader of the correct sub-type."""
|
||||||
|
# Try and extract the schema version to see if it's valid
|
||||||
|
json_reader = Roll20CharacterReader(filename)
|
||||||
|
try:
|
||||||
|
json_reader.schema_version
|
||||||
|
except KeyError:
|
||||||
|
# Assume it's a foundry file
|
||||||
|
json_reader = FoundryCharacterReader(filename)
|
||||||
|
return json_reader
|
||||||
|
|
||||||
|
|
||||||
class JSONCharacterReader(BaseCharacterReader):
|
class JSONCharacterReader(BaseCharacterReader):
|
||||||
"""Callable to parse a JSON character file from Roll20 VVTES.
|
"""Callable to parse a JSON character file from Roll20 VVTES.
|
||||||
|
|
||||||
The definition file should be a JSON file following one of the
|
The definition file should be a JSON file following one of the
|
||||||
supported formats, filled with variables describing the character.
|
supported formats, filled with variables describing the character.
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
filename
|
|
||||||
The path to the file that will be imported.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
@@ -78,6 +95,12 @@ class JSONCharacterReader(BaseCharacterReader):
|
|||||||
val = 0
|
val = 0
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
class Roll20CharacterReader(JSONCharacterReader):
|
||||||
|
@property
|
||||||
|
def schema_version(self):
|
||||||
|
return self.json_data()["schema_version"]
|
||||||
|
|
||||||
def get_attrib(self, key, which="current", default=None):
|
def get_attrib(self, key, which="current", default=None):
|
||||||
for obj in self.json_data()["attribs"]:
|
for obj in self.json_data()["attribs"]:
|
||||||
if obj["name"] == key:
|
if obj["name"] == key:
|
||||||
@@ -93,35 +116,6 @@ class JSONCharacterReader(BaseCharacterReader):
|
|||||||
false_profs = ["", "0"]
|
false_profs = ["", "0"]
|
||||||
return self.get_attrib(key=f"{key}_prof") not in false_profs
|
return self.get_attrib(key=f"{key}_prof") not in false_profs
|
||||||
|
|
||||||
def spells(self, prepared: bool = False):
|
|
||||||
"""Iterator over the spells the character knows.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
==========
|
|
||||||
prepared
|
|
||||||
If true, only return prepared spells.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# "name": "repeating_spell-cantrip_-MEzYWPA5cUZYd4ZOvMS_spellname",
|
|
||||||
prof_re = re.compile(
|
|
||||||
"repeating_spell-(cantrip|[0-9]+)_([-0-9a-zA-Z]+)_spellname"
|
|
||||||
)
|
|
||||||
for obj in self.json_data()["attribs"]:
|
|
||||||
match = prof_re.match(obj["name"])
|
|
||||||
if match:
|
|
||||||
level = match.group(1)
|
|
||||||
spell_id = match.group(2)
|
|
||||||
spell_name = self.get_attrib(
|
|
||||||
f"repeating_spell-{level}_{spell_id}_spellname"
|
|
||||||
)
|
|
||||||
is_prepared = self.as_int(
|
|
||||||
self.get_attrib(
|
|
||||||
f"repeating_spell-{level}_{spell_id}_spellprepared", default=0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not prepared or is_prepared:
|
|
||||||
yield spell_name.lower()
|
|
||||||
|
|
||||||
def proficiencies(self, kind=None):
|
def proficiencies(self, kind=None):
|
||||||
"""Iterator over the skills in which the character is proficient.
|
"""Iterator over the skills in which the character is proficient.
|
||||||
|
|
||||||
@@ -174,14 +168,41 @@ class JSONCharacterReader(BaseCharacterReader):
|
|||||||
weapon_name = weapon_name.split(",")[0].strip()
|
weapon_name = weapon_name.split(",")[0].strip()
|
||||||
yield weapon_name
|
yield weapon_name
|
||||||
|
|
||||||
def __call__(self, filename: str):
|
def spells(self, prepared: bool = False):
|
||||||
|
"""Iterator over the spells the character knows.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
==========
|
||||||
|
prepared
|
||||||
|
If true, only return prepared spells.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# "name": "repeating_spell-cantrip_-MEzYWPA5cUZYd4ZOvMS_spellname",
|
||||||
|
prof_re = re.compile(
|
||||||
|
"repeating_spell-(cantrip|[0-9]+)_([-0-9a-zA-Z]+)_spellname"
|
||||||
|
)
|
||||||
|
for obj in self.json_data()["attribs"]:
|
||||||
|
match = prof_re.match(obj["name"])
|
||||||
|
if match:
|
||||||
|
level = match.group(1)
|
||||||
|
spell_id = match.group(2)
|
||||||
|
spell_name = self.get_attrib(
|
||||||
|
f"repeating_spell-{level}_{spell_id}_spellname"
|
||||||
|
)
|
||||||
|
is_prepared = self.as_int(
|
||||||
|
self.get_attrib(
|
||||||
|
f"repeating_spell-{level}_{spell_id}_spellprepared", default=0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not prepared or is_prepared:
|
||||||
|
yield spell_name.lower()
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
"""Create a character property dictionary from the JSON file."""
|
"""Create a character property dictionary from the JSON file."""
|
||||||
# Verify the version compatibility
|
# Verify the version compatibility
|
||||||
self.filename = filename
|
if self.schema_version != 2:
|
||||||
version = self.json_data()["schema_version"]
|
|
||||||
if version != 2:
|
|
||||||
raise exceptions.JSONFormatError(
|
raise exceptions.JSONFormatError(
|
||||||
"Cannot parse JSON schema version: %s" % version
|
"Cannot parse JSON schema version: %s" % self.schema_version
|
||||||
)
|
)
|
||||||
# Parse the json tree to get character properties
|
# Parse the json tree to get character properties
|
||||||
char_props = {}
|
char_props = {}
|
||||||
@@ -274,8 +295,247 @@ class JSONCharacterReader(BaseCharacterReader):
|
|||||||
return char_props
|
return char_props
|
||||||
|
|
||||||
|
|
||||||
|
class FoundryCharacterReader(JSONCharacterReader):
|
||||||
|
def _skill_proficiency_value(self, key: str) -> float:
|
||||||
|
proficiency_labels = {
|
||||||
|
"acrobatics": "acr",
|
||||||
|
"animal_handling": "ani",
|
||||||
|
"arcana": "arc",
|
||||||
|
"athletics": "ath",
|
||||||
|
"deception": "dec",
|
||||||
|
"history": "his",
|
||||||
|
"insight": "ins",
|
||||||
|
"intimidation": "itm",
|
||||||
|
"investigation": "inv",
|
||||||
|
"medicine": "med",
|
||||||
|
"nature": "nat",
|
||||||
|
"perception": "prc",
|
||||||
|
"performance": "prf",
|
||||||
|
"persuasion": "per",
|
||||||
|
"religion": "rel",
|
||||||
|
"sleight_of_hand": "slt",
|
||||||
|
"stealth": "ste",
|
||||||
|
"survival": "sur",
|
||||||
|
}
|
||||||
|
proficiency_value = float(
|
||||||
|
self.json_data()["data"]["skills"][proficiency_labels[key]]["value"]
|
||||||
|
)
|
||||||
|
return proficiency_value
|
||||||
|
|
||||||
|
def skill_proficiency(self, key: str) -> bool:
|
||||||
|
return self._skill_proficiency_value(key) >= 1.0
|
||||||
|
|
||||||
|
def skill_expertise(self, key: str) -> bool:
|
||||||
|
return self._skill_proficiency_value(key) > 1.0
|
||||||
|
|
||||||
|
def proficiencies(self, kind=None):
|
||||||
|
"""Iterator over the skills in which the character is proficient.
|
||||||
|
|
||||||
|
*kind* can be one of "weapon", "language", or None (all
|
||||||
|
proficiencies).
|
||||||
|
|
||||||
|
"""
|
||||||
|
yield_weapons = kind is None or kind == "weapon"
|
||||||
|
yield_languages = kind is None or kind == "language"
|
||||||
|
# Weapon proficiencies
|
||||||
|
if yield_weapons:
|
||||||
|
weapon_prof = self.json_data()["data"]["traits"]["weaponProf"]
|
||||||
|
# Simple and martial weapons
|
||||||
|
if "sim" in weapon_prof["value"]:
|
||||||
|
yield "simple weapons"
|
||||||
|
if "mar" in weapon_prof["value"]:
|
||||||
|
yield "martial weapons"
|
||||||
|
# Extra weapons
|
||||||
|
for weapon in weapon_prof["custom"].split(";"):
|
||||||
|
yield weapon.strip()
|
||||||
|
if yield_languages:
|
||||||
|
# Languages
|
||||||
|
lang_json = self.json_data()["data"]["traits"]["languages"]
|
||||||
|
languages = lang_json["value"]
|
||||||
|
languages.extend([s.strip() for s in lang_json["custom"].split(";")])
|
||||||
|
yield from languages
|
||||||
|
|
||||||
|
def weapons(self):
|
||||||
|
"""Iterator over the weapons the character is carrying in her inventory."""
|
||||||
|
items = self.json_data()["items"]
|
||||||
|
for item in items:
|
||||||
|
if item["type"] == "weapon" and item["name"] != "<no name>":
|
||||||
|
yield item["name"].lower()
|
||||||
|
|
||||||
|
def armor(self):
|
||||||
|
items = self.json_data()["items"]
|
||||||
|
armor_types = ["light", "medium", "heavy"]
|
||||||
|
for item in items:
|
||||||
|
if (
|
||||||
|
item["type"] == "equipment"
|
||||||
|
and item["data"]["armor"]["type"] in armor_types
|
||||||
|
):
|
||||||
|
return item["name"].lower()
|
||||||
|
|
||||||
|
def shield(self):
|
||||||
|
items = self.json_data()["items"]
|
||||||
|
for item in items:
|
||||||
|
if (
|
||||||
|
item["type"] == "equipment"
|
||||||
|
and item["data"]["armor"]["type"] == "shield"
|
||||||
|
):
|
||||||
|
return item["name"].lower()
|
||||||
|
|
||||||
|
def equipment(self):
|
||||||
|
"""Iterator over the non-weapons the character is carrying in her
|
||||||
|
inventory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
items = self.json_data()["items"]
|
||||||
|
item_types = ["consumable", "equipment", "tool", "backpack", "loot"]
|
||||||
|
for item in items:
|
||||||
|
if item["type"] in item_types:
|
||||||
|
item_name = item["name"]
|
||||||
|
quantity = self.as_int(item["data"]["quantity"])
|
||||||
|
if quantity != 1:
|
||||||
|
item_name += f"({quantity})"
|
||||||
|
yield item_name.lower()
|
||||||
|
|
||||||
|
def class_levels(self):
|
||||||
|
for item in self.json_data()["items"]:
|
||||||
|
if item["type"] == "class":
|
||||||
|
yield (item["name"], item["data"]["levels"])
|
||||||
|
|
||||||
|
def spells(self, prepared: bool = False):
|
||||||
|
"""Iterator over the spells the character knows.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
==========
|
||||||
|
prepared
|
||||||
|
If true, only return prepared spells.
|
||||||
|
|
||||||
|
"""
|
||||||
|
spells = (item for item in self.json_data()["items"] if item["type"] == "spell")
|
||||||
|
if prepared:
|
||||||
|
spells = (d for d in spells if d["data"]["preparation"]["prepared"])
|
||||||
|
spell_names = (d["name"] for d in spells)
|
||||||
|
yield from spell_names
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
"""Create a character property dictionary from the JSON file."""
|
||||||
|
# Parse the json tree to get character properties
|
||||||
|
char_props = {}
|
||||||
|
json_data = self.json_data()
|
||||||
|
details = json_data["data"]["details"]
|
||||||
|
char_props["name"] = json_data["name"]
|
||||||
|
char_props["background"] = details["background"]
|
||||||
|
char_props["race"] = details["race"]
|
||||||
|
char_props["alignment"] = details["alignment"]
|
||||||
|
char_props["xp"] = self.as_int(details["xp"]["value"])
|
||||||
|
classes, levels = zip(*self.class_levels())
|
||||||
|
char_props["levels"] = list(levels)
|
||||||
|
char_props["classes"] = list(classes)
|
||||||
|
# Attributes
|
||||||
|
attribute_names = {
|
||||||
|
"str": "strength",
|
||||||
|
"dex": "dexterity",
|
||||||
|
"con": "constitution",
|
||||||
|
"int": "intelligence",
|
||||||
|
"wis": "wisdom",
|
||||||
|
"cha": "charisma",
|
||||||
|
}
|
||||||
|
abilities = self.json_data()["data"]["abilities"]
|
||||||
|
save_proficiences = []
|
||||||
|
for abbr, attr in attribute_names.items():
|
||||||
|
char_props[attr] = self.as_int(abilities[abbr]["value"])
|
||||||
|
# Check proficiency
|
||||||
|
is_proficient = bool(abilities[abbr]["proficient"])
|
||||||
|
if is_proficient:
|
||||||
|
save_proficiences.append(attr)
|
||||||
|
char_props["saving_throw_proficiencies"] = save_proficiences
|
||||||
|
# Skill proficiencies
|
||||||
|
skill_names = [
|
||||||
|
"acrobatics",
|
||||||
|
"animal_handling",
|
||||||
|
"arcana",
|
||||||
|
"athletics",
|
||||||
|
"deception",
|
||||||
|
"history",
|
||||||
|
"insight",
|
||||||
|
"intimidation",
|
||||||
|
"investigation",
|
||||||
|
"medicine",
|
||||||
|
"nature",
|
||||||
|
"perception",
|
||||||
|
"performance",
|
||||||
|
"persuasion",
|
||||||
|
"religion",
|
||||||
|
"sleight_of_hand",
|
||||||
|
"stealth",
|
||||||
|
"survival",
|
||||||
|
]
|
||||||
|
skill_profs = [skill for skill in skill_names if self.skill_proficiency(skill)]
|
||||||
|
char_props["skill_proficiencies"] = skill_profs
|
||||||
|
skill_expertise = [
|
||||||
|
skill for skill in skill_names if self.skill_expertise(skill)
|
||||||
|
]
|
||||||
|
char_props["skill_expertise"] = skill_expertise
|
||||||
|
# Other proficiencies
|
||||||
|
char_props["weapon_proficiencies"] = self.proficiencies("weapon")
|
||||||
|
char_props["languages"] = ", ".join(self.proficiencies("language"))
|
||||||
|
# Tool proficiencies
|
||||||
|
tool_labels = {
|
||||||
|
"art": "artisan's tools",
|
||||||
|
"disg": "disguise kit",
|
||||||
|
"forg": "forger's kit",
|
||||||
|
"game": "gaming set",
|
||||||
|
"herb": "herbalism kit",
|
||||||
|
"music": "musical instrument",
|
||||||
|
"navg": "navigator's tools",
|
||||||
|
"pois": "poisoner's kit",
|
||||||
|
"thief": "thieves' tools",
|
||||||
|
"vehicle": "vehicle (land or water)",
|
||||||
|
}
|
||||||
|
tool_profs = json_data["data"]["traits"]["toolProf"]["value"]
|
||||||
|
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
|
||||||
|
# Combat stats
|
||||||
|
char_props["hp_max"] = self.as_int(
|
||||||
|
json_data["data"]["attributes"]["hp"]["value"]
|
||||||
|
)
|
||||||
|
# Equipment
|
||||||
|
currency = json_data["data"]["currency"]
|
||||||
|
char_props["cp"] = currency["cp"]
|
||||||
|
char_props["sp"] = currency["sp"]
|
||||||
|
char_props["ep"] = currency["ep"]
|
||||||
|
char_props["gp"] = currency["gp"]
|
||||||
|
char_props["pp"] = currency["pp"]
|
||||||
|
char_props["weapons"] = list(self.weapons())
|
||||||
|
char_props["equipment"] = ", ".join(self.equipment())
|
||||||
|
char_props["armor"] = self.armor()
|
||||||
|
char_props["shield"] = self.shield()
|
||||||
|
# Personality, etc
|
||||||
|
char_props["personality_traits"] = details["trait"].strip()
|
||||||
|
char_props["flaws"] = details["flaw"].strip()
|
||||||
|
char_props["ideals"] = details["ideal"].strip()
|
||||||
|
char_props["bonds"] = details["bond"].strip()
|
||||||
|
# Spells
|
||||||
|
char_props["spells"] = self.spells()
|
||||||
|
char_props["spells_prepared"] = self.spells(prepared=True)
|
||||||
|
# Some unused values
|
||||||
|
warn_msg = (
|
||||||
|
"Importing the following traits from JSON is not yet supported: "
|
||||||
|
"magic_items, attacks_and_spellcasting, "
|
||||||
|
"infusions, wild_shapes."
|
||||||
|
)
|
||||||
|
warnings.warn(warn_msg)
|
||||||
|
log.warning(warn_msg)
|
||||||
|
char_props["magic_items"] = ()
|
||||||
|
char_props["attacks_and_spellcasting"] = ""
|
||||||
|
char_props["infusions"] = []
|
||||||
|
char_props["wild_shapes"] = []
|
||||||
|
return char_props
|
||||||
|
|
||||||
|
|
||||||
class PythonCharacterReader(BaseCharacterReader):
|
class PythonCharacterReader(BaseCharacterReader):
|
||||||
def __call__(self, filename: str):
|
def __call__(self):
|
||||||
"""Create a character object from the given definition file.
|
"""Create a character object from the given definition file.
|
||||||
|
|
||||||
The definition file should be an importable python file, filled
|
The definition file should be an importable python file, filled
|
||||||
@@ -287,6 +547,7 @@ class PythonCharacterReader(BaseCharacterReader):
|
|||||||
The path to the file that will be imported.
|
The path to the file that will be imported.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
filename = self.filename
|
||||||
# Check if this file contains the version string
|
# Check if this file contains the version string
|
||||||
version_re = re.compile(r'dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
|
version_re = re.compile(r'dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
|
||||||
with open(filename, mode="r") as f:
|
with open(filename, mode="r") as f:
|
||||||
@@ -315,5 +576,5 @@ class PythonCharacterReader(BaseCharacterReader):
|
|||||||
|
|
||||||
readers_by_extension = {
|
readers_by_extension = {
|
||||||
".py": PythonCharacterReader,
|
".py": PythonCharacterReader,
|
||||||
".json": JSONCharacterReader,
|
".json": json_character_reader,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+124
-3
@@ -7,7 +7,8 @@ from dungeonsheets.readers import read_character_file
|
|||||||
|
|
||||||
EG_DIR = (Path(__file__).parent.parent / "examples").resolve()
|
EG_DIR = (Path(__file__).parent.parent / "examples").resolve()
|
||||||
CHAR_PYTHON_FILE = EG_DIR / "rogue1.py"
|
CHAR_PYTHON_FILE = EG_DIR / "rogue1.py"
|
||||||
CHAR_JSON_FILE = EG_DIR / "barbarian3.json"
|
ROLL20_JSON_FILE = EG_DIR / "barbarian3.json"
|
||||||
|
FOUNDRY_JSON_FILE = EG_DIR / "bard3_foundry.json"
|
||||||
SPELLCASTER_JSON_FILE = EG_DIR / "artificer2.json"
|
SPELLCASTER_JSON_FILE = EG_DIR / "artificer2.json"
|
||||||
|
|
||||||
|
|
||||||
@@ -18,9 +19,9 @@ class PythonReaderTests(unittest.TestCase):
|
|||||||
self.assertEqual(result["strength"], 10)
|
self.assertEqual(result["strength"], 10)
|
||||||
|
|
||||||
|
|
||||||
class JSONReaderTests(unittest.TestCase):
|
class Roll20ReaderTests(unittest.TestCase):
|
||||||
def test_load_json_file(self):
|
def test_load_json_file(self):
|
||||||
charfile = CHAR_JSON_FILE
|
charfile = ROLL20_JSON_FILE
|
||||||
with warnings.catch_warnings(record=True):
|
with warnings.catch_warnings(record=True):
|
||||||
result = read_character_file(charfile)
|
result = read_character_file(charfile)
|
||||||
expected_data = dict(
|
expected_data = dict(
|
||||||
@@ -120,3 +121,123 @@ class JSONReaderTests(unittest.TestCase):
|
|||||||
if isinstance(this_result, types.GeneratorType):
|
if isinstance(this_result, types.GeneratorType):
|
||||||
this_result = list(this_result)
|
this_result = list(this_result)
|
||||||
self.assertEqual(this_result, val, key)
|
self.assertEqual(this_result, val, key)
|
||||||
|
|
||||||
|
|
||||||
|
class FoundryReaderTests(unittest.TestCase):
|
||||||
|
def test_load_json_file(self):
|
||||||
|
charfile = FOUNDRY_JSON_FILE
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
result = read_character_file(charfile)
|
||||||
|
expected_data = dict(
|
||||||
|
name="Sam Lloyd",
|
||||||
|
classes=["Bard"],
|
||||||
|
levels=[6],
|
||||||
|
background="Attorney",
|
||||||
|
alignment="Lawful Neutral",
|
||||||
|
race="Variant",
|
||||||
|
xp=0,
|
||||||
|
strength=6,
|
||||||
|
dexterity=12,
|
||||||
|
constitution=14,
|
||||||
|
intelligence=10,
|
||||||
|
wisdom=12,
|
||||||
|
charisma=18,
|
||||||
|
hp_max=47,
|
||||||
|
skill_proficiencies=[
|
||||||
|
"deception",
|
||||||
|
"insight",
|
||||||
|
"investigation",
|
||||||
|
"perception",
|
||||||
|
"persuasion",
|
||||||
|
"sleight_of_hand",
|
||||||
|
],
|
||||||
|
skill_expertise=[
|
||||||
|
"insight",
|
||||||
|
"persuasion",
|
||||||
|
],
|
||||||
|
weapon_proficiencies=[
|
||||||
|
"simple weapons",
|
||||||
|
"martial weapons",
|
||||||
|
"crossbow",
|
||||||
|
"knives",
|
||||||
|
],
|
||||||
|
_proficiencies_text=[
|
||||||
|
"artisan's tools",
|
||||||
|
"disguise kit",
|
||||||
|
"forger's kit",
|
||||||
|
"gaming set",
|
||||||
|
"herbalism kit",
|
||||||
|
"musical instrument",
|
||||||
|
"navigator's tools",
|
||||||
|
"poisoner's kit",
|
||||||
|
"thieves' tools",
|
||||||
|
"vehicle (land or water)",
|
||||||
|
"chopsticks",
|
||||||
|
"juggling balls"
|
||||||
|
],
|
||||||
|
saving_throw_proficiencies=["dexterity", "charisma"],
|
||||||
|
languages="common, elvish, law jargon, spanish",
|
||||||
|
cp=0,
|
||||||
|
sp=0,
|
||||||
|
ep=0,
|
||||||
|
gp=162,
|
||||||
|
pp=2,
|
||||||
|
weapons=["rapier"],
|
||||||
|
magic_items=(),
|
||||||
|
armor="padded armor",
|
||||||
|
shield="shield",
|
||||||
|
personality_traits="Loves a good lawyer joke.",
|
||||||
|
ideals="Every form in triplicate.",
|
||||||
|
bonds="Just show up to your court date and it won't be a problem.",
|
||||||
|
flaws="Too many to list.",
|
||||||
|
equipment=(
|
||||||
|
"rations(7), ring of acid resistance, cartographer's tools, bag of holding, diamonds(20), padded armor, shield"
|
||||||
|
),
|
||||||
|
attacks_and_spellcasting="",
|
||||||
|
spells_prepared=["Bane", "Faerie Fire", "Thunderwave", "Detect Thoughts"],
|
||||||
|
spells=["Vicious Mockery", "Message", "Prestidigitation", "Bane", "Faerie Fire", "Thunderwave", "Healing Word", "Blindness/Deafness", "Detect Thoughts", "Hold Person", "Fear", "Heat Metal"],
|
||||||
|
)
|
||||||
|
for key, val in expected_data.items():
|
||||||
|
this_result = result[key]
|
||||||
|
# Force evaluation of generators
|
||||||
|
if isinstance(this_result, types.GeneratorType):
|
||||||
|
this_result = list(this_result)
|
||||||
|
self.assertEqual(this_result, val, key)
|
||||||
|
|
||||||
|
def test_load_json_spells(self):
|
||||||
|
charfile = SPELLCASTER_JSON_FILE
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
result = read_character_file(charfile)
|
||||||
|
expected_data = dict(
|
||||||
|
spells_prepared=[
|
||||||
|
"cure wounds",
|
||||||
|
],
|
||||||
|
spells=[
|
||||||
|
"spare the dying",
|
||||||
|
"fire bolt",
|
||||||
|
"absorb elements",
|
||||||
|
"alarm",
|
||||||
|
"catapult",
|
||||||
|
"cure wounds",
|
||||||
|
"detect magic",
|
||||||
|
"disguise self",
|
||||||
|
"expeditious retreat",
|
||||||
|
"faerie fire",
|
||||||
|
"false life",
|
||||||
|
"feather fall",
|
||||||
|
"grease",
|
||||||
|
"identify",
|
||||||
|
"jump",
|
||||||
|
"longstrider",
|
||||||
|
"purify food and drink",
|
||||||
|
"sanctuary",
|
||||||
|
"snare",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for key, val in expected_data.items():
|
||||||
|
this_result = result[key]
|
||||||
|
# Force evaluation of generators
|
||||||
|
if isinstance(this_result, types.GeneratorType):
|
||||||
|
this_result = list(this_result)
|
||||||
|
self.assertEqual(this_result, val, key)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user