Added ability to parse JSON files exported from Foundry.

This commit is contained in:
Mark Wolfman
2021-05-21 09:47:40 -05:00
parent 9b1a33bfa2
commit 90e362055a
9 changed files with 3599 additions and 171 deletions
+1 -1
View File
@@ -1 +1 @@
0.13.0
0.14.0
+10 -6
View File
@@ -326,16 +326,20 @@ recommended subclass method above, so that attributes and descriptions
can be given.
VTTES JSON Files
================
Roll20 (VTTES) and Foundry JSON Files
=====================================
Dungeonsheets has partial support for reading JSON files exporting
using the `VTTES browser extension`_. This allows character sheets to
be exported from systems like Roll20.net, and then rendered into full
character sheets.
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/
+46 -9
View File
@@ -4,6 +4,7 @@ import re
import warnings
import math
from types import ModuleType
from typing import Sequence, Union
import jinja2
@@ -223,13 +224,38 @@ class Character:
custom_features = list()
feature_choices = list()
def __init__(self, **attrs):
"""Takes a bunch of attrs and passes them to ``set_attrs``"""
def __init__(
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()
# make sure class, race, background are set first
my_classes = attrs.pop("classes", [])
my_levels = attrs.pop("levels", [])
my_subclasses = attrs.pop("subclasses", [])
my_classes = classes
my_levels = levels
my_subclasses = subclasses
# backwards compatability
if len(my_classes) == 0:
if "class" in attrs:
@@ -281,8 +307,9 @@ class Character:
cls: (classes.CharClass, type, str),
level: (int, str),
subclass=None,
feature_choices=[],
feature_choices: Sequence = [],
):
"""Add a class, level, and subclass the character has attained."""
if isinstance(cls, str):
cls = cls.strip().title().replace(" ", "")
try:
@@ -298,8 +325,18 @@ class Character:
)
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):
classes_list = [classes_list]
if (
@@ -909,7 +946,7 @@ class Character:
return ()
@classmethod
def load(cls, character_file):
def load(Cls, character_file):
# Create a character from the character definition
char_props = read_character_file(character_file)
classes = char_props.get("classes", [])
@@ -920,7 +957,7 @@ class Character:
]
char_props["levels"] = [str(char_props.pop("level"))]
# Create the character with loaded properties
char = Character(**char_props)
char = Cls(**char_props)
return char
def save(self, filename, template_file="character_template.txt"):
+3 -1
View File
@@ -27,6 +27,7 @@ def text_box(string):
)
return new_string
def create_character_pdf_template(character, basename, flatten=False):
# Prepare the list of 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")
make_pdf(fields, src_pdf=src_pdf, basename=basename, flatten=flatten)
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.
@@ -556,4 +558,4 @@ def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False):
popenargs.append("flatten")
subprocess.call(popenargs)
# Clean up temporary files
os.remove(fdfname)
os.remove(fdfname)
+1 -3
View File
@@ -46,9 +46,7 @@ def _remove_temp_files(basename_):
filename.unlink()
def create_latex_pdf(
tex, basename, keep_temp_files=False, use_dnd_decorations=False
):
def create_latex_pdf(tex, basename, keep_temp_files=False, use_dnd_decorations=False):
# Create tex document
tex_file = f"{basename}.tex"
with open(tex_file, mode="w", encoding="utf-8") as f:
+57 -104
View File
@@ -21,105 +21,25 @@ from dungeonsheets.fill_pdf_template import (
)
from dungeonsheets.character import Character
"""Program to take character definitions and build a PDF of the
character sheet."""
PDFTK_CMD = "pdftk"
log = logging.getLogger(__name__)
ORDINALS = {
1: "1st",
2: "2nd",
3: "3rd",
4: "4th",
5: "5th",
6: "6th",
7: "7th",
2: "2nd",
3: "3rd",
4: "4th",
5: "5th",
6: "6th",
7: "7th",
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(
loader=PackageLoader("dungeonsheets", "forms"),
block_start_string="[%",
@@ -141,6 +61,7 @@ def create_subclasses_tex(
template = jinja_env.get_template("subclasses_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_features_tex(
character: Character,
use_dnd_decorations: bool = False,
@@ -148,6 +69,7 @@ def create_features_tex(
template = jinja_env.get_template("features_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_magic_items_tex(
character: Character,
use_dnd_decorations: bool = False,
@@ -155,12 +77,16 @@ def create_magic_items_tex(
template = jinja_env.get_template("magic_items_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_spellbook_tex(
character: Character,
use_dnd_decorations: bool = False,
) -> str:
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(
character: Character,
@@ -169,6 +95,7 @@ def create_infusions_tex(
template = jinja_env.get_template("infusions_template.tex")
return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_druid_shapes_tex(
character: Character,
use_dnd_decorations: bool = False,
@@ -211,7 +138,11 @@ def make_sheet(
char_base = os.path.splitext(character_file)[0] + "_char"
sheets = [char_base + ".pdf"]
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
char_pdf = create_character_pdf_template(
@@ -230,33 +161,54 @@ def make_sheet(
features_base = "{:s}_features".format(os.path.splitext(character_file)[0])
# Create a list of subcasses
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
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:
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
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
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
if getattr(character, "wild_shapes", []):
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(
create_druid_shapes_tex(character, use_dnd_decorations=fancy_decorations)
)
tex.append(
jinja_env.get_template("postamble.tex").render(
use_dnd_decorations=fancy_decorations
)
)
# Typeset combined LaTeX file
try:
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")
final_pdf = os.path.splitext(character_file)[0] + ".pdf"
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)
load_character_file = readers.read_character_file
# # Deprecated:
# load_character_file = readers.read_character_file
def _build(filename, args) -> int:
+305 -44
View File
@@ -7,7 +7,6 @@ import logging
from pathlib import Path
from dungeonsheets import exceptions
log = logging.getLogger(__file__)
@@ -30,17 +29,28 @@ def read_character_file(filename: str):
# Parse the file name
ext = filename.suffix
try:
reader = readers_by_extension[ext]()
reader = readers_by_extension[ext](filename=filename)
except KeyError:
raise ValueError(f"Character definition {filename} is not a known file type.")
else:
new_char = reader(filename=filename)
new_char = reader()
return new_char
class BaseCharacterReader:
"""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):
"""
Parameters
@@ -51,17 +61,24 @@ class BaseCharacterReader:
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):
"""Callable to parse a JSON character file from Roll20 VVTES.
The definition file should be a JSON file following one of the
supported formats, filled with variables describing the character.
Parameters
----------
filename
The path to the file that will be imported.
"""
@lru_cache()
@@ -78,6 +95,12 @@ class JSONCharacterReader(BaseCharacterReader):
val = 0
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):
for obj in self.json_data()["attribs"]:
if obj["name"] == key:
@@ -93,35 +116,6 @@ class JSONCharacterReader(BaseCharacterReader):
false_profs = ["", "0"]
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):
"""Iterator over the skills in which the character is proficient.
@@ -174,14 +168,41 @@ class JSONCharacterReader(BaseCharacterReader):
weapon_name = weapon_name.split(",")[0].strip()
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."""
# Verify the version compatibility
self.filename = filename
version = self.json_data()["schema_version"]
if version != 2:
if self.schema_version != 2:
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
char_props = {}
@@ -274,8 +295,247 @@ class JSONCharacterReader(BaseCharacterReader):
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):
def __call__(self, filename: str):
def __call__(self):
"""Create a character object from the given definition file.
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.
"""
filename = self.filename
# Check if this file contains the version string
version_re = re.compile(r'dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
with open(filename, mode="r") as f:
@@ -315,5 +576,5 @@ class PythonCharacterReader(BaseCharacterReader):
readers_by_extension = {
".py": PythonCharacterReader,
".json": JSONCharacterReader,
".json": json_character_reader,
}
File diff suppressed because one or more lines are too long
+124 -3
View File
@@ -7,7 +7,8 @@ from dungeonsheets.readers import read_character_file
EG_DIR = (Path(__file__).parent.parent / "examples").resolve()
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"
@@ -18,9 +19,9 @@ class PythonReaderTests(unittest.TestCase):
self.assertEqual(result["strength"], 10)
class JSONReaderTests(unittest.TestCase):
class Roll20ReaderTests(unittest.TestCase):
def test_load_json_file(self):
charfile = CHAR_JSON_FILE
charfile = ROLL20_JSON_FILE
with warnings.catch_warnings(record=True):
result = read_character_file(charfile)
expected_data = dict(
@@ -120,3 +121,123 @@ class JSONReaderTests(unittest.TestCase):
if isinstance(this_result, types.GeneratorType):
this_result = list(this_result)
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)