mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Added ability to import character files in JSON format from VTTES.
This commit is contained in:
+2
-2
@@ -59,8 +59,8 @@ not, then this feature will be skipped.
|
||||
Usage
|
||||
=====
|
||||
|
||||
Each character is described by a python file, which gives many
|
||||
attributes associated with the character. See examples_ for more
|
||||
Each character is described by a python (or JSON) file, which gives
|
||||
many attributes associated with the character. See examples_ for more
|
||||
information about the character descriptions.
|
||||
|
||||
.. _examples: https://github.com/canismarko/dungeon-sheets/tree/master/examples
|
||||
|
||||
+10
-45
@@ -1,10 +1,10 @@
|
||||
"""Tools for describing a player character."""
|
||||
__all__ = ('Character',)
|
||||
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import warnings
|
||||
import math
|
||||
|
||||
@@ -18,6 +18,7 @@ from dungeonsheets.dice import read_dice_str
|
||||
from dungeonsheets.stats import (Ability, ArmorClass, Initiative, Skill, Speed,
|
||||
findattr)
|
||||
from dungeonsheets.weapons import Weapon
|
||||
from dungeonsheets.readers import read_character_file
|
||||
|
||||
|
||||
def read(fname):
|
||||
@@ -323,7 +324,7 @@ class Character():
|
||||
Set maximum HP based on value in charlist py or calc from classes
|
||||
"""
|
||||
if hp_max:
|
||||
assert isinstance(hp_max, int)
|
||||
assert isinstance(hp_max, int), hp_max.__class__
|
||||
self.hp_max = hp_max
|
||||
else:
|
||||
const_mod = self.constitution.modifier
|
||||
@@ -696,7 +697,13 @@ class Character():
|
||||
try:
|
||||
NewWeapon = findattr(weapons, weapon)
|
||||
except AttributeError:
|
||||
raise AttributeError(f'Weapon "{weapon}" is not defined')
|
||||
try:
|
||||
findattr(spells, weapon)
|
||||
except AttributeError:
|
||||
raise AttributeError(f'Weapon "{weapon}" is not defined')
|
||||
else:
|
||||
warnings.warn(f"Ignoring spell {weapon} listed as weapon.")
|
||||
return
|
||||
weapon_ = NewWeapon(wielder=self)
|
||||
elif issubclass(weapon, weapons.Weapon):
|
||||
weapon_ = weapon(wielder=self)
|
||||
@@ -802,48 +809,6 @@ class Character():
|
||||
flatten=kwargs.get('flatten', True))
|
||||
|
||||
|
||||
def read_character_file(filename):
|
||||
"""Create a character object from the given definition file.
|
||||
|
||||
The definition file should be an importable python file, filled
|
||||
with variables describing the character.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
The path to the file that will be imported.
|
||||
|
||||
"""
|
||||
# Parse the file name
|
||||
dir_, fname = os.path.split(os.path.abspath(filename))
|
||||
module_name, ext = os.path.splitext(fname)
|
||||
if ext != '.py':
|
||||
raise ValueError(f"Character definition {filename} is not a python file.")
|
||||
# Check if this file contains the version string
|
||||
version_re = re.compile('dungeonsheets_version\s*=\s*[\'"]([0-9.]+)[\'"]')
|
||||
with open(filename, mode='r') as f:
|
||||
version = None
|
||||
for line in f:
|
||||
match = version_re.match(line)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
break
|
||||
if version is None:
|
||||
# Not a valid DND character file
|
||||
raise exceptions.CharacterFileFormatError(
|
||||
f"No ``dungeonsheets_version = `` entry in `{filename}`.")
|
||||
# Import the module to extract the information
|
||||
spec = importlib.util.spec_from_file_location('module', filename)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# Prepare a list of properties for this character
|
||||
char_props = {}
|
||||
for prop_name in dir(module):
|
||||
if prop_name[0:2] != '__':
|
||||
char_props[prop_name] = getattr(module, prop_name)
|
||||
return char_props
|
||||
|
||||
|
||||
# Add backwards compatability for tests
|
||||
class Artificer(Character):
|
||||
def __init__(self, level=1, **attrs):
|
||||
|
||||
@@ -12,3 +12,9 @@ class LatexNotFoundError(LatexError):
|
||||
|
||||
class MonsterError(AttributeError):
|
||||
"""Error retriving or using a D&D Monster."""
|
||||
|
||||
class JSONFormatError(RuntimeError):
|
||||
"""The JSON file doesn't conform to the understood formats."""
|
||||
|
||||
class UnknownFileType(RuntimeError):
|
||||
"""The input file does not match one of the known formats."""
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import subprocess
|
||||
import warnings
|
||||
import re
|
||||
from pathlib import Path
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from itertools import product
|
||||
|
||||
@@ -15,7 +16,7 @@ import pdfrw
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
from dungeonsheets import character as _char
|
||||
from dungeonsheets import exceptions, classes
|
||||
from dungeonsheets import exceptions, classes, readers
|
||||
from dungeonsheets.stats import mod_str
|
||||
|
||||
|
||||
@@ -37,14 +38,14 @@ dice_re = re.compile(r'`*(\d+d\d+(?:\s*\+\s*\d+)?)`*')
|
||||
# - 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('^[ \t\r\f\v]*\n' # A blank line
|
||||
'((?:\s*[-*+]\s+[^\n]*\n)+)' # The first line of each list item
|
||||
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('^\s*[-*+]\s+', flags=re.MULTILINE)
|
||||
list_item_re = re.compile(r'^\s*[-*+]\s+', flags=re.MULTILINE)
|
||||
|
||||
|
||||
def _parse_rst_lists(rst):
|
||||
@@ -143,7 +144,7 @@ def rst_to_latex(rst, top_heading_level=0):
|
||||
list_tex = "\n\\begin{itemize}\n"
|
||||
for item in list_items:
|
||||
list_tex += f"\\item{{{item}}}\n"
|
||||
list_tex += "\end{itemize}\n"
|
||||
list_tex += "\\end{itemize}\n"
|
||||
tex = tex.replace(list_rst, list_tex)
|
||||
# Inline text formatting
|
||||
tex = bold_re.sub(r'\\textbf{\1}', tex)
|
||||
@@ -676,11 +677,22 @@ def merge_pdfs(src_filenames, dest_filename, clean_up=False):
|
||||
os.remove(sheet)
|
||||
|
||||
|
||||
load_character_file = _char.read_character_file
|
||||
load_character_file = readers.read_character_file
|
||||
|
||||
|
||||
def _build(filename, args):
|
||||
basename = os.path.splitext(filename)[0]
|
||||
def _build(filename, args) -> int:
|
||||
known_extensions = readers.readers_by_extension.keys()
|
||||
# Check if it's a directory we can recurse through
|
||||
if filename.is_dir():
|
||||
num_imported = 0
|
||||
for child_file in filename.iterdir():
|
||||
num_imported += _build(child_file, args)
|
||||
return num_imported
|
||||
# Check if we know how to import this file
|
||||
if filename.suffix not in known_extensions:
|
||||
return 0
|
||||
# Single known file, so import it
|
||||
basename = filename.stem
|
||||
print(f"Processing {basename}...")
|
||||
try:
|
||||
make_sheet(character_file=filename, flatten=(not args.editable),
|
||||
@@ -695,13 +707,13 @@ def _build(filename, args):
|
||||
raise
|
||||
else:
|
||||
print(f"{basename} done")
|
||||
|
||||
return 1
|
||||
|
||||
def main():
|
||||
# Prepare an argument parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Prepare Dungeons and Dragons character sheets as PDFs')
|
||||
parser.add_argument('filename', type=str, nargs="?",
|
||||
parser.add_argument('filename', type=str, nargs="*",
|
||||
help="Python file with character definition")
|
||||
parser.add_argument('--editable', '-e', action="store_true",
|
||||
help="Keep the PDF fields in place once processed.")
|
||||
@@ -714,11 +726,25 @@ def main():
|
||||
# Prepare logging if necessary
|
||||
if args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
# Process the requested files
|
||||
if args.filename is None:
|
||||
filenames = [f for f in os.listdir('.') if os.path.splitext(f)[1] == '.py']
|
||||
# Build the true list of filenames
|
||||
input_filenames = args.filename
|
||||
known_extensions = readers.readers_by_extension.keys()
|
||||
if input_filenames == []:
|
||||
input_filenames = [Path()]
|
||||
else:
|
||||
filenames = [args.filename]
|
||||
input_filenames = [Path(f) for f in input_filenames]
|
||||
def get_char_files(fpath):
|
||||
valid_files = []
|
||||
if fpath.is_dir():
|
||||
for f in fpath.iterdir():
|
||||
valid_files.extend(get_char_files(f))
|
||||
elif fpath.suffix in known_extensions:
|
||||
valid_files.append(fpath)
|
||||
return valid_files
|
||||
filenames = []
|
||||
for fpath in input_filenames:
|
||||
filenames.extend(get_char_files(fpath))
|
||||
# Process the requested files
|
||||
if args.debug:
|
||||
for filename in filenames:
|
||||
_build(filename, args)
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import importlib
|
||||
import warnings
|
||||
import json
|
||||
import re
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from dungeonsheets import exceptions
|
||||
from dungeonsheets import spells
|
||||
|
||||
log = logging.getLogger(__file__)
|
||||
|
||||
|
||||
def read_character_file(filename: str):
|
||||
"""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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename
|
||||
The path to the file that will be imported.
|
||||
|
||||
"""
|
||||
filename = Path(filename)
|
||||
# Parse the file name
|
||||
dir_ = filename.parent
|
||||
fname = filename.name
|
||||
ext = filename.suffix
|
||||
try:
|
||||
reader = readers_by_extension[ext]()
|
||||
except KeyError:
|
||||
raise ValueError(f"Character definition {filename} is not a known file type.")
|
||||
else:
|
||||
new_char = reader(filename=filename)
|
||||
return new_char
|
||||
|
||||
|
||||
class BaseCharacterReader():
|
||||
"""Callable to parse a generic character file. Meant to be subclassed."""
|
||||
def __call__(self, filename):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
filename
|
||||
The path to the file that will be imported.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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()
|
||||
def json_data(self):
|
||||
# Load the JSON data from disk
|
||||
with open(self.filename, mode='r') as fp:
|
||||
data = json.load(fp)
|
||||
return data
|
||||
|
||||
def as_int(self, val):
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
val = 0
|
||||
return val
|
||||
|
||||
def get_attrib(self, key, which="current", default=None):
|
||||
for obj in self.json_data()['attribs']:
|
||||
if obj['name'] == key:
|
||||
val = obj[which]
|
||||
return val
|
||||
# No object was found
|
||||
if default is not None:
|
||||
return default
|
||||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def has_skill_proficiency(self, key):
|
||||
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.
|
||||
|
||||
*kind* can be one of "weapon", "language", or None (all
|
||||
proficiencies).
|
||||
|
||||
"""
|
||||
prof_re = re.compile("repeating_proficiencies_([-0-9a-zA-Z]+)_name")
|
||||
for obj in self.json_data()['attribs']:
|
||||
match = prof_re.match(obj['name'])
|
||||
if match:
|
||||
prof_id = match.group(1)
|
||||
prof_type = self.get_attrib(f"repeating_proficiencies_{prof_id}_prof_type")
|
||||
if kind is None or prof_type == kind.upper():
|
||||
yield self.get_attrib(f"repeating_proficiencies_{prof_id}_name").lower()
|
||||
|
||||
def equipment(self, kind=None):
|
||||
"""Iterator over items in the character's inventory.
|
||||
|
||||
"""
|
||||
prof_re = re.compile("repeating_inventory_([-0-9a-zA-Z]+)_itemname")
|
||||
for obj in self.json_data()['attribs']:
|
||||
match = prof_re.match(obj['name'])
|
||||
if match:
|
||||
item_id = match.group(1)
|
||||
item_name = self.get_attrib(match.group(0))
|
||||
item_count = int(self.get_attrib(f"repeating_inventory_{item_id}_itemcount", default=1))
|
||||
item_weight = self.get_attrib(f"repeating_inventory_{item_id}_itemweight", default=0)
|
||||
item_str = item_name.lower().strip()
|
||||
if item_count > 1:
|
||||
item_str += f" ({item_count})"
|
||||
yield item_str
|
||||
|
||||
def weapons(self):
|
||||
"""Iterator over the weapons the character is carrying in her inventory."""
|
||||
item_re = re.compile("repeating_attack_([-0-9a-zA-Z]+)_atkname")
|
||||
for obj in self.json_data()['attribs']:
|
||||
match = item_re.match(obj['name'])
|
||||
if match:
|
||||
weapon_name = self.get_attrib(match.group()).lower()
|
||||
if weapon_name[:3] == "i. ":
|
||||
# Ignore artificer infusions
|
||||
warnings.warn("Ignoring weapon infusion")
|
||||
else:
|
||||
weapon_name = weapon_name.split('(')[0].strip()
|
||||
weapon_name = weapon_name.split(',')[0].strip()
|
||||
yield weapon_name
|
||||
|
||||
def __call__(self, filename: str):
|
||||
"""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:
|
||||
raise exceptions.JSONFormatError("Cannot parse JSON schema version: %s" % version)
|
||||
# Parse the json tree to get character properties
|
||||
char_props = {}
|
||||
char_props['name'] = self.json_data()['name']
|
||||
char_props['level'] = self.as_int(self.get_attrib('base_level'))
|
||||
char_props['classes'] = [self.get_attrib('class')]
|
||||
char_props['background'] = self.get_attrib('background')
|
||||
char_props['race'] = self.get_attrib('subrace')
|
||||
char_props['alignment'] = self.get_attrib('alignment')
|
||||
char_props['xp'] = self.as_int(self.get_attrib('experience', default=0))
|
||||
# Attributes
|
||||
attribute_names = ['strength', 'dexterity', 'constitution',
|
||||
'intelligence', 'wisdom', 'charisma']
|
||||
for attr in attribute_names:
|
||||
char_props[attr] = self.as_int(self.get_attrib(f"{attr}_base"))
|
||||
# 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.has_skill_proficiency(skill)]
|
||||
char_props['skill_proficiencies'] = skill_profs
|
||||
# Other proficiencies
|
||||
char_props['weapon_proficiencies'] = self.proficiencies("weapon")
|
||||
char_props['languages'] = ", ".join(self.proficiencies("language"))
|
||||
# Tool proficiencies
|
||||
prof_re = re.compile("repeating_tool_([-0-9a-zA-Z]+)_toolname")
|
||||
tool_profs = []
|
||||
for obj in self.json_data()['attribs']:
|
||||
match = prof_re.match(obj['name'])
|
||||
if match:
|
||||
tool_profs.append(self.get_attrib(match.group(0)))
|
||||
char_props['_proficiencies_text'] = tool_profs
|
||||
# Combat stats
|
||||
char_props['hp_max'] = self.as_int(self.get_attrib('hp', which="max"))
|
||||
# Equipment
|
||||
char_props['cp'] = self.as_int(self.get_attrib('cp', default=0))
|
||||
char_props['sp'] = self.as_int(self.get_attrib('sp', default=0))
|
||||
char_props['ep'] = self.as_int(self.get_attrib('ep', default=0))
|
||||
char_props['gp'] = self.as_int(self.get_attrib('gp', default=0))
|
||||
char_props['pp'] = self.as_int(self.get_attrib('pp', default=0))
|
||||
char_props['weapons'] = self.weapons()
|
||||
char_props['equipment'] = ", ".join(self.equipment())
|
||||
# Personality, etc
|
||||
char_props['personality_traits'] = self.get_attrib('personality_traits').strip()
|
||||
char_props['flaws'] = self.get_attrib('flaws').strip()
|
||||
char_props['ideals'] = self.get_attrib('ideals').strip()
|
||||
char_props['bonds'] = self.get_attrib('bonds').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, armor, shield, attacks_and_spellcasting, "
|
||||
"infusions, wild_shapes")
|
||||
warnings.warn(warn_msg)
|
||||
log.warning(warn_msg)
|
||||
char_props['magic_items'] = ()
|
||||
char_props['armor'] = ""
|
||||
char_props["shield"] = ""
|
||||
char_props["attacks_and_spellcasting"] = ""
|
||||
char_props["infusions"] = []
|
||||
char_props["wild_shapes"] = []
|
||||
return char_props
|
||||
|
||||
|
||||
class PythonCharacterReader(BaseCharacterReader):
|
||||
def __call__(self, filename: str):
|
||||
"""Create a character object from the given definition file.
|
||||
|
||||
The definition file should be an importable python file, filled
|
||||
with variables describing the character.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename
|
||||
The path to the file that will be imported.
|
||||
|
||||
"""
|
||||
# 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:
|
||||
version = None
|
||||
for line in f:
|
||||
match = version_re.match(line)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
break
|
||||
if version is None:
|
||||
# Not a valid DND character file
|
||||
raise exceptions.CharacterFileFormatError(
|
||||
f"No ``dungeonsheets_version = `` entry in `{filename}`.")
|
||||
# Import the module to extract the information
|
||||
spec = importlib.util.spec_from_file_location('module', filename)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# Prepare a list of properties for this character
|
||||
char_props = {}
|
||||
for prop_name in dir(module):
|
||||
if prop_name[0:2] != '__':
|
||||
char_props[prop_name] = getattr(module, prop_name)
|
||||
return char_props
|
||||
|
||||
|
||||
readers_by_extension = {
|
||||
'.py': PythonCharacterReader,
|
||||
'.json': JSONCharacterReader,
|
||||
}
|
||||
@@ -484,6 +484,9 @@ class Unarmed(MeleeWeapon):
|
||||
ability = "strength"
|
||||
|
||||
|
||||
UnarmedStrike = Unarmed
|
||||
|
||||
|
||||
class SunBolt(RangedWeapon):
|
||||
name = "Sun Bolt"
|
||||
cost = "0 gp"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from unittest import TestCase
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
from dungeonsheets import race, monsters, exceptions, spells
|
||||
from dungeonsheets.character import Character, Wizard, Druid
|
||||
from dungeonsheets.character import Character, Wizard, Druid, read_character_file
|
||||
from dungeonsheets.weapons import Weapon, Shortsword
|
||||
from dungeonsheets.armor import Armor, LeatherArmor, Shield
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import unittest
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
from dungeonsheets import make_sheets, character
|
||||
|
||||
@@ -7,6 +9,10 @@ EG_DIR = os.path.abspath(os.path.join(os.path.split(__file__)[0], '../examples/'
|
||||
CHARFILE = os.path.join(EG_DIR, 'rogue1.py')
|
||||
|
||||
class CharacterFileTestCase(unittest.TestCase):
|
||||
example_dir = Path(__file__).parent.parent / "examples"
|
||||
# def test_(self):
|
||||
# print(self.example_dir)
|
||||
# subprocess.run(['makesheets', self.example_dir])
|
||||
def test_load_character_file(self):
|
||||
charfile = CHARFILE
|
||||
result = make_sheets.load_character_file(charfile)
|
||||
@@ -110,3 +116,5 @@ class MarkdownTestCase(unittest.TestCase):
|
||||
tex = make_sheets.rst_to_latex(md_list)
|
||||
print(tex)
|
||||
self.assertIn("\\begin{itemize}", tex)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
import types
|
||||
|
||||
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'
|
||||
SPELLCASTER_JSON_FILE = EG_DIR / 'artificer2.json'
|
||||
|
||||
class PythonReaderTests(unittest.TestCase):
|
||||
def test_load_python_file(self):
|
||||
charfile = CHAR_PYTHON_FILE
|
||||
result = read_character_file(charfile)
|
||||
self.assertEqual(result['strength'], 10)
|
||||
|
||||
|
||||
class JSONReaderTests(unittest.TestCase):
|
||||
def test_load_json_file(self):
|
||||
charfile = CHAR_JSON_FILE
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
result = read_character_file(charfile)
|
||||
expected_data = dict(
|
||||
name="Ulthar Jenkins",
|
||||
classes=["Barbarian"],
|
||||
level=2,
|
||||
background="Soldier",
|
||||
alignment="Lawful Evil",
|
||||
race="Hill Dwarf",
|
||||
xp=557,
|
||||
strength=13,
|
||||
dexterity=12,
|
||||
constitution=19,
|
||||
intelligence=8,
|
||||
hp_max=32,
|
||||
skill_proficiencies=["athletics", "survival",],
|
||||
weapon_proficiencies=["simple weapons", "martial weapons", "battleaxe", "handaxe", "light hammer", "warhammer", "unarmed strike",],
|
||||
_proficiencies_text=["Brewer's Supplies",],
|
||||
languages="common, dwarvish",
|
||||
cp=26,
|
||||
sp=55,
|
||||
ep=0,
|
||||
gp=207,
|
||||
pp=0,
|
||||
weapons=["handaxe", "javelin", "warhammer"],
|
||||
magic_items=(),
|
||||
armor="",
|
||||
shield="",
|
||||
personality_traits="Can easily dismember a body\n\nKnow fight battle tactics",
|
||||
ideals="Vengence",
|
||||
bonds="friends and adventurers.",
|
||||
flaws="Bloodthirsty and wants to solve every problem by murder",
|
||||
equipment=("warhammer, handaxe, explorer's pack, javelin (4), backpack, "
|
||||
"bedroll, mess kit, tinderbox, torch (10), rations (10), "
|
||||
"waterskin, hempen rope"),
|
||||
attacks_and_spellcasting="",
|
||||
spells_prepared=[],
|
||||
spells=[],
|
||||
)
|
||||
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) as w:
|
||||
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