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",
"background",
"spells",
"import_homebrew"
"import_homebrew",
)
from dungeonsheets import background, features, race, spells, weapons, mechanics
+1
View File
@@ -110,6 +110,7 @@ class RivalIntern(Background):
company, ready to put your skills lo use.
"""
name = "Rival Intern"
skill_proficiencies = ("history", "investigation")
proficiencies_text = ("One type of artisan's tools",)
+22 -13
View File
@@ -175,18 +175,18 @@ class Character(Entity):
# Appearance
# portrait = placeholder not sure how to implement
age = 0
height = ''
weight = ''
eyes = ''
skin = ''
hair = ''
height = ""
weight = ""
eyes = ""
skin = ""
hair = ""
# Background
allies = ''
faction_name = ''
allies = ""
faction_name = ""
# faction_symbol = placeholder not sure how to implement
backstory = ''
other_feats_traits = ''
treasure = ''
backstory = ""
other_feats_traits = ""
treasure = ""
def __init__(
self,
@@ -344,7 +344,9 @@ class Character(Entity):
self._race = newrace(owner=self)
elif isinstance(newrace, str):
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:
msg = f'Race "{newrace}" not defined. Please add it to ``race.py``'
self._race = race.Race(owner=self)
@@ -365,7 +367,9 @@ class Character(Entity):
self._background = bg(owner=self)
elif isinstance(bg, str):
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:
msg = (
f'Background "{bg}" not defined. Please add it to ``background.py``'
@@ -634,7 +638,12 @@ class Character(Entity):
self.other_weapon_proficiencies = ()
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
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)
self.other_weapon_proficiencies = list(wps)
+21 -4
View File
@@ -3,10 +3,20 @@ from functools import lru_cache
import importlib.util
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
def __init__(self):
@@ -36,7 +46,9 @@ class ContentRegistry():
bonus = i
name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
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("_")])
# Check each module in the registry
found_attrs = []
@@ -51,7 +63,12 @@ class ContentRegistry():
if len(valid_classes) > 0:
is_valid = [False for attr in found_attrs]
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]
# Check that we found a valid, unique attribute
if len(found_attrs) == 0:
+1
View File
@@ -344,6 +344,7 @@ class InsideInformant(Feature):
with your contacts, gaining information at the DM's discretion.
"""
name = "Inside Informant"
source = "Background (Rival Intern)"
+13 -7
View File
@@ -87,7 +87,9 @@ def create_character_pdf_template(character, basename, flatten=False):
# Hit points
"HDTotal": character.hit_dice,
"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 "",
# Personality traits and other features
"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 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")
for _fields, weapon in zip(weapon_fields, character.weapons):
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[dmg_field] = f"{weapon.damage}/{weapon.damage_type}"
# Additional attacks beyond 3
attack = [f"{w.name}: Atk {w.attack_modifier:+d}, Dam {w.damage}/{w.damage_type}"
for w in character.weapons[len(weapon_fields):]]
attack = [
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
if 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,
"Backstory": text_box(character.backstory),
"Feat+Traits": text_box(character.other_feats_traits),
"Treasure": text_box(character.treasure)
"Treasure": text_box(character.treasure),
}
# Prepare the actual PDF
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
if len(spells) > len(field_numbers[level]):
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(
f"{character.name} knows more spells than the number of "
"lines available in spell sheet. Limited to prepared "
"spells only.")
"spells only."
)
# Build the list of PDF controls to set/toggle
field_names = [f"Spells {i}" for i in field_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}
[% if use_dnd_decorations %]
@@ -55,3 +65,5 @@
[% endfor %]
\end{tabular}
[% endif %]
[% endif %]
-1
View File
@@ -1,2 +1 @@
"""Tools useful for defining homebrew content."""
+11 -8
View File
@@ -94,10 +94,14 @@ def create_monsters_tex(
def create_party_summary_tex(
party: Sequence[Entity],
summary_rst: str,
use_dnd_decorations: bool = False,
) -> str:
log.debug("Preparing summary table for party: %s", party)
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(
@@ -196,7 +200,7 @@ def make_gm_sheet(
title=gm_props["session_title"],
)
]
# Add the party stats table
# Add the party stats table and session summary
party = []
for char_file in gm_props.get("party", []):
# Resolve the file path
@@ -208,9 +212,11 @@ def make_gm_sheet(
character_props = readers.read_sheet_file(char_file)
member = _char.Character.load(character_props)
party.append(member)
if len(party) > 0:
summary = gm_props.get("summary", "")
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
monsters_ = []
@@ -222,10 +228,7 @@ def make_gm_sheet(
try:
MyMonster = find_content(monster, valid_classes=[monsters.Monster])
except AttributeError:
msg = (
f"Monster '{monster}' not found. Please add it to"
" ``monsters.py``"
)
msg = f"Monster '{monster}' not found. Please add it to ``monsters.py``"
warnings.warn(msg)
continue
else:
+1
View File
@@ -92,6 +92,7 @@ class Acolyte(Monster):
(1d4) bludgeoning damage.
"""
name = "Acolyte"
description = "Medium humanoid, any alignment"
challenge_rating = 0.25
+6
View File
@@ -450,6 +450,7 @@ class BlackEarthGuard(Monster):
burrowsharks.
"""
name = "Black Earth Guard"
description = "Medium humanoid (human), neutral evil"
challenge_rating = 2
@@ -507,6 +508,7 @@ class BlackEarthPriest(Monster):
over the rest of Ogrémoch's followers.
"""
name = "Black Earth Priest"
description = "Medium humanoid (human), neutral evil"
challenge_rating = 3
@@ -553,6 +555,7 @@ class BlackPudding(Monster):
AC to 10.
"""
name = "Black Pudding"
description = "Large ooze, unaligned"
challenge_rating = 4
@@ -885,6 +888,7 @@ class Burrowshark(Monster):
neither moved nor knocked prone.
"""
name = "Burrowshark"
description = "Medium humanoid (human), neutral evil"
challenge_rating = 4
@@ -927,6 +931,7 @@ class Bulette(Monster):
prone in the bulette's space.
"""
name = "Bulette"
description = "Large monstrosity, unaligned"
challenge_rating = 5
@@ -944,5 +949,6 @@ class Bulette(Monster):
swim_speed = 0
fly_speed = 0
climb_speed = 0
burrow_speed = 40
hp_max = 94
hit_dice = "9d10+45"
+1 -1
View File
@@ -40,6 +40,7 @@ class Dao(Monster):
check or be knocked prone.
"""
name = "Dao"
description = "Large elemental, neutral evil"
challenge_rating = 11
@@ -64,7 +65,6 @@ class Dao(Monster):
hit_dice = "15d10 + 105"
class Darkmantle(Monster):
"""
**Echolocation**: The darkmantle can't use its blindsight while deafened.
+2
View File
@@ -50,6 +50,7 @@ class EarthElemental(Monster):
14 (2d8 + 5) bludgeoning damage.
"""
name = "Earth Elemental"
description = "Large elemental, neutral"
challenge_rating = 5
@@ -92,6 +93,7 @@ class EarthElementalMyrmidon(Monster):
creators.
"""
name = "Earth Elemental Myrmidon"
description = "Medium elemental, neutral"
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
had advantage on the attack roll, 17 (4d6+3) psychic damage.
"""
name = "Shadow Demon"
description = "Medium fiend (demon), chaotic evil"
challenge_rating = 4
@@ -323,9 +324,14 @@ class ShadowDemon(Monster):
skills = "Stealth +7"
saving_throws = "Dex +5, Cha +4"
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"
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"
languages = "Abyssal, telepathy 120 ft."
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.
"""
name = "Stone Golem"
description = "Large construct, unaligned"
challenge_rating = 10
@@ -874,6 +881,7 @@ class Stonemelder(Monster):
it serves as a conduit for Ogrémoch's wrath.
"""
name = "Stonemelder"
description = "Medium humanoid (human), neutral evil"
challenge_rating = 4
@@ -895,7 +903,6 @@ class Stonemelder(Monster):
hit_dice = "10d8 + 30"
class StormGiant(Monster):
"""
**Amphibious**: The giant can breathe air and water.
+5 -2
View File
@@ -45,7 +45,7 @@ def read_sheet_file(filename: Union[str, Path]) -> dict:
these_props = reader()
# Resolve parent_sheets
char_props = {}
parent_sheets = these_props.pop('parent_sheets', [])
parent_sheets = these_props.pop("parent_sheets", [])
for parent_sheet in parent_sheets:
parent_sheet = (filename.parent / parent_sheet).resolve()
if parent_sheet != filename:
@@ -386,7 +386,10 @@ class FoundryCharacterReader(JSONCharacterReader):
"""Iterator over the weapons the character is carrying in her inventory."""
items = self.json_data()["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:
yield item["name"].lower()
+6 -7
View File
@@ -3,7 +3,7 @@ from collections import namedtuple
from math import ceil
import logging
from dungeonsheets.armor import Armor, HeavyArmor, NoArmor, NoShield, Shield
from dungeonsheets.armor import HeavyArmor, NoArmor, NoShield
from dungeonsheets.features import (
AmbushMaster,
Defense,
@@ -25,7 +25,6 @@ from dungeonsheets.features import (
UnarmoredDefenseMonk,
UnarmoredMovement,
)
from dungeonsheets.weapons import Weapon
log = logging.getLogger(__name__)
@@ -89,14 +88,15 @@ class Skill:
self.character = entity
def __get__(self, entity, owner):
log.debug("Getting skill '%s' for '%s'",
self.skill_name, entity.name)
log.debug("Getting skill '%s' for '%s'", self.skill_name, entity.name)
ability = getattr(entity, self.ability_name)
modifier = ability.modifier
# Check for proficiency
proficiencies = [p.replace("_", " ") for p in entity.skill_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:
modifier += entity.proficiency_bonus
elif entity.has_feature(JackOfAllTrades):
@@ -109,8 +109,7 @@ class Skill:
is_expert = self.skill_name in entity.skill_expertise
if is_expert:
modifier += entity.proficiency_bonus
log.debug("'%s' modifier for '%s': %d",
self.skill_name, entity.name, modifier)
log.info("'%s' modifier for '%s': %d", self.skill_name, entity.name, modifier)
return modifier
+2
View File
@@ -9,6 +9,8 @@ dungeonsheets_version = "0.15.0"
sheet_type = "gm"
summary = """The party is about the enter the dungeon of *eternal tortuosity*."""
session_title = "Objects in Space - Session 1"
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):
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(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.StormGiant,
monsters.SuccubusIncubus,
monsters.SwarmofBats,
monsters.SwarmofBeetles,
monsters.SwarmofCentipedes,
monsters.SwarmofInsects,
monsters.SwarmofPoisonousSnakes,
monsters.SwarmofQuippers,
monsters.SwarmofRats,
monsters.SwarmofRavens,
monsters.SwarmofSpiders,
monsters.SwarmofWasps,
monsters.SwarmOfBats,
monsters.SwarmOfBeetles,
monsters.SwarmOfCentipedes,
monsters.SwarmOfInsects,
monsters.SwarmOfPoisonousSnakes,
monsters.SwarmOfQuippers,
monsters.SwarmOfRats,
monsters.SwarmOfRavens,
monsters.SwarmOfSpiders,
monsters.SwarmOfWasps,
monsters.Tarrasque,
monsters.Thug,
monsters.Tiger,