mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-19 04:33:26 +02:00
Fixed broken test and added ability to put session summary into GM notes.
This commit is contained in:
@@ -7,7 +7,7 @@ __all__ = (
|
|||||||
"race",
|
"race",
|
||||||
"background",
|
"background",
|
||||||
"spells",
|
"spells",
|
||||||
"import_homebrew"
|
"import_homebrew",
|
||||||
)
|
)
|
||||||
|
|
||||||
from dungeonsheets import background, features, race, spells, weapons, mechanics
|
from dungeonsheets import background, features, race, spells, weapons, mechanics
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ class RivalIntern(Background):
|
|||||||
the knowledge you gained there for an advantage at Acquisitions
|
the knowledge you gained there for an advantage at Acquisitions
|
||||||
Incorporated. Either way, you're now bringing your talents to the
|
Incorporated. Either way, you're now bringing your talents to the
|
||||||
company, ready to put your skills lo use.
|
company, ready to put your skills lo use.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Rival Intern"
|
name = "Rival Intern"
|
||||||
skill_proficiencies = ("history", "investigation")
|
skill_proficiencies = ("history", "investigation")
|
||||||
proficiencies_text = ("One type of artisan's tools",)
|
proficiencies_text = ("One type of artisan's tools",)
|
||||||
|
|||||||
+25
-16
@@ -77,13 +77,13 @@ def _resolve_mechanic(mechanic, SuperClass, warning_message=None):
|
|||||||
used by a character::
|
used by a character::
|
||||||
|
|
||||||
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
>>> _resolve_mechanic("mage_hand", SuperClass=spells.Spell)
|
||||||
|
|
||||||
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
>>> _resolve_mechanic("mage_hand", SuperClass=None)
|
||||||
|
|
||||||
>>> from dungeonsheets import spells
|
>>> from dungeonsheets import spells
|
||||||
>>> class MySpell(spells.Spell): pass
|
>>> class MySpell(spells.Spell): pass
|
||||||
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
>>> _resolve_mechanic(MySpell, SuperClass=spells.Spell)
|
||||||
|
|
||||||
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
>>> _resolve_mechanic("hocus pocus", SuperClass=spells.Spell)
|
||||||
|
|
||||||
The acceptable entries for *mechanic*, in priority order, are:
|
The acceptable entries for *mechanic*, in priority order, are:
|
||||||
@@ -175,18 +175,18 @@ class Character(Entity):
|
|||||||
# Appearance
|
# Appearance
|
||||||
# portrait = placeholder not sure how to implement
|
# portrait = placeholder not sure how to implement
|
||||||
age = 0
|
age = 0
|
||||||
height = ''
|
height = ""
|
||||||
weight = ''
|
weight = ""
|
||||||
eyes = ''
|
eyes = ""
|
||||||
skin = ''
|
skin = ""
|
||||||
hair = ''
|
hair = ""
|
||||||
# Background
|
# Background
|
||||||
allies = ''
|
allies = ""
|
||||||
faction_name = ''
|
faction_name = ""
|
||||||
# faction_symbol = placeholder not sure how to implement
|
# faction_symbol = placeholder not sure how to implement
|
||||||
backstory = ''
|
backstory = ""
|
||||||
other_feats_traits = ''
|
other_feats_traits = ""
|
||||||
treasure = ''
|
treasure = ""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -344,7 +344,9 @@ class Character(Entity):
|
|||||||
self._race = newrace(owner=self)
|
self._race = newrace(owner=self)
|
||||||
elif isinstance(newrace, str):
|
elif isinstance(newrace, str):
|
||||||
try:
|
try:
|
||||||
self._race = find_content(newrace, valid_classes=[race.Race])(owner=self)
|
self._race = find_content(newrace, valid_classes=[race.Race])(
|
||||||
|
owner=self
|
||||||
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = f'Race "{newrace}" not defined. Please add it to ``race.py``'
|
msg = f'Race "{newrace}" not defined. Please add it to ``race.py``'
|
||||||
self._race = race.Race(owner=self)
|
self._race = race.Race(owner=self)
|
||||||
@@ -365,7 +367,9 @@ class Character(Entity):
|
|||||||
self._background = bg(owner=self)
|
self._background = bg(owner=self)
|
||||||
elif isinstance(bg, str):
|
elif isinstance(bg, str):
|
||||||
try:
|
try:
|
||||||
self._background = find_content(bg, valid_classes=[background.Background])(owner=self)
|
self._background = find_content(
|
||||||
|
bg, valid_classes=[background.Background]
|
||||||
|
)(owner=self)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = (
|
msg = (
|
||||||
f'Background "{bg}" not defined. Please add it to ``background.py``'
|
f'Background "{bg}" not defined. Please add it to ``background.py``'
|
||||||
@@ -634,7 +638,12 @@ class Character(Entity):
|
|||||||
self.other_weapon_proficiencies = ()
|
self.other_weapon_proficiencies = ()
|
||||||
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
|
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
|
||||||
wps = set(
|
wps = set(
|
||||||
[_resolve_mechanic(w, SuperClass=weapons.Weapon, warning_message=msg) for w in val]
|
[
|
||||||
|
_resolve_mechanic(
|
||||||
|
w, SuperClass=weapons.Weapon, warning_message=msg
|
||||||
|
)
|
||||||
|
for w in val
|
||||||
|
]
|
||||||
)
|
)
|
||||||
wps -= set(self.weapon_proficiencies)
|
wps -= set(self.weapon_proficiencies)
|
||||||
self.other_weapon_proficiencies = list(wps)
|
self.other_weapon_proficiencies = list(wps)
|
||||||
|
|||||||
@@ -3,29 +3,39 @@ from functools import lru_cache
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
from typing import Union, List, Optional
|
from typing import Union, List, Optional
|
||||||
|
|
||||||
from dungeonsheets import weapons, monsters, race, background, armor, spells, infusions, magic_items, features
|
from dungeonsheets import (
|
||||||
|
weapons,
|
||||||
|
monsters,
|
||||||
|
race,
|
||||||
|
background,
|
||||||
|
armor,
|
||||||
|
spells,
|
||||||
|
infusions,
|
||||||
|
magic_items,
|
||||||
|
features,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentRegistry():
|
class ContentRegistry:
|
||||||
modules = None
|
modules = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.modules = []
|
self.modules = []
|
||||||
|
|
||||||
def add_module(self, new_module):
|
def add_module(self, new_module):
|
||||||
if new_module not in self.modules:
|
if new_module not in self.modules:
|
||||||
self.modules.append(new_module)
|
self.modules.append(new_module)
|
||||||
|
|
||||||
def findattr(self, name, valid_classes=[]):
|
def findattr(self, name, valid_classes=[]):
|
||||||
"""Resolve the name of a piece of content to the corresponding Class.
|
"""Resolve the name of a piece of content to the corresponding Class.
|
||||||
|
|
||||||
Similar to builtin getattr(obj, name) but more forgiving to
|
Similar to builtin getattr(obj, name) but more forgiving to
|
||||||
whitespace and capitalization.
|
whitespace and capitalization.
|
||||||
|
|
||||||
valid_classes
|
valid_classes
|
||||||
If given, only subclasses of classes in this list will be
|
If given, only subclasses of classes in this list will be
|
||||||
returned.
|
returned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Come up with several options
|
# Come up with several options
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
@@ -36,7 +46,9 @@ class ContentRegistry():
|
|||||||
bonus = i
|
bonus = i
|
||||||
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
|
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
|
||||||
break
|
break
|
||||||
py_name = name.replace("-", "_").replace(" ", "_").replace("'", "").replace("/", "")
|
py_name = (
|
||||||
|
name.replace("-", "_").replace(" ", "_").replace("'", "").replace("/", "")
|
||||||
|
)
|
||||||
camel_case = "".join([s.capitalize() for s in py_name.split("_")])
|
camel_case = "".join([s.capitalize() for s in py_name.split("_")])
|
||||||
# Check each module in the registry
|
# Check each module in the registry
|
||||||
found_attrs = []
|
found_attrs = []
|
||||||
@@ -51,7 +63,12 @@ class ContentRegistry():
|
|||||||
if len(valid_classes) > 0:
|
if len(valid_classes) > 0:
|
||||||
is_valid = [False for attr in found_attrs]
|
is_valid = [False for attr in found_attrs]
|
||||||
for cls in valid_classes:
|
for cls in valid_classes:
|
||||||
is_valid = [v or isinstance(attr, cls) or (isinstance(attr, type) and issubclass(attr, cls)) for v, attr in zip(is_valid, found_attrs)]
|
is_valid = [
|
||||||
|
v
|
||||||
|
or isinstance(attr, cls)
|
||||||
|
or (isinstance(attr, type) and issubclass(attr, cls))
|
||||||
|
for v, attr in zip(is_valid, found_attrs)
|
||||||
|
]
|
||||||
found_attrs = [attr for attr, v in zip(found_attrs, is_valid) if v]
|
found_attrs = [attr for attr, v in zip(found_attrs, is_valid) if v]
|
||||||
# Check that we found a valid, unique attribute
|
# Check that we found a valid, unique attribute
|
||||||
if len(found_attrs) == 0:
|
if len(found_attrs) == 0:
|
||||||
@@ -94,7 +111,7 @@ def find_content(name: str, valid_classes: Optional[List]):
|
|||||||
valid_classes
|
valid_classes
|
||||||
A list of parent classes to look for. If ``None`` or ``[]``, all
|
A list of parent classes to look for. If ``None`` or ``[]``, all
|
||||||
classes will be considered valid.
|
classes will be considered valid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if valid_classes is None:
|
if valid_classes is None:
|
||||||
valid_classes = []
|
valid_classes = []
|
||||||
@@ -112,7 +129,7 @@ def import_homebrew(filepath: Union[str, Path]):
|
|||||||
==========
|
==========
|
||||||
filepath
|
filepath
|
||||||
The location of the python file containing the homebrew content.
|
The location of the python file containing the homebrew content.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
=======
|
=======
|
||||||
mod
|
mod
|
||||||
|
|||||||
@@ -342,8 +342,9 @@ class InsideInformant(Feature):
|
|||||||
"""You have connections to your previous employer or other groups you
|
"""You have connections to your previous employer or other groups you
|
||||||
dealt with during your previous employment. You can communicate
|
dealt with during your previous employment. You can communicate
|
||||||
with your contacts, gaining information at the DM's discretion.
|
with your contacts, gaining information at the DM's discretion.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Inside Informant"
|
name = "Inside Informant"
|
||||||
source = "Background (Rival Intern)"
|
source = "Background (Rival Intern)"
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,9 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
# Hit points
|
# Hit points
|
||||||
"HDTotal": character.hit_dice,
|
"HDTotal": character.hit_dice,
|
||||||
"HPMax": str(character.hp_max),
|
"HPMax": str(character.hp_max),
|
||||||
"HPCurrent": str(character.hp_current) if character.hp_current is not None else "",
|
"HPCurrent": str(character.hp_current)
|
||||||
|
if character.hp_current is not None
|
||||||
|
else "",
|
||||||
"HPTemp": str(character.hp_temp) if character.hp_temp > 0 else "",
|
"HPTemp": str(character.hp_temp) if character.hp_temp > 0 else "",
|
||||||
# Personality traits and other features
|
# Personality traits and other features
|
||||||
"PersonalityTraits ": text_box(character.personality_traits),
|
"PersonalityTraits ": text_box(character.personality_traits),
|
||||||
@@ -148,7 +150,7 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
|
("Wpn Name 2", "Wpn2 AtkBonus ", "Wpn2 Damage "),
|
||||||
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
|
("Wpn Name 3", "Wpn3 AtkBonus ", "Wpn3 Damage "),
|
||||||
]
|
]
|
||||||
if len(character.weapons) == 0 or hasattr(character, 'Monk'):
|
if len(character.weapons) == 0 or hasattr(character, "Monk"):
|
||||||
character.wield_weapon("unarmed")
|
character.wield_weapon("unarmed")
|
||||||
for _fields, weapon in zip(weapon_fields, character.weapons):
|
for _fields, weapon in zip(weapon_fields, character.weapons):
|
||||||
name_field, atk_field, dmg_field = _fields
|
name_field, atk_field, dmg_field = _fields
|
||||||
@@ -156,8 +158,10 @@ def create_character_pdf_template(character, basename, flatten=False):
|
|||||||
fields[atk_field] = "{:+d}".format(weapon.attack_modifier)
|
fields[atk_field] = "{:+d}".format(weapon.attack_modifier)
|
||||||
fields[dmg_field] = f"{weapon.damage}/{weapon.damage_type}"
|
fields[dmg_field] = f"{weapon.damage}/{weapon.damage_type}"
|
||||||
# Additional attacks beyond 3
|
# Additional attacks beyond 3
|
||||||
attack = [f"{w.name}: Atk {w.attack_modifier:+d}, Dam {w.damage}/{w.damage_type}"
|
attack = [
|
||||||
for w in character.weapons[len(weapon_fields):]]
|
f"{w.name}: Atk {w.attack_modifier:+d}, Dam {w.damage}/{w.damage_type}"
|
||||||
|
for w in character.weapons[len(weapon_fields):]
|
||||||
|
]
|
||||||
# Other attack information
|
# Other attack information
|
||||||
if character.armor:
|
if character.armor:
|
||||||
attack.append(f"Armor: {character.armor}")
|
attack.append(f"Armor: {character.armor}")
|
||||||
@@ -192,7 +196,7 @@ def create_personality_pdf_template(character, basename, flatten=False):
|
|||||||
"FactionName": character.faction_name,
|
"FactionName": character.faction_name,
|
||||||
"Backstory": text_box(character.backstory),
|
"Backstory": text_box(character.backstory),
|
||||||
"Feat+Traits": text_box(character.other_feats_traits),
|
"Feat+Traits": text_box(character.other_feats_traits),
|
||||||
"Treasure": text_box(character.treasure)
|
"Treasure": text_box(character.treasure),
|
||||||
}
|
}
|
||||||
# Prepare the actual PDF
|
# Prepare the actual PDF
|
||||||
dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/")
|
dirname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "forms/")
|
||||||
@@ -464,9 +468,11 @@ def create_spells_pdf_template(character, basename, flatten=False):
|
|||||||
# Determine if we should omit un-prepared spells to save space
|
# Determine if we should omit un-prepared spells to save space
|
||||||
if len(spells) > len(field_numbers[level]):
|
if len(spells) > len(field_numbers[level]):
|
||||||
spells = [s for s in spells if s in character.spells_prepared]
|
spells = [s for s in spells if s in character.spells_prepared]
|
||||||
warnings.warn(f"{character.name} knows more spells than the number of "
|
warnings.warn(
|
||||||
"lines available in spell sheet. Limited to prepared "
|
f"{character.name} knows more spells than the number of "
|
||||||
"spells only.")
|
"lines available in spell sheet. Limited to prepared "
|
||||||
|
"spells only."
|
||||||
|
)
|
||||||
# Build the list of PDF controls to set/toggle
|
# Build the list of PDF controls to set/toggle
|
||||||
field_names = [f"Spells {i}" for i in field_numbers[level]]
|
field_names = [f"Spells {i}" for i in field_numbers[level]]
|
||||||
prep_names = tuple(f"Check Box {i}" for i in prep_numbers[level])
|
prep_names = tuple(f"Check Box {i}" for i in prep_numbers[level])
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
[% if summary %]
|
||||||
|
|
||||||
|
\section*{Summary}
|
||||||
|
|
||||||
|
[[ summary | rst_to_latex ]]
|
||||||
|
|
||||||
|
[% endif %]
|
||||||
|
|
||||||
|
[% if party %]
|
||||||
|
|
||||||
\section*{Party}
|
\section*{Party}
|
||||||
|
|
||||||
[% if use_dnd_decorations %]
|
[% if use_dnd_decorations %]
|
||||||
@@ -55,3 +65,5 @@
|
|||||||
[% endfor %]
|
[% endfor %]
|
||||||
\end{tabular}
|
\end{tabular}
|
||||||
[% endif %]
|
[% endif %]
|
||||||
|
|
||||||
|
[% endif %]
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
"""Tools useful for defining homebrew content."""
|
"""Tools useful for defining homebrew content."""
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,15 @@ def create_monsters_tex(
|
|||||||
|
|
||||||
|
|
||||||
def create_party_summary_tex(
|
def create_party_summary_tex(
|
||||||
party: Sequence[Entity],
|
party: Sequence[Entity],
|
||||||
use_dnd_decorations: bool = False,
|
summary_rst: str,
|
||||||
|
use_dnd_decorations: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
log.debug("Preparing summary table for party: %s", party)
|
||||||
template = jinja_env.get_template("party_summary_template.tex")
|
template = jinja_env.get_template("party_summary_template.tex")
|
||||||
return template.render(party=party, use_dnd_decorations=use_dnd_decorations)
|
return template.render(
|
||||||
|
party=party, summary=summary_rst, use_dnd_decorations=use_dnd_decorations
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_spellbook_tex(
|
def create_spellbook_tex(
|
||||||
@@ -196,7 +200,7 @@ def make_gm_sheet(
|
|||||||
title=gm_props["session_title"],
|
title=gm_props["session_title"],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
# Add the party stats table
|
# Add the party stats table and session summary
|
||||||
party = []
|
party = []
|
||||||
for char_file in gm_props.get("party", []):
|
for char_file in gm_props.get("party", []):
|
||||||
# Resolve the file path
|
# Resolve the file path
|
||||||
@@ -208,10 +212,12 @@ def make_gm_sheet(
|
|||||||
character_props = readers.read_sheet_file(char_file)
|
character_props = readers.read_sheet_file(char_file)
|
||||||
member = _char.Character.load(character_props)
|
member = _char.Character.load(character_props)
|
||||||
party.append(member)
|
party.append(member)
|
||||||
if len(party) > 0:
|
summary = gm_props.get("summary", "")
|
||||||
tex.append(
|
tex.append(
|
||||||
create_party_summary_tex(party, use_dnd_decorations=fancy_decorations)
|
create_party_summary_tex(
|
||||||
|
party, summary_rst=summary, use_dnd_decorations=fancy_decorations
|
||||||
)
|
)
|
||||||
|
)
|
||||||
# Add the monsters
|
# Add the monsters
|
||||||
monsters_ = []
|
monsters_ = []
|
||||||
for monster in gm_props.get("monsters", []):
|
for monster in gm_props.get("monsters", []):
|
||||||
@@ -222,10 +228,7 @@ def make_gm_sheet(
|
|||||||
try:
|
try:
|
||||||
MyMonster = find_content(monster, valid_classes=[monsters.Monster])
|
MyMonster = find_content(monster, valid_classes=[monsters.Monster])
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
msg = (
|
msg = f"Monster '{monster}' not found. Please add it to ``monsters.py``"
|
||||||
f"Monster '{monster}' not found. Please add it to"
|
|
||||||
" ``monsters.py``"
|
|
||||||
)
|
|
||||||
warnings.warn(msg)
|
warnings.warn(msg)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Monster(Entity):
|
|||||||
saving_throws = ""
|
saving_throws = ""
|
||||||
# TODO: Consider refactoring stats.Speed to consider all of these
|
# TODO: Consider refactoring stats.Speed to consider all of these
|
||||||
# just like we do stats.Ability
|
# just like we do stats.Ability
|
||||||
|
|
||||||
swim_speed = 0
|
swim_speed = 0
|
||||||
fly_speed = 0
|
fly_speed = 0
|
||||||
climb_speed = 0
|
climb_speed = 0
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Aboleth(Monster):
|
|||||||
charmed target is under the aboleth's control and can't take
|
charmed target is under the aboleth's control and can't take
|
||||||
reactions, and the aboleth and the target can communicate
|
reactions, and the aboleth and the target can communicate
|
||||||
telepathically with each other over any distance.
|
telepathically with each other over any distance.
|
||||||
|
|
||||||
Whenever the charmed target takes damage, the target can repeat
|
Whenever the charmed target takes damage, the target can repeat
|
||||||
the saving throw. On a success, the effect ends. No more than
|
the saving throw. On a success, the effect ends. No more than
|
||||||
once every 24 hours, the target can also repeat the saving throw
|
once every 24 hours, the target can also repeat the saving throw
|
||||||
@@ -79,7 +79,7 @@ class Aboleth(Monster):
|
|||||||
|
|
||||||
class Acolyte(Monster):
|
class Acolyte(Monster):
|
||||||
"""Spellcasting
|
"""Spellcasting
|
||||||
|
|
||||||
The acolyte is a 1st-level spellcaster. Its spellcasting ability
|
The acolyte is a 1st-level spellcaster. Its spellcasting ability
|
||||||
is Wisdom (spell save DC 12, +4 to hit with spell attacks). The
|
is Wisdom (spell save DC 12, +4 to hit with spell attacks). The
|
||||||
acolyte has following cleric spells prepared:
|
acolyte has following cleric spells prepared:
|
||||||
@@ -92,6 +92,7 @@ class Acolyte(Monster):
|
|||||||
(1d4) bludgeoning damage.
|
(1d4) bludgeoning damage.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Acolyte"
|
name = "Acolyte"
|
||||||
description = "Medium humanoid, any alignment"
|
description = "Medium humanoid, any alignment"
|
||||||
challenge_rating = 0.25
|
challenge_rating = 0.25
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ class BlackEarthGuard(Monster):
|
|||||||
burrowsharks.
|
burrowsharks.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Black Earth Guard"
|
name = "Black Earth Guard"
|
||||||
description = "Medium humanoid (human), neutral evil"
|
description = "Medium humanoid (human), neutral evil"
|
||||||
challenge_rating = 2
|
challenge_rating = 2
|
||||||
@@ -507,6 +508,7 @@ class BlackEarthPriest(Monster):
|
|||||||
over the rest of Ogrémoch's followers.
|
over the rest of Ogrémoch's followers.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Black Earth Priest"
|
name = "Black Earth Priest"
|
||||||
description = "Medium humanoid (human), neutral evil"
|
description = "Medium humanoid (human), neutral evil"
|
||||||
challenge_rating = 3
|
challenge_rating = 3
|
||||||
@@ -551,8 +553,9 @@ class BlackPudding(Monster):
|
|||||||
dissolved and takes a permanent and cumulative -1 penalty to the
|
dissolved and takes a permanent and cumulative -1 penalty to the
|
||||||
AC it offers. The armor is destroyed if the penalty reduces its
|
AC it offers. The armor is destroyed if the penalty reduces its
|
||||||
AC to 10.
|
AC to 10.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Black Pudding"
|
name = "Black Pudding"
|
||||||
description = "Large ooze, unaligned"
|
description = "Large ooze, unaligned"
|
||||||
challenge_rating = 4
|
challenge_rating = 4
|
||||||
@@ -885,6 +888,7 @@ class Burrowshark(Monster):
|
|||||||
neither moved nor knocked prone.
|
neither moved nor knocked prone.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Burrowshark"
|
name = "Burrowshark"
|
||||||
description = "Medium humanoid (human), neutral evil"
|
description = "Medium humanoid (human), neutral evil"
|
||||||
challenge_rating = 4
|
challenge_rating = 4
|
||||||
@@ -927,6 +931,7 @@ class Bulette(Monster):
|
|||||||
prone in the bulette's space.
|
prone in the bulette's space.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Bulette"
|
name = "Bulette"
|
||||||
description = "Large monstrosity, unaligned"
|
description = "Large monstrosity, unaligned"
|
||||||
challenge_rating = 5
|
challenge_rating = 5
|
||||||
@@ -944,5 +949,6 @@ class Bulette(Monster):
|
|||||||
swim_speed = 0
|
swim_speed = 0
|
||||||
fly_speed = 0
|
fly_speed = 0
|
||||||
climb_speed = 0
|
climb_speed = 0
|
||||||
|
burrow_speed = 40
|
||||||
hp_max = 94
|
hp_max = 94
|
||||||
hit_dice = "9d10+45"
|
hit_dice = "9d10+45"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Dao(Monster):
|
|||||||
The dao's innate spellcasting ability is Charisma (spell save DC
|
The dao's innate spellcasting ability is Charisma (spell save DC
|
||||||
14, +6 to hit with spell attacks). It can innately cast the
|
14, +6 to hit with spell attacks). It can innately cast the
|
||||||
following spells, requiring no material components:
|
following spells, requiring no material components:
|
||||||
|
|
||||||
At will: detect evil and good, detect magic, stone shape. 3/day
|
At will: detect evil and good, detect magic, stone shape. 3/day
|
||||||
each: passwall, move earth, tongues. 1/day each: conjure
|
each: passwall, move earth, tongues. 1/day each: conjure
|
||||||
elemental (earth elemental only), gaseous form, invisibility,
|
elemental (earth elemental only), gaseous form, invisibility,
|
||||||
@@ -38,8 +38,9 @@ class Dao(Monster):
|
|||||||
target. *Hit:* 20 (4d6 + 6) bludgeoning damage. If the target is
|
target. *Hit:* 20 (4d6 + 6) bludgeoning damage. If the target is
|
||||||
a Huge or smaller creature, it must succeed on a DC 18 Strength
|
a Huge or smaller creature, it must succeed on a DC 18 Strength
|
||||||
check or be knocked prone.
|
check or be knocked prone.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Dao"
|
name = "Dao"
|
||||||
description = "Large elemental, neutral evil"
|
description = "Large elemental, neutral evil"
|
||||||
challenge_rating = 11
|
challenge_rating = 11
|
||||||
@@ -64,7 +65,6 @@ class Dao(Monster):
|
|||||||
hit_dice = "15d10 + 105"
|
hit_dice = "15d10 + 105"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Darkmantle(Monster):
|
class Darkmantle(Monster):
|
||||||
"""
|
"""
|
||||||
**Echolocation**: The darkmantle can't use its blindsight while deafened.
|
**Echolocation**: The darkmantle can't use its blindsight while deafened.
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ class EarthElemental(Monster):
|
|||||||
Slam.
|
Slam.
|
||||||
Melee Weapon Attack: +8 to hit, reach 10 ft., one target. Hit:
|
Melee Weapon Attack: +8 to hit, reach 10 ft., one target. Hit:
|
||||||
14 (2d8 + 5) bludgeoning damage.
|
14 (2d8 + 5) bludgeoning damage.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Earth Elemental"
|
name = "Earth Elemental"
|
||||||
description = "Large elemental, neutral"
|
description = "Large elemental, neutral"
|
||||||
challenge_rating = 5
|
challenge_rating = 5
|
||||||
@@ -92,6 +93,7 @@ class EarthElementalMyrmidon(Monster):
|
|||||||
creators.
|
creators.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Earth Elemental Myrmidon"
|
name = "Earth Elemental Myrmidon"
|
||||||
description = "Medium elemental, neutral"
|
description = "Medium elemental, neutral"
|
||||||
challenge_rating = 7
|
challenge_rating = 7
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ class ShadowDemon(Monster):
|
|||||||
creature. *Hit:* 10 (2d6+3) psychic damage or, if the demon
|
creature. *Hit:* 10 (2d6+3) psychic damage or, if the demon
|
||||||
had advantage on the attack roll, 17 (4d6+3) psychic damage.
|
had advantage on the attack roll, 17 (4d6+3) psychic damage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Shadow Demon"
|
name = "Shadow Demon"
|
||||||
description = "Medium fiend (demon), chaotic evil"
|
description = "Medium fiend (demon), chaotic evil"
|
||||||
challenge_rating = 4
|
challenge_rating = 4
|
||||||
@@ -323,9 +324,14 @@ class ShadowDemon(Monster):
|
|||||||
skills = "Stealth +7"
|
skills = "Stealth +7"
|
||||||
saving_throws = "Dex +5, Cha +4"
|
saving_throws = "Dex +5, Cha +4"
|
||||||
damage_vulnerabilities = "radiant"
|
damage_vulnerabilities = "radiant"
|
||||||
damage_resistances = "acid, fire, necrotic, thunder; bludgeoning, piercing, and slashing from nonmagical attacks"
|
damage_resistances = (
|
||||||
|
"acid, fire, necrotic, thunder; bludgeoning, piercing, and slashing from"
|
||||||
|
" nonmagical attacks"
|
||||||
|
)
|
||||||
damage_immunities = "cold, lightning, poison"
|
damage_immunities = "cold, lightning, poison"
|
||||||
condition_immunities = "exhaustion, grappled, paralyzed, petrified, poisoned, prone, restrained"
|
condition_immunities = (
|
||||||
|
"exhaustion, grappled, paralyzed, petrified, poisoned, prone, restrained"
|
||||||
|
)
|
||||||
senses = "Darkvision 120 ft., Passive Perception 11"
|
senses = "Darkvision 120 ft., Passive Perception 11"
|
||||||
languages = "Abyssal, telepathy 120 ft."
|
languages = "Abyssal, telepathy 120 ft."
|
||||||
strength = Ability(1)
|
strength = Ability(1)
|
||||||
@@ -812,6 +818,7 @@ class StoneGolem(Monster):
|
|||||||
|
|
||||||
**Slow**: The golem targets one or more creatures it can see within 10 ft. of it. Each target must make a DC 17 Wisdom saving throw against this magic. On a failed save, a target can't use reactions, its speed is halved, and it can't make more than one attack on its turn. In addition, the target can take either an action or a bonus action on its turn, not both. These effects last for 1 minute. A target can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success.
|
**Slow**: The golem targets one or more creatures it can see within 10 ft. of it. Each target must make a DC 17 Wisdom saving throw against this magic. On a failed save, a target can't use reactions, its speed is halved, and it can't make more than one attack on its turn. In addition, the target can take either an action or a bonus action on its turn, not both. These effects last for 1 minute. A target can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Stone Golem"
|
name = "Stone Golem"
|
||||||
description = "Large construct, unaligned"
|
description = "Large construct, unaligned"
|
||||||
challenge_rating = 10
|
challenge_rating = 10
|
||||||
@@ -874,6 +881,7 @@ class Stonemelder(Monster):
|
|||||||
it serves as a conduit for Ogrémoch's wrath.
|
it serves as a conduit for Ogrémoch's wrath.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Stonemelder"
|
name = "Stonemelder"
|
||||||
description = "Medium humanoid (human), neutral evil"
|
description = "Medium humanoid (human), neutral evil"
|
||||||
challenge_rating = 4
|
challenge_rating = 4
|
||||||
@@ -895,7 +903,6 @@ class Stonemelder(Monster):
|
|||||||
hit_dice = "10d8 + 30"
|
hit_dice = "10d8 + 30"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class StormGiant(Monster):
|
class StormGiant(Monster):
|
||||||
"""
|
"""
|
||||||
**Amphibious**: The giant can breathe air and water.
|
**Amphibious**: The giant can breathe air and water.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def read_sheet_file(filename: Union[str, Path]) -> dict:
|
|||||||
these_props = reader()
|
these_props = reader()
|
||||||
# Resolve parent_sheets
|
# Resolve parent_sheets
|
||||||
char_props = {}
|
char_props = {}
|
||||||
parent_sheets = these_props.pop('parent_sheets', [])
|
parent_sheets = these_props.pop("parent_sheets", [])
|
||||||
for parent_sheet in parent_sheets:
|
for parent_sheet in parent_sheets:
|
||||||
parent_sheet = (filename.parent / parent_sheet).resolve()
|
parent_sheet = (filename.parent / parent_sheet).resolve()
|
||||||
if parent_sheet != filename:
|
if parent_sheet != filename:
|
||||||
@@ -322,7 +322,7 @@ class FoundryCharacterReader(JSONCharacterReader):
|
|||||||
"unarmed strike (monk)",
|
"unarmed strike (monk)",
|
||||||
"<no name>",
|
"<no name>",
|
||||||
]
|
]
|
||||||
|
|
||||||
def _skill_proficiency_value(self, key: str) -> float:
|
def _skill_proficiency_value(self, key: str) -> float:
|
||||||
proficiency_labels = {
|
proficiency_labels = {
|
||||||
"acrobatics": "acr",
|
"acrobatics": "acr",
|
||||||
@@ -386,7 +386,10 @@ class FoundryCharacterReader(JSONCharacterReader):
|
|||||||
"""Iterator over the weapons the character is carrying in her inventory."""
|
"""Iterator over the weapons the character is carrying in her inventory."""
|
||||||
items = self.json_data()["items"]
|
items = self.json_data()["items"]
|
||||||
for item in items:
|
for item in items:
|
||||||
is_valid_weapon = (item["type"] == "weapon" and item["name"].lower() not in self._invalid_weapons)
|
is_valid_weapon = (
|
||||||
|
item["type"] == "weapon"
|
||||||
|
and item["name"].lower() not in self._invalid_weapons
|
||||||
|
)
|
||||||
if is_valid_weapon:
|
if is_valid_weapon:
|
||||||
yield item["name"].lower()
|
yield item["name"].lower()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from collections import namedtuple
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dungeonsheets.armor import Armor, HeavyArmor, NoArmor, NoShield, Shield
|
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
|
||||||
from dungeonsheets.features import (
|
from dungeonsheets.features import (
|
||||||
AmbushMaster,
|
AmbushMaster,
|
||||||
Defense,
|
Defense,
|
||||||
@@ -25,7 +25,6 @@ from dungeonsheets.features import (
|
|||||||
UnarmoredDefenseMonk,
|
UnarmoredDefenseMonk,
|
||||||
UnarmoredMovement,
|
UnarmoredMovement,
|
||||||
)
|
)
|
||||||
from dungeonsheets.weapons import Weapon
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -89,14 +88,15 @@ class Skill:
|
|||||||
self.character = entity
|
self.character = entity
|
||||||
|
|
||||||
def __get__(self, entity, owner):
|
def __get__(self, entity, owner):
|
||||||
log.debug("Getting skill '%s' for '%s'",
|
log.debug("Getting skill '%s' for '%s'", self.skill_name, entity.name)
|
||||||
self.skill_name, entity.name)
|
|
||||||
ability = getattr(entity, self.ability_name)
|
ability = getattr(entity, self.ability_name)
|
||||||
modifier = ability.modifier
|
modifier = ability.modifier
|
||||||
# Check for proficiency
|
# Check for proficiency
|
||||||
proficiencies = [p.replace("_", " ") for p in entity.skill_proficiencies]
|
proficiencies = [p.replace("_", " ") for p in entity.skill_proficiencies]
|
||||||
is_proficient = self.skill_name in proficiencies
|
is_proficient = self.skill_name in proficiencies
|
||||||
log.debug("%s is proficient in %s: %s", entity.name, self.skill_name, is_proficient)
|
log.debug(
|
||||||
|
"%s is proficient in %s: %s", entity.name, self.skill_name, is_proficient
|
||||||
|
)
|
||||||
if is_proficient:
|
if is_proficient:
|
||||||
modifier += entity.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
elif entity.has_feature(JackOfAllTrades):
|
elif entity.has_feature(JackOfAllTrades):
|
||||||
@@ -109,8 +109,7 @@ class Skill:
|
|||||||
is_expert = self.skill_name in entity.skill_expertise
|
is_expert = self.skill_name in entity.skill_expertise
|
||||||
if is_expert:
|
if is_expert:
|
||||||
modifier += entity.proficiency_bonus
|
modifier += entity.proficiency_bonus
|
||||||
log.debug("'%s' modifier for '%s': %d",
|
log.info("'%s' modifier for '%s': %d", self.skill_name, entity.name, modifier)
|
||||||
self.skill_name, entity.name, modifier)
|
|
||||||
return modifier
|
return modifier
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ dungeonsheets_version = "0.15.0"
|
|||||||
|
|
||||||
sheet_type = "gm"
|
sheet_type = "gm"
|
||||||
|
|
||||||
|
summary = """The party is about the enter the dungeon of *eternal tortuosity*."""
|
||||||
|
|
||||||
session_title = "Objects in Space - Session 1"
|
session_title = "Objects in Space - Session 1"
|
||||||
|
|
||||||
parent_sheets = ["gm-campaign-notes.py"]
|
parent_sheets = ["gm-campaign-notes.py"]
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ class TexCreatorTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create_party_summary_tex(self):
|
def test_create_party_summary_tex(self):
|
||||||
char = self.new_character()
|
char = self.new_character()
|
||||||
tex = make_sheets.create_party_summary_tex(party=[char])
|
tex = make_sheets.create_party_summary_tex(party=[char], summary_rst="")
|
||||||
self.assertIn(r"\section*{Party}", tex)
|
self.assertIn(r"\section*{Party}", tex)
|
||||||
self.assertIn(char.name, tex)
|
self.assertIn(char.name, tex)
|
||||||
|
|
||||||
|
def test_create_summary_tex(self):
|
||||||
|
rst = "The party's create *adventure*."
|
||||||
|
tex = make_sheets.create_party_summary_tex(party=[], summary_rst=rst)
|
||||||
|
self.assertIn(r"\section*{Summary}", tex)
|
||||||
|
# Check that the RST is parsed
|
||||||
|
self.assertIn(r"\emph{adventure}", tex)
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -274,16 +274,16 @@ class AutoGeneratedMonsters(TestCase):
|
|||||||
monsters.StoneGolem,
|
monsters.StoneGolem,
|
||||||
monsters.StormGiant,
|
monsters.StormGiant,
|
||||||
monsters.SuccubusIncubus,
|
monsters.SuccubusIncubus,
|
||||||
monsters.SwarmofBats,
|
monsters.SwarmOfBats,
|
||||||
monsters.SwarmofBeetles,
|
monsters.SwarmOfBeetles,
|
||||||
monsters.SwarmofCentipedes,
|
monsters.SwarmOfCentipedes,
|
||||||
monsters.SwarmofInsects,
|
monsters.SwarmOfInsects,
|
||||||
monsters.SwarmofPoisonousSnakes,
|
monsters.SwarmOfPoisonousSnakes,
|
||||||
monsters.SwarmofQuippers,
|
monsters.SwarmOfQuippers,
|
||||||
monsters.SwarmofRats,
|
monsters.SwarmOfRats,
|
||||||
monsters.SwarmofRavens,
|
monsters.SwarmOfRavens,
|
||||||
monsters.SwarmofSpiders,
|
monsters.SwarmOfSpiders,
|
||||||
monsters.SwarmofWasps,
|
monsters.SwarmOfWasps,
|
||||||
monsters.Tarrasque,
|
monsters.Tarrasque,
|
||||||
monsters.Thug,
|
monsters.Thug,
|
||||||
monsters.Tiger,
|
monsters.Tiger,
|
||||||
|
|||||||
Reference in New Issue
Block a user