diff --git a/README.rst b/README.rst index 7ceac6d..c6667a8 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,11 @@ A tool to create character sheets for Dungeons and Dragons. .. image:: https://travis-ci.com/canismarko/dungeon-sheets.svg?branch=master - :target: https://travis-ci.com/canismarko/dungeon-sheets + :target: https://travis-ci.com/canismarko/dungeon-sheets + +.. image:: https://readthedocs.org/projects/dungeon-sheets/badge/?version=latest + :target: https://dungeon-sheets.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status Installation ============ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..298ea9e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/character_files.html b/docs/character_files.html new file mode 100644 index 0000000..dfc1202 --- /dev/null +++ b/docs/character_files.html @@ -0,0 +1,392 @@ + + + + + + +Character Files + + + +
+

Character Files

+ +

Dungeonsheets expects one file per character, with a .dnd +extension. An older python file format is still supported, but not +recommended for security reasons. Details for this older style are +provided below.

+
+

Old Python Character Files

+
+

Warning

+

The mechanism described below is insecure since it involves +directly running python modules. Do not run files that you did not +write yourself and do not trust completely. Maliciously-crafted +files can run python code during import time.

+
+

Previous versions of this library used python files that are directly +imported to produce the appropriate values. The benefit is that this +allows much more versatile content, but at the expense of allowing +these files to run un-sanitized python code on your machine. This +behavior is still supported for now for backwards compatibility +reasons, but do so at your own risk.

+

An example of this syntax can be found in :ref:`this wizard example<Wizard Example (Python)>`.

+
+

System Message: ERROR/3 (character_files.rst, line 29); backlink

