Fixed broken test and added ability to put session summary into GM notes.

This commit is contained in:
Mark Wolfman
2021-06-16 10:03:35 -05:00
parent a9c893a4e3
commit c5672f950f
20 changed files with 156 additions and 80 deletions
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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)
+26 -9
View File
@@ -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
+2 -1
View File
@@ -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)"
+14 -8
View File
@@ -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
View File
@@ -1,2 +1 @@
"""Tools useful for defining homebrew content.""" """Tools useful for defining homebrew content."""
+14 -11
View File
@@ -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:
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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
+7 -1
View File
@@ -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"
+3 -3
View File
@@ -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.
+3 -1
View File
@@ -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
+10 -3
View File
@@ -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.
+6 -3
View File
@@ -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()
+6 -7
View File
@@ -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
+2
View File
@@ -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"]
+9 -1
View File
@@ -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
View File
@@ -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,