mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Made a Content base class, and changed GM extra content interface.
This commit is contained in:
@@ -13,12 +13,5 @@ __all__ = (
|
||||
from dungeonsheets import background, features, race, spells, weapons, mechanics
|
||||
from dungeonsheets.character import Character
|
||||
from dungeonsheets.content_registry import import_homebrew
|
||||
from dungeonsheets.content import __version__
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
|
||||
__version__ = read("../VERSION")
|
||||
|
||||
@@ -23,7 +23,7 @@ from dungeonsheets import (
|
||||
)
|
||||
from dungeonsheets.content_registry import find_content
|
||||
from dungeonsheets.weapons import Weapon
|
||||
from dungeonsheets.entity import Entity
|
||||
from dungeonsheets.content import Creature
|
||||
|
||||
|
||||
dice_re = re.compile(r"(\d+)d(\d+)")
|
||||
@@ -70,7 +70,7 @@ multiclass_spellslots_by_level = {
|
||||
}
|
||||
|
||||
|
||||
class Character(Entity):
|
||||
class Character(Creature):
|
||||
"""A generic player character."""
|
||||
|
||||
# Character-specific
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Base classes for the various D&D 5e content types."""
|
||||
|
||||
|
||||
import warnings
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
|
||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open((Path(__file__).parent / fname).resolve()).read()
|
||||
|
||||
|
||||
__version__ = read("../VERSION").strip()
|
||||
|
||||
|
||||
class Content(ABC):
|
||||
"""A base class for all D&D 5e content types.
|
||||
|
||||
Every piece of content (e.g. class feature, spell, monster) should
|
||||
have this base class in its inheritance tree.
|
||||
|
||||
"""
|
||||
dungeonsheets_version = __version__
|
||||
name = "Generic content"
|
||||
|
||||
|
||||
class Creature(Content):
|
||||
"""A thing with stats. Use Monster or Character, not this class
|
||||
directly!
|
||||
|
||||
"""
|
||||
# General attributes
|
||||
alignment = "Neutral"
|
||||
_race = None
|
||||
name = "Generic creature"
|
||||
# Hit points
|
||||
hp_max = None
|
||||
# Base stats (ability scores)
|
||||
strength = Ability()
|
||||
dexterity = Ability()
|
||||
constitution = Ability()
|
||||
intelligence = Ability()
|
||||
wisdom = Ability()
|
||||
charisma = Ability()
|
||||
# Numerical things
|
||||
armor_class = ArmorClass()
|
||||
initiative = Initiative()
|
||||
speed = Speed()
|
||||
# Proficiencies and Languages
|
||||
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
|
||||
other_weapon_proficiencies = tuple() # add to class/race proficiencies
|
||||
skill_proficiencies = list()
|
||||
skill_expertise = list()
|
||||
languages = ""
|
||||
senses = ""
|
||||
# Skills
|
||||
acrobatics = Skill(ability="dexterity")
|
||||
animal_handling = Skill(ability="wisdom")
|
||||
arcana = Skill(ability="intelligence")
|
||||
athletics = Skill(ability="strength")
|
||||
deception = Skill(ability="charisma")
|
||||
history = Skill(ability="intelligence")
|
||||
insight = Skill(ability="wisdom")
|
||||
intimidation = Skill(ability="charisma")
|
||||
investigation = Skill(ability="intelligence")
|
||||
medicine = Skill(ability="wisdom")
|
||||
nature = Skill(ability="intelligence")
|
||||
perception = Skill(ability="wisdom")
|
||||
performance = Skill(ability="charisma")
|
||||
persuasion = Skill(ability="charisma")
|
||||
religion = Skill(ability="intelligence")
|
||||
sleight_of_hand = Skill(ability="dexterity")
|
||||
stealth = Skill(ability="dexterity")
|
||||
survival = Skill(ability="wisdom")
|
||||
# Inventory
|
||||
cp = 0
|
||||
sp = 0
|
||||
ep = 0
|
||||
gp = 0
|
||||
pp = 0
|
||||
equipment = ""
|
||||
_weapons = list()
|
||||
magic_items = list()
|
||||
armor = None
|
||||
shield = None
|
||||
# Magic
|
||||
spellcasting_ability = None
|
||||
_spells = list()
|
||||
_spells_prepared = list()
|
||||
infusions = list()
|
||||
# Features IN MAJOR DEVELOPMENT
|
||||
custom_features = list()
|
||||
feature_choices = list()
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def weapons(self):
|
||||
return self._weapons.copy()
|
||||
|
||||
@property
|
||||
def passive_wisdom(self):
|
||||
return self.perception.modifier + 10
|
||||
|
||||
@property
|
||||
def abilities(self):
|
||||
return [self.strength, self.dexterity, self.constitution,
|
||||
self.intelligence, self.wisdom, self.charisma]
|
||||
|
||||
@property
|
||||
def skills(self):
|
||||
return [self.acrobatics, self.animal_handling, self.arcana,
|
||||
self.athletics, self.deception, self.history,
|
||||
self.insight, self.intimidation, self.investigation,
|
||||
self.medicine, self.nature, self.perception,
|
||||
self.performance, self.persuasion, self.religion,
|
||||
self.sleight_of_hand, self.stealth, self.survival]
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mechanic(mechanic, SuperClass, warning_message=None):
|
||||
"""Take a raw entry in a character sheet and turn it into a usable object.
|
||||
|
||||
Eg: spells can be defined in many ways. This function accepts all
|
||||
of those options and returns an actual *Spell* class that can be
|
||||
used by a character::
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
||||
|
||||
>>> from dungeonsheets import spells
|
||||
>>> class MySpell(spells.Spell): pass
|
||||
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
||||
|
||||
The acceptable entries for *mechanic*, in priority order, are:
|
||||
1. A subclass of *SuperClass*
|
||||
2. A string with the name of defined content
|
||||
3. The name of an unknown spell (creates generic object using *factory*)
|
||||
|
||||
*SuperClass* can be ``None`` to match any class, however this will
|
||||
raise an exception if more than one content type has this
|
||||
name. For example, "shield" can refer to both the armor or the
|
||||
spell, so ``_resolve_mechanic("shield")`` will raise an exception.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
mechanic : str, type
|
||||
The thing to be resolved, either a string with the name of the
|
||||
mechanic, or a subclass of *ParentClass* describing the
|
||||
mechanic.
|
||||
SuperClass : type
|
||||
Class to determine whether *mechanic* should just be allowed
|
||||
through as is.
|
||||
error_message : str, optional
|
||||
A string whose ``str.format()`` method (receiving one positional
|
||||
argument *mechanic*) will be used for displaying a warning when an
|
||||
unknown mechanic is resolved. If omitted, no warning will be
|
||||
displayed.
|
||||
|
||||
Returns
|
||||
=======
|
||||
Mechanic
|
||||
A class representing the resolved game mechanic. This will
|
||||
likely be a subclass of *SuperClass* if the other parameters are
|
||||
well behaved, but this is not enforced.
|
||||
|
||||
"""
|
||||
is_already_resolved = isinstance(mechanic, type) and issubclass(
|
||||
mechanic, SuperClass
|
||||
)
|
||||
if is_already_resolved:
|
||||
Mechanic = mechanic
|
||||
elif SuperClass is not None and isinstance(mechanic, SuperClass):
|
||||
# Has been instantiated for some reason
|
||||
Mechanic = type(Mechanic)
|
||||
else:
|
||||
try:
|
||||
# Retrieve pre-defined mechanic
|
||||
valid_classes = [SuperClass] if SuperClass is not None else []
|
||||
Mechanic = find_content(mechanic, valid_classes=valid_classes)
|
||||
except ValueError:
|
||||
# No pre-defined mechanic available
|
||||
if warning_message is not None:
|
||||
# Emit the warning
|
||||
msg = warning_message.format(mechanic)
|
||||
warnings.warn(msg)
|
||||
else:
|
||||
# Create a generic message so we can make a docstring later.
|
||||
msg = f'Mechanic "{mechanic}" not defined. Please add it.'
|
||||
# Create generic mechanic from the factory
|
||||
class_name = "".join([s.title() for s in mechanic.split("_")])
|
||||
mechanic_name = mechanic.replace("_", " ").title()
|
||||
attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"}
|
||||
Mechanic = type(class_name, (SuperClass,), attrs)
|
||||
return Mechanic
|
||||
@@ -1,194 +0,0 @@
|
||||
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
|
||||
from abc import ABC
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
|
||||
__version__ = read("../VERSION").strip()
|
||||
|
||||
|
||||
class Entity(ABC):
|
||||
"""A thing with stats. Use Monster or Character, not this class directly!"""
|
||||
|
||||
# General attributes
|
||||
dungeonsheets_version = __version__
|
||||
name = ""
|
||||
alignment = "Neutral"
|
||||
_race = None
|
||||
|
||||
# Hit points
|
||||
hp_max = None
|
||||
|
||||
# Base stats (ability scores)
|
||||
strength = Ability()
|
||||
dexterity = Ability()
|
||||
constitution = Ability()
|
||||
intelligence = Ability()
|
||||
wisdom = Ability()
|
||||
charisma = Ability()
|
||||
|
||||
# Numerical things
|
||||
armor_class = ArmorClass()
|
||||
initiative = Initiative()
|
||||
speed = Speed()
|
||||
|
||||
# Proficiencies and Languages
|
||||
_saving_throw_proficiencies = tuple() # use to overwrite class proficiencies
|
||||
other_weapon_proficiencies = tuple() # add to class/race proficiencies
|
||||
skill_proficiencies = list()
|
||||
skill_expertise = list()
|
||||
languages = ""
|
||||
senses = ""
|
||||
|
||||
# Skills
|
||||
acrobatics = Skill(ability="dexterity")
|
||||
animal_handling = Skill(ability="wisdom")
|
||||
arcana = Skill(ability="intelligence")
|
||||
athletics = Skill(ability="strength")
|
||||
deception = Skill(ability="charisma")
|
||||
history = Skill(ability="intelligence")
|
||||
insight = Skill(ability="wisdom")
|
||||
intimidation = Skill(ability="charisma")
|
||||
investigation = Skill(ability="intelligence")
|
||||
medicine = Skill(ability="wisdom")
|
||||
nature = Skill(ability="intelligence")
|
||||
perception = Skill(ability="wisdom")
|
||||
performance = Skill(ability="charisma")
|
||||
persuasion = Skill(ability="charisma")
|
||||
religion = Skill(ability="intelligence")
|
||||
sleight_of_hand = Skill(ability="dexterity")
|
||||
stealth = Skill(ability="dexterity")
|
||||
survival = Skill(ability="wisdom")
|
||||
|
||||
# Inventory
|
||||
cp = 0
|
||||
sp = 0
|
||||
ep = 0
|
||||
gp = 0
|
||||
pp = 0
|
||||
equipment = ""
|
||||
_weapons = list()
|
||||
magic_items = list()
|
||||
armor = None
|
||||
shield = None
|
||||
|
||||
# Magic
|
||||
spellcasting_ability = None
|
||||
_spells = list()
|
||||
_spells_prepared = list()
|
||||
infusions = list()
|
||||
|
||||
# Features IN MAJOR DEVELOPMENT
|
||||
custom_features = list()
|
||||
feature_choices = list()
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def weapons(self):
|
||||
return self._weapons.copy()
|
||||
|
||||
@property
|
||||
def passive_wisdom(self):
|
||||
return self.perception.modifier + 10
|
||||
|
||||
@property
|
||||
def abilities(self):
|
||||
return [self.strength, self.dexterity, self.constitution,
|
||||
self.intelligence, self.wisdom, self.charisma]
|
||||
|
||||
@property
|
||||
def skills(self):
|
||||
return [self.acrobatics, self.animal_handling, self.arcana,
|
||||
self.athletics, self.deception, self.history,
|
||||
self.insight, self.intimidation, self.investigation,
|
||||
self.medicine, self.nature, self.perception,
|
||||
self.performance, self.persuasion, self.religion,
|
||||
self.sleight_of_hand, self.stealth, self.survival,]
|
||||
|
||||
@staticmethod
|
||||
def _resolve_mechanic(mechanic, SuperClass, warning_message=None):
|
||||
"""Take a raw entry in a character sheet and turn it into a usable object.
|
||||
|
||||
Eg: spells can be defined in many ways. This function accepts all
|
||||
of those options and returns an actual *Spell* class that can be
|
||||
used by a character::
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
||||
|
||||
>>> from dungeonsheets import spells
|
||||
>>> class MySpell(spells.Spell): pass
|
||||
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
||||
|
||||
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
||||
|
||||
The acceptable entries for *mechanic*, in priority order, are:
|
||||
1. A subclass of *SuperClass*
|
||||
2. A string with the name of defined content
|
||||
3. The name of an unknown spell (creates generic object using *factory*)
|
||||
|
||||
*SuperClass* can be ``None`` to match any class, however this will
|
||||
raise an exception if more than one content type has this
|
||||
name. For example, "shield" can refer to both the armor or the
|
||||
spell, so ``_resolve_mechanic("shield")`` will raise an exception.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
mechanic : str, type
|
||||
The thing to be resolved, either a string with the name of the
|
||||
mechanic, or a subclass of *ParentClass* describing the
|
||||
mechanic.
|
||||
SuperClass : type
|
||||
Class to determine whether *mechanic* should just be allowed
|
||||
through as is.
|
||||
error_message : str, optional
|
||||
A string whose ``str.format()`` method (receiving one positional
|
||||
argument *mechanic*) will be used for displaying a warning when an
|
||||
unknown mechanic is resolved. If omitted, no warning will be
|
||||
displayed.
|
||||
|
||||
Returns
|
||||
=======
|
||||
Mechanic
|
||||
A class representing the resolved game mechanic. This will
|
||||
likely be a subclass of *SuperClass* if the other parameters are
|
||||
well behaved, but this is not enforced.
|
||||
|
||||
"""
|
||||
is_already_resolved = isinstance(mechanic, type) and issubclass(
|
||||
mechanic, SuperClass
|
||||
)
|
||||
if is_already_resolved:
|
||||
Mechanic = mechanic
|
||||
elif SuperClass is not None and isinstance(mechanic, SuperClass):
|
||||
# Has been instantiated for some reason
|
||||
Mechanic = type(Mechanic)
|
||||
else:
|
||||
try:
|
||||
# Retrieve pre-defined mechanic
|
||||
valid_classes = [SuperClass] if SuperClass is not None else []
|
||||
Mechanic = find_content(mechanic, valid_classes=valid_classes)
|
||||
except ValueError:
|
||||
# No pre-defined mechanic available
|
||||
if warning_message is not None:
|
||||
# Emit the warning
|
||||
msg = warning_message.format(mechanic)
|
||||
warnings.warn(msg)
|
||||
else:
|
||||
# Create a generic message so we can make a docstring later.
|
||||
msg = f'Mechanic "{mechanic}" not defined. Please add it.'
|
||||
# Create generic mechanic from the factory
|
||||
class_name = "".join([s.title() for s in mechanic.split("_")])
|
||||
mechanic_name = mechanic.replace("_", " ").title()
|
||||
attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"}
|
||||
Mechanic = type(class_name, (SuperClass,), attrs)
|
||||
return Mechanic
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<h1 id="[[ title | to_heading_id ]]">[[ title ]]</h1>
|
||||
[% for section in sections %]
|
||||
|
||||
[[ rst | rst_to_html ]]
|
||||
<h1 id="extra-[[ section.name | to_heading_id ]]">[[ section.name ]]</h1>
|
||||
|
||||
[[ section.__doc__ | rst_to_html ]]
|
||||
|
||||
[% endfor %]
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
\pdfbookmark[0]{[[ title ]]}{[[ title ]]}
|
||||
\section*{[[ title ]]}
|
||||
[% for section in sections %]
|
||||
|
||||
[[ rst | rst_to_latex ]]
|
||||
\pdfbookmark[0]{[[ section.name ]]}{[[ section.name ]]}
|
||||
\section*{[[ section.name ]]}
|
||||
|
||||
[[ section.__doc__ | rst_to_latex ]]
|
||||
|
||||
[% endfor %]
|
||||
|
||||
@@ -28,7 +28,7 @@ from dungeonsheets.fill_pdf_template import (
|
||||
create_spells_pdf_template,
|
||||
)
|
||||
from dungeonsheets.character import Character
|
||||
from dungeonsheets.entity import Entity
|
||||
from dungeonsheets.content import Creature
|
||||
|
||||
"""Program to take character definitions and build a PDF of the
|
||||
character sheet."""
|
||||
@@ -92,7 +92,7 @@ def create_monsters_content(
|
||||
|
||||
|
||||
def create_party_summary_content(
|
||||
party: Sequence[Entity],
|
||||
party: Sequence[Creature],
|
||||
summary_rst: str,
|
||||
suffix: str,
|
||||
use_dnd_decorations: bool = False,
|
||||
@@ -104,8 +104,6 @@ def create_party_summary_content(
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def create_random_tables_content(
|
||||
conjure_animals: bool,
|
||||
suffix: str,
|
||||
@@ -117,12 +115,19 @@ def create_random_tables_content(
|
||||
)
|
||||
|
||||
|
||||
def create_extra_gm_content(rst: str, title: str, suffix: str, use_dnd_decorations: bool=False):
|
||||
"""Create content for arbitrary additional text provided in a GM sheet."""
|
||||
def create_extra_gm_content(sections: Sequence, suffix: str, use_dnd_decorations: bool=False):
|
||||
"""Create content for arbitrary additional text provided in a GM sheet.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
sections
|
||||
Subclasses of Content that will each be included as new sections
|
||||
in the output document.
|
||||
|
||||
"""
|
||||
template = jinja_env.get_template(f"extra_gm_content.{suffix}")
|
||||
full_title = title.replace("_", " ").title()
|
||||
return template.render(
|
||||
rst=rst, title=full_title, use_dnd_decorations=use_dnd_decorations
|
||||
sections=sections, use_dnd_decorations=use_dnd_decorations
|
||||
)
|
||||
|
||||
|
||||
@@ -267,16 +272,12 @@ def make_gm_sheet(
|
||||
use_dnd_decorations=fancy_decorations,
|
||||
)
|
||||
)
|
||||
# Parse any extra homebrew attributes, etc.
|
||||
gm_props.pop("dungeonsheets_version")
|
||||
gm_props.pop("sheet_type")
|
||||
extra_gm_attrs = []
|
||||
for attr, text in gm_props.items():
|
||||
if isinstance(text, str):
|
||||
extra_gm_attrs.append(attr)
|
||||
content.append(create_extra_gm_content(rst=text, title=attr, suffix=content_suffix, use_dnd_decorations=fancy_decorations))
|
||||
for attr in extra_gm_attrs:
|
||||
gm_props.pop(attr)
|
||||
# Parse any extra homebrew sections, etc.
|
||||
content.append(
|
||||
create_extra_gm_content(sections=gm_props.pop("extra_sections", []),
|
||||
suffix=content_suffix,
|
||||
use_dnd_decorations=fancy_decorations)
|
||||
)
|
||||
# Add the closing TeX
|
||||
content.append(
|
||||
jinja_env.get_template(f"postamble.{format_suffixes[output_format]}").render(
|
||||
@@ -284,6 +285,8 @@ def make_gm_sheet(
|
||||
)
|
||||
)
|
||||
# Warn about any unhandled sheet properties
|
||||
gm_props.pop("dungeonsheets_version")
|
||||
gm_props.pop("sheet_type")
|
||||
if len(gm_props.keys()) > 0:
|
||||
msg = f"Unhandled attributes in '{str(gm_file)}': {','.join(gm_props.keys())}"
|
||||
log.warning(msg)
|
||||
|
||||
@@ -6,9 +6,8 @@ shape forms.
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
from dungeonsheets.entity import Entity
|
||||
from dungeonsheets.content import Creature
|
||||
from dungeonsheets.spells import Spell
|
||||
from dungeonsheets.content_registry import find_content
|
||||
|
||||
|
||||
class SpellFactory(ABCMeta):
|
||||
@@ -19,7 +18,7 @@ class SpellFactory(ABCMeta):
|
||||
each entry on that list, anything that is not already a spell
|
||||
class (so probably a string) will be resolved into the
|
||||
corresponding ``spells.Spell`` class.
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, name, bases, attrs):
|
||||
for idx, spell in enumerate(self.spells):
|
||||
@@ -27,7 +26,7 @@ class SpellFactory(ABCMeta):
|
||||
self.spells[idx] = TheSpell
|
||||
|
||||
|
||||
class Monster(Entity, metaclass=SpellFactory):
|
||||
class Monster(Creature, metaclass=SpellFactory):
|
||||
|
||||
"""A monster that may be encountered when adventuring."""
|
||||
|
||||
|
||||
@@ -1239,7 +1239,7 @@ class SuccubusIncubus(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofBats(Monster):
|
||||
class SwarmOfBats(Monster):
|
||||
"""
|
||||
Echolocation.
|
||||
The swarm can't use its blindsight while deafened.
|
||||
@@ -1314,7 +1314,7 @@ class SwarmOfBats(Monster):
|
||||
hit_dice = "5d8"
|
||||
|
||||
|
||||
class SwarmofBeetles(Monster):
|
||||
class SwarmOfBeetles(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1347,7 +1347,7 @@ class SwarmofBeetles(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofCentipedes(Monster):
|
||||
class SwarmOfCentipedes(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1384,7 +1384,7 @@ class SwarmofCentipedes(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofInsects(Monster):
|
||||
class SwarmOfInsects(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1417,7 +1417,7 @@ class SwarmofInsects(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofPoisonousSnakes(Monster):
|
||||
class SwarmOfPoisonousSnakes(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1452,7 +1452,7 @@ class SwarmofPoisonousSnakes(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofQuippers(Monster):
|
||||
class SwarmOfQuippers(Monster):
|
||||
"""
|
||||
Blood Frenzy.
|
||||
The swarm has advantage on melee attack rolls against any creature
|
||||
@@ -1490,7 +1490,7 @@ class SwarmofQuippers(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofRats(Monster):
|
||||
class SwarmOfRats(Monster):
|
||||
"""
|
||||
Keen Smell.
|
||||
The swarm has advantage on Wisdom (Perception) checks that rely on
|
||||
@@ -1526,7 +1526,7 @@ class SwarmofRats(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofRavens(Monster):
|
||||
class SwarmOfRavens(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1559,7 +1559,7 @@ class SwarmofRavens(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofSpiders(Monster):
|
||||
class SwarmOfSpiders(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
@@ -1600,7 +1600,7 @@ class SwarmofSpiders(Monster):
|
||||
spells = []
|
||||
|
||||
|
||||
class SwarmofWasps(Monster):
|
||||
class SwarmOfWasps(Monster):
|
||||
"""
|
||||
Swarm.
|
||||
The swarm can occupy another creature's space and vice versa, and the
|
||||
|
||||
+60
-60
@@ -64,28 +64,28 @@ class Ability:
|
||||
# ability score dictionary exists but doesn't have this ability
|
||||
obj._ability_scores[self.ability_name] = self.default_value
|
||||
|
||||
def __get__(self, entity, Entity):
|
||||
self._check_dict(entity)
|
||||
score = entity._ability_scores[self.ability_name]
|
||||
def __get__(self, actor, Actor):
|
||||
self._check_dict(actor)
|
||||
score = actor._ability_scores[self.ability_name]
|
||||
modifier = math.floor((score - 10) / 2)
|
||||
# Check for proficiency
|
||||
saving_throw = modifier
|
||||
if self.ability_name is not None and hasattr(
|
||||
entity, "saving_throw_proficiencies"
|
||||
actor, "saving_throw_proficiencies"
|
||||
):
|
||||
is_proficient = self.ability_name in entity.saving_throw_proficiencies
|
||||
is_proficient = self.ability_name in actor.saving_throw_proficiencies
|
||||
if is_proficient:
|
||||
saving_throw += entity.proficiency_bonus
|
||||
saving_throw += actor.proficiency_bonus
|
||||
# Check for bonuses to saving throws from magic items
|
||||
for mitem in entity.magic_items:
|
||||
for mitem in actor.magic_items:
|
||||
saving_throw += getattr(mitem, "st_bonus", 0)
|
||||
# Create the named tuple
|
||||
value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw, name=self.ability_name)
|
||||
return value
|
||||
|
||||
def __set__(self, entity, val):
|
||||
self._check_dict(entity)
|
||||
entity._ability_scores[self.ability_name] = val
|
||||
def __set__(self, actor, val):
|
||||
self._check_dict(actor)
|
||||
actor._ability_scores[self.ability_name] = val
|
||||
self.value = val
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ class Skill:
|
||||
|
||||
ability_name = ""
|
||||
skill_name = ""
|
||||
entity = None
|
||||
actor = None
|
||||
|
||||
def __init__(self, ability):
|
||||
self.ability_name = ability
|
||||
@@ -129,8 +129,8 @@ class Skill:
|
||||
def __set_name__(self, owner, name):
|
||||
self.skill_name = name.lower().replace("_", " ")
|
||||
|
||||
def __get__(self, entity, owner):
|
||||
self.entity = entity
|
||||
def __get__(self, actor, owner):
|
||||
self.actor = actor
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
@@ -139,7 +139,7 @@ class Skill:
|
||||
@property
|
||||
def is_remarkable_athlete(self):
|
||||
already_proficient = (self.is_proficient or self.is_expertise)
|
||||
if self.entity.has_feature(RemarkableAthlete) and not already_proficient:
|
||||
if self.actor.has_feature(RemarkableAthlete) and not already_proficient:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -147,7 +147,7 @@ class Skill:
|
||||
@property
|
||||
def is_jack_of_all_trades(self):
|
||||
already_proficient = (self.is_proficient or self.is_expertise or self.is_remarkable_athlete)
|
||||
if self.entity.has_feature(JackOfAllTrades) and not already_proficient:
|
||||
if self.actor.has_feature(JackOfAllTrades) and not already_proficient:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -155,33 +155,33 @@ class Skill:
|
||||
@property
|
||||
def is_proficient(self):
|
||||
# Check for proficiency
|
||||
proficiencies = [p.replace("_", " ") for p in self.entity.skill_proficiencies]
|
||||
proficiencies = [p.replace("_", " ") for p in self.actor.skill_proficiencies]
|
||||
is_proficient = self.skill_name in proficiencies
|
||||
return is_proficient
|
||||
|
||||
@property
|
||||
def is_expertise(self):
|
||||
return self.skill_name in self.entity.skill_expertise
|
||||
return self.skill_name in self.actor.skill_expertise
|
||||
|
||||
@property
|
||||
def proficiency_modifier(self):
|
||||
modifier = 0
|
||||
if self.is_proficient:
|
||||
modifier += self.entity.proficiency_bonus
|
||||
modifier += self.actor.proficiency_bonus
|
||||
if self.is_remarkable_athlete:
|
||||
modifier += ceil(self.entity.proficiency_bonus / 2.0)
|
||||
modifier += ceil(self.actor.proficiency_bonus / 2.0)
|
||||
if self.is_jack_of_all_trades:
|
||||
modifier += self.entity.proficiency_bonus // 2
|
||||
modifier += self.actor.proficiency_bonus // 2
|
||||
# Check for expertise
|
||||
if self.is_expertise:
|
||||
modifier += self.entity.proficiency_bonus
|
||||
modifier += self.actor.proficiency_bonus
|
||||
return modifier
|
||||
|
||||
@property
|
||||
def modifier(self):
|
||||
ability = getattr(self.entity, self.ability_name)
|
||||
ability = getattr(self.actor, self.ability_name)
|
||||
modifier = ability.modifier + self.proficiency_modifier
|
||||
log.info("%s modifier for '%s': %d", self, self.entity.name, modifier)
|
||||
log.info("%s modifier for '%s': %d", self, self.actor.name, modifier)
|
||||
return modifier
|
||||
|
||||
|
||||
@@ -190,36 +190,36 @@ class ArmorClass:
|
||||
The Armor Class of a character
|
||||
"""
|
||||
|
||||
def __get__(self, entity, Entity):
|
||||
armor = entity.armor or NoArmor()
|
||||
def __get__(self, actor, Actor):
|
||||
armor = actor.armor or NoArmor()
|
||||
ac = armor.base_armor_class
|
||||
# calculate and apply modifiers
|
||||
if armor.dexterity_mod_max is None:
|
||||
ac += entity.dexterity.modifier
|
||||
ac += actor.dexterity.modifier
|
||||
else:
|
||||
ac += min(entity.dexterity.modifier, armor.dexterity_mod_max)
|
||||
if entity.has_feature(NaturalArmor):
|
||||
ac = max(ac, 13 + entity.dexterity.modifier)
|
||||
shield = entity.shield or NoShield()
|
||||
ac += min(actor.dexterity.modifier, armor.dexterity_mod_max)
|
||||
if actor.has_feature(NaturalArmor):
|
||||
ac = max(ac, 13 + actor.dexterity.modifier)
|
||||
shield = actor.shield or NoShield()
|
||||
ac += shield.base_armor_class
|
||||
# Compute feature-specific additions
|
||||
if entity.has_feature(UnarmoredDefenseMonk):
|
||||
if actor.has_feature(UnarmoredDefenseMonk):
|
||||
if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
|
||||
ac += entity.wisdom.modifier
|
||||
if entity.has_feature(UnarmoredDefenseBarbarian):
|
||||
ac += actor.wisdom.modifier
|
||||
if actor.has_feature(UnarmoredDefenseBarbarian):
|
||||
if isinstance(armor, NoArmor):
|
||||
ac += entity.constitution.modifier
|
||||
if entity.has_feature(DraconicResilience):
|
||||
ac += actor.constitution.modifier
|
||||
if actor.has_feature(DraconicResilience):
|
||||
if isinstance(armor, NoArmor):
|
||||
ac += 3
|
||||
if entity.has_feature(Defense):
|
||||
if actor.has_feature(Defense):
|
||||
if not isinstance(armor, NoArmor):
|
||||
ac += 1
|
||||
if entity.has_feature(SoulOfTheForge):
|
||||
if actor.has_feature(SoulOfTheForge):
|
||||
if isinstance(armor, HeavyArmor):
|
||||
ac += 1
|
||||
# Check if any magic items add to AC
|
||||
for mitem in entity.magic_items:
|
||||
for mitem in actor.magic_items:
|
||||
if hasattr(mitem, "ac_bonus"):
|
||||
ac += mitem.ac_bonus
|
||||
return ac
|
||||
@@ -230,25 +230,25 @@ class Speed:
|
||||
The speed of a character
|
||||
"""
|
||||
|
||||
def __get__(self, entity, Entity):
|
||||
speed = entity.race.speed
|
||||
def __get__(self, actor, Actor):
|
||||
speed = actor.race.speed
|
||||
other_speed = ""
|
||||
if isinstance(speed, str):
|
||||
other_speed = speed[2:]
|
||||
speed = int(speed[:2]) # ignore other speeds, like fly
|
||||
if entity.has_feature(FastMovement):
|
||||
if not isinstance(entity.armor, HeavyArmor):
|
||||
if actor.has_feature(FastMovement):
|
||||
if not isinstance(actor.armor, HeavyArmor):
|
||||
speed += 10
|
||||
if entity.has_feature(SuperiorMobility):
|
||||
if actor.has_feature(SuperiorMobility):
|
||||
speed += 10
|
||||
if isinstance(entity.armor, NoArmor) or (entity.armor is None):
|
||||
for f in entity.features:
|
||||
if isinstance(actor.armor, NoArmor) or (actor.armor is None):
|
||||
for f in actor.features:
|
||||
if isinstance(f, UnarmoredMovement):
|
||||
speed += f.speed_bonus
|
||||
if entity.has_feature(GiftOfTheDepths):
|
||||
if actor.has_feature(GiftOfTheDepths):
|
||||
if "swim" not in other_speed:
|
||||
other_speed += " ({:d} swim)".format(speed)
|
||||
if entity.has_feature(SeaSoul):
|
||||
if actor.has_feature(SeaSoul):
|
||||
if "swim" not in other_speed:
|
||||
other_speed += " (30 swim)"
|
||||
return "{:d}{:s}".format(speed, other_speed)
|
||||
@@ -257,19 +257,19 @@ class Speed:
|
||||
class NumericalInitiative:
|
||||
"""A numerical representation of initiative"""
|
||||
|
||||
def __get__(self, entity, Entity):
|
||||
ini = entity.dexterity.modifier
|
||||
if entity.has_feature(QuickDraw):
|
||||
ini += entity.proficiency_bonus
|
||||
if entity.has_feature(DreadAmbusher):
|
||||
ini += entity.wisdom.modifier
|
||||
if entity.has_feature(RakishAudacity):
|
||||
ini += entity.charisma.modifier
|
||||
def __get__(self, actor, Actor):
|
||||
ini = actor.dexterity.modifier
|
||||
if actor.has_feature(QuickDraw):
|
||||
ini += actor.proficiency_bonus
|
||||
if actor.has_feature(DreadAmbusher):
|
||||
ini += actor.wisdom.modifier
|
||||
if actor.has_feature(RakishAudacity):
|
||||
ini += actor.charisma.modifier
|
||||
|
||||
has_advantage = (
|
||||
entity.has_feature(NaturalExplorerRevised)
|
||||
or entity.has_feature(FeralInstinct)
|
||||
or entity.has_feature(AmbushMaster)
|
||||
actor.has_feature(NaturalExplorerRevised)
|
||||
or actor.has_feature(FeralInstinct)
|
||||
or actor.has_feature(AmbushMaster)
|
||||
)
|
||||
return ini, has_advantage
|
||||
|
||||
@@ -277,8 +277,8 @@ class NumericalInitiative:
|
||||
class Initiative(NumericalInitiative):
|
||||
"""A character's initiative"""
|
||||
|
||||
def __get__(self, entity, Entity):
|
||||
ini, has_advantage = super(Initiative, self).__get__(entity, Entity)
|
||||
def __get__(self, actor, Actor):
|
||||
ini, has_advantage = super(Initiative, self).__get__(actor, Actor)
|
||||
ini = "{:+d}".format(ini)
|
||||
if has_advantage:
|
||||
ini += "(A)"
|
||||
|
||||
@@ -29,16 +29,27 @@ parent_sheets = ["gm-campaign-notes.py"]
|
||||
# the output
|
||||
monsters = ["aboleth", "wolf", "giant eagle", "Vashta Nerada", "priest"]
|
||||
|
||||
# Arbitrary sections can be added to the GM notes. Any attribute that
|
||||
# is a string and whose name doesn't start with an underscore ("_")
|
||||
# will be included as a separate section
|
||||
BBEG_motivation = (
|
||||
"""Hans Gruber is after the $640 in bearer bonds stored in *Nakatomi
|
||||
plaza*."""
|
||||
)
|
||||
# Arbitrary sections can be added to the GM notes. The
|
||||
# ``extra_sections`` attribute should be a sequence of subclasses of
|
||||
# the *Content* base class. For each entry in the sequence, the *name*
|
||||
# attribute will be used for the section title, and the docstring will
|
||||
# make up the body
|
||||
|
||||
the_bar_fight = (
|
||||
"If the characters decide to go to the *Alliance Friendly Bar*, "
|
||||
"they will probably have to fight their way out against 5 enemies "
|
||||
"(3 Veteran, 2 Soldier)."
|
||||
)
|
||||
|
||||
class BBEGMotivation():
|
||||
"""Hans Gruber is after the $640 in bearer bonds stored in *Nakatomi
|
||||
plaza*.
|
||||
|
||||
"""
|
||||
name = "Big-Bad-Evil-Guy Motivation"
|
||||
|
||||
class BarFight():
|
||||
"""If the characters decide to go to the *Alliance Friendly Bar*,
|
||||
they will probably have to fight their way out against 5 enemies
|
||||
(3 Veteran, 2 Soldier).
|
||||
|
||||
"""
|
||||
name = "The Bar Fight"
|
||||
|
||||
|
||||
extra_sections = [BBEGMotivation, BarFight]
|
||||
|
||||
@@ -22,7 +22,7 @@ class MakeSheetsTestCase(unittest.TestCase):
|
||||
|
||||
def test_main(self):
|
||||
make_sheets.main(args=[str(CHARFILE), "--debug"])
|
||||
|
||||
|
||||
def test_make_sheets(self):
|
||||
# Character PDF
|
||||
make_sheets.make_sheet(sheet_file=CHARFILE)
|
||||
@@ -167,10 +167,17 @@ class HtmlCreatorTestCase(unittest.TestCase):
|
||||
self.assertIn(r"Wish", html)
|
||||
# Check fancy extended properties
|
||||
html = make_sheets.create_monsters_content(monsters=monsters_,
|
||||
suffix="html",
|
||||
use_dnd_decorations=True)
|
||||
|
||||
|
||||
suffix="html",
|
||||
use_dnd_decorations=True)
|
||||
|
||||
def test_create_extra_gm_content(self):
|
||||
class MySection():
|
||||
name = "My D&D Homebrew Content"
|
||||
|
||||
html = make_sheets.create_extra_gm_content(sections=[MySection], suffix="html")
|
||||
self.assertIn('<h1 id="extra-My-DD-Homebrew-Content">', html)
|
||||
tex = make_sheets.create_extra_gm_content(sections=[MySection], suffix="tex")
|
||||
self.assertIn(r'\section*{My D&D Homebrew Content', tex)
|
||||
|
||||
|
||||
class TexCreatorTestCase(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user