+Unknown interpreted text role "ref".
+
+
+ + diff --git a/docs/character_files.rst b/docs/character_files.rst new file mode 100644 index 0000000..f7f46a8 --- /dev/null +++ b/docs/character_files.rst @@ -0,0 +1,215 @@ +================= + Character Files +================= + +.. warning:: + + Character files are python modules that are imported when + parsed. **NEVER parse a character file without inspecting it** to + verify that there are no unexpected consequences, especially a file + from someone you do not trust. + +Dungeonsheets expects one file per character, with a ``.py`` +extension. This file is a python module, most likely with a series of +variables set describing the character. They are roughly grouped into +sections, which are documented below. Additionally, some +:ref:`examples` may be useful. + +Each character file must contain a line like:: + + dungeonsheets_version = "0.4.2" + +Without this line, the `makesheets`_ command-line utility will ignore +the file. This is necessary to avoid importing non-D&D python files. + +.. note:: + + Some proficiencies, character traits, abilities, etc.\ are the + result of the character's race and/or background. These **must + still be included** in the character file and will not be + automatically added if omitted. + +Basic Info +========== + +The character file will contain several basic information values that +are fairly self-evident. The values for ``character_class``, +``background``, ``race`` and ``alignment`` must match entries in the +standard 5e rules, and are case-insensitive. Refer to the D&D +`player's handbook`_ for more information. + +.. code:: python + + name = 'Inara Serradon' + character_class = 'wizard' + player_name = 'Mark' + background = "Acolyte" + race = "High-Elf" + level = 3 + alignment = "Chaotic good" + xp = 2190 + hp_max = 16 + +Ability Scores +============== + +Ability scores are numeric scores for each ability, as described in +the `player's handbook`_. + +.. code:: python + + # Ability Scores + strength = 10 + dexterity = 15 + constitution = 14 + intelligence = 16 + wisdom = 12 + charisma = 8 + +Proficiencies and Languages +=========================== + +This section may contain entries, one for ``skill_proficiencies`` and +one for ``languages``. ``skill_proficiencies`` must be an iterable of +case-insensitive strings matching skills described in the `player's +handbook`_. Languages is a standard string, since language proficiency +does not affect other areas of the character. + +.. code:: python + + # Proficiencies and languages + skill_proficiencies = [ + 'arcana', + 'insight', + 'investigation', + 'perception', + 'religion', + ] + languages = "Common, Elvish, Draconic, Dwarvish, Goblin." + + +Inventory +========= + +There are five entries for currencies, which must be +integers. ``weapons`` (iterable of strings), ``armor`` (string) and +``shield`` (string) must correspond to items available in the +`player's handbook`_. The ``equipment`` is a string that is rendered +as-is on the character sheet. + +.. todo:: Allow custom weapons and armor to be specified in the + character file. + +.. warning:: + + Not all weapons and armor have been entered into the + ``dungeonsheets`` library. If you receive an ``AttributeError`` + stating the item you entered is not defined despite being listed in + the `player's handbook`_, please submit an `issue`_. + +.. code:: python + + cp = 950 + sp = 75 + ep = 50 + gp = 120 + pp = 0 + weapons = ('shortsword', 'shortbow') + armor = 'light leather armor' + shield = 'shield' + equipment = ( + """Shortsword, shortbow, 20 arrows, leather armor, thieves’ tools, + backpack, bell, 5 candles, crowbar, hammer, 10 pitons, 50 feet of + hempen rope, hooded lantern, 2 flasks of oil, 5 days rations, + tinderbox, waterskin, crowbar, set of dark common clothes + including a hood, pouch.""") + +Spells +====== + +Two entries are available for spell-casting, and only if the class +supports spells. Both are lists of case-insensitive strings that must +correspond to spells described in the `player's handbook`_. + +.. todo:: Allow custom spells to be specified in the character file. + +.. warning:: + + Not all spells have been entered into the ``dungeonsheets`` + library. If you receive a ``UserWarning`` stating the spell you + entered is not defined despite being listed in the `player's + handbook`_, please submit an `issue`_. + +.. code:: python + + # List of known spells + spells = ('blindness deafness', 'burning hands', 'detect magic', + 'false life', 'mage armor', 'mage hand', 'magic missile', + 'prestidigitation', 'ray of frost', 'ray of sickness', 'shield', + 'shocking grasp', 'sleep',) + # Which spells have been prepared (not including cantrips) + spells_prepared = ('blindness deafness', 'false life', 'mage armor', + 'ray of sickness', 'shield', 'sleep',) + + +Personality and Backstory +========================= + +This section contains string that describe the nature and backstory of +the character. They will be printed as-is on the character +sheet. Triple-quoted string and parenthesis may make the character's +source file more readable, but are not required. + +.. code:: python + + # Backstory + personality_traits = """I use polysyllabic words that convey the impression of + erudition. Also, I’ve spent so long in the temple that I have little + experience dealing with people on a casual basis.""" + + ideals = """Knowledge. The path to power and self-improvement is through + knowledge.""" + + bonds = """The tome I carry with me is the record of my life’s work so far, + and no vault is secure enough to keep it safe.""" + + flaws = """I’ll do just about anything to uncover historical secrets that + would add to my research.""" + + features_and_traits = ( + """Spellcasting Ability: Intelligence is your spellcasting ability for + your spells. The saving throw DC to resist a spell you cast is + 13. Your attack bonus when you make an attack with a spell is + +5. See the rulebook for rules on casting your spells. + + Arcane Recovery: You can regain some of your magical energy by + studying your spellbook. Once per day during a short rest, you can + choose to recover expended spell slots with a combined level equal + to or less than half your wizard level (rounded up). + + Darkvision: You see in dim light within a 60-foot radius of you as + if it were bright light, and in darkness in that radius as if it + were dim light. You can’t discern color in darkness, only shades + of gray. + + Fey Ancestry: You have advantage on saving throws against being + charmed, and magic can’t put you to sleep. + + Trance: Elves don’t need to sleep. They meditate deeply, remaining + semiconscious, for 4 hours a day and gain the same benefit a human + does from 8 hours of sleep. + + Shelter of the Faithful: As a servant of Oghma, you command the + respect of those who share your faith, and you can perform the + rites of Oghma. You and your companions can expect to receive free + healing and care at a temple, shrine, or other established + presence of Oghma’s faith. Those who share your religion will + support you (and only you) at a modest lifestyle. You also have + ties to the temple of Oghma in Neverwinter, where you have a + residence. When you are in Neverwinter, you can call upon the + priests there for assistance that won’t endanger them.""") + + +.. _player's handbook: http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook + +.. _issue: https://github.com/canismarko/dungeon-sheets/issues diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..fa34e21 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'dungeonsheets' +copyright = '2018, Mark Wolfman' +author = 'Mark Wolfman' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.4.2' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.autosectionlabel', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'dungeonsheetsdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'dungeonsheets.tex', 'dungeonsheets Documentation', + 'Mark Wolfman', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'dungeonsheets', 'dungeonsheets Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'dungeonsheets', 'dungeonsheets Documentation', + author, 'dungeonsheets', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..edfba4d --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,20 @@ +========== + Examples +========== + +.. contents:: :local: + +Wizard +====== + +.. literalinclude:: ../examples/wizard.py + +Warlock +======= + +.. literalinclude:: ../examples/warlock.py + +Rogue +===== + +.. literalinclude:: ../examples/rogue.py diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..64e4178 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. dungeonsheets documentation master file, created by + sphinx-quickstart on Sun Oct 14 18:07:48 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Dungeonsheets's documentation! +========================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + character_files + examples + +To-Do Tasks +=========== +.. todolist:: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..27f573b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/dungeonsheets/character.py b/dungeonsheets/character.py index 79a92c6..3963614 100644 --- a/dungeonsheets/character.py +++ b/dungeonsheets/character.py @@ -106,7 +106,9 @@ class Character(): """Bulk setting of attributes. Useful for loading a character from a dictionary.""" for attr, val in attrs.items(): - if attr == 'weapons': + if attr == 'dungeonsheets_version': + pass # Maybe we'll verify this later? + elif attr == 'weapons': # Treat weapons specially for weap in val: self.wield_weapon(weap) @@ -249,7 +251,7 @@ class Character(): try: NewWeapon = findattr(weapons, weapon) except AttributeError: - raise AttributeError(f'Weapon {weapon} is not defined') + raise AttributeError(f'Weapon "{weapon}" is not defined') weapon_ = NewWeapon() # Set weapon attributes based on character if weapon_.is_finesse: diff --git a/dungeonsheets/exceptions.py b/dungeonsheets/exceptions.py index db6d580..6a72e38 100644 --- a/dungeonsheets/exceptions.py +++ b/dungeonsheets/exceptions.py @@ -1,3 +1,6 @@ +class CharacterFileFormatError(ValueError): + """The given file is not a valid Dungeons and Dragons file.""" + class DiceError(ValueError): """Improper formatting for a dice string.""" diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index e93d98c..298a60e 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -7,6 +7,7 @@ import importlib.util import os import subprocess import warnings +import re from fdfgen import forge_fdf @@ -43,6 +44,19 @@ def load_character_file(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-4.]+)[\'"]') + 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( + "No ``dungeonsheets_version = `` entry.") # Import the module to extract the information spec = importlib.util.spec_from_file_location('module', filename) module = importlib.util.module_from_spec(spec) @@ -265,7 +279,10 @@ def create_character_pdf(character, basename, flatten=False): } # Add skill proficienies for skill in character.skill_proficiencies: - fields.append((skill_boxes[skill.replace(' ', '_')], 'Yes')) + try: + fields.append((skill_boxes[skill.replace(' ', '_').lower()], 'Yes')) + except KeyError: + raise KeyError(f"Unknown skill: '{skill}'") # Add weapons weapon_fields = [('Wpn Name', 'Wpn1 AtkBonus', 'Wpn1 Damage'), ('Wpn Name 2', 'Wpn2 AtkBonus ', 'Wpn2 Damage '), @@ -368,6 +385,11 @@ def main(): print(f"Processing {os.path.splitext(filename)[0]}...", end='') try: make_sheet(character_file=filename, flatten=(not args.editable)) + except exceptions.CharacterFileFormatError as e: + # Only raise the failed exception if this file is explicitly given + print('invalid') + if args.filename: + raise except Exception as e: print('failed') raise diff --git a/examples/rogue.pdf b/examples/rogue.pdf index 3477fee..35825c8 100644 Binary files a/examples/rogue.pdf and b/examples/rogue.pdf differ diff --git a/examples/rogue.py b/examples/rogue.py index 14d8656..a7b4784 100644 --- a/examples/rogue.py +++ b/examples/rogue.py @@ -1,3 +1,6 @@ +dungeonsheets_version = "0.4.2" + +# Basic information name = 'Mr. Stabby' character_class = 'rogue' player_name = 'Mike' diff --git a/examples/warlock.pdf b/examples/warlock.pdf index 319d40b..d96e986 100644 Binary files a/examples/warlock.pdf and b/examples/warlock.pdf differ diff --git a/examples/warlock.py b/examples/warlock.py index 0f632ef..1726ee4 100644 --- a/examples/warlock.py +++ b/examples/warlock.py @@ -1,3 +1,6 @@ +dungeonsheets_version = "0.4.2" + +# Basic information name = 'Sid Istick' character_class = 'Warlock' player_name = 'Mark' diff --git a/examples/wizard.pdf b/examples/wizard.pdf index 9c49884..6b6c5de 100644 Binary files a/examples/wizard.pdf and b/examples/wizard.pdf differ diff --git a/examples/wizard.py b/examples/wizard.py index 38a3f58..a0cc6d2 100644 --- a/examples/wizard.py +++ b/examples/wizard.py @@ -1,3 +1,6 @@ +dungeonsheets_version = "0.4.2" + +# Basic information name = 'Inara Serradon' character_class = 'wizard' player_name = 'Mark' @@ -15,6 +18,8 @@ constitution = 14 intelligence = 16 wisdom = 12 charisma = 8 + +# Proficiencies and languages skill_proficiencies = [ 'arcana', 'insight', @@ -22,8 +27,6 @@ skill_proficiencies = [ 'perception', 'religion', ] - -# Proficiencies and languages languages = "Common, Elvish, Draconic, Dwarvish, Goblin." # Inventory @@ -42,7 +45,7 @@ equipment = ( # List of known spells spells = ('blindness deafness', 'burning hands', 'detect magic', - 'false life', 'mage armor', 'mage hand', 'magic missile', + 'falsee life', 'mage armor', 'mage hand', 'magic missile', 'prestidigitation', 'ray of frost', 'ray of sickness', 'shield', 'shocking grasp', 'sleep',) # Which spells have been prepared (not including cantrips) @@ -68,7 +71,7 @@ features_and_traits = ( your spells. The saving throw DC to resist a spell you cast is 13. Your attack bonus when you make an attack with a spell is +5. See the rulebook for rules on casting your spells. - + Arcane Recovery: You can regain some of your magical energy by studying your spellbook. Once per day during a short rest, you can choose to recover expended spell slots with a combined level equal diff --git a/requirements.txt b/requirements.txt index 09dbe45..67bd0b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ fdfgen>=0.16 npyscreen jinja2 pytest +sphinx