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",
|
||||
"background",
|
||||
"spells",
|
||||
"import_homebrew"
|
||||
"import_homebrew",
|
||||
)
|
||||
|
||||
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
|
||||
Incorporated. Either way, you're now bringing your talents to the
|
||||
company, ready to put your skills lo use.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
name = "Rival Intern"
|
||||
skill_proficiencies = ("history", "investigation")
|
||||
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::
|
||||
|
||||
>>> _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:
|
||||
@@ -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)
|
||||
|
||||
@@ -3,29 +3,39 @@ 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):
|
||||
self.modules = []
|
||||
|
||||
|
||||
def add_module(self, new_module):
|
||||
if new_module not in self.modules:
|
||||
self.modules.append(new_module)
|
||||
|
||||
def findattr(self, name, valid_classes=[]):
|
||||
"""Resolve the name of a piece of content to the corresponding Class.
|
||||
|
||||
|
||||
Similar to builtin getattr(obj, name) but more forgiving to
|
||||
whitespace and capitalization.
|
||||
|
||||
valid_classes
|
||||
If given, only subclasses of classes in this list will be
|
||||
returned.
|
||||
|
||||
|
||||
"""
|
||||
# Come up with several options
|
||||
name = name.strip()
|
||||
@@ -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:
|
||||
@@ -94,7 +111,7 @@ def find_content(name: str, valid_classes: Optional[List]):
|
||||
valid_classes
|
||||
A list of parent classes to look for. If ``None`` or ``[]``, all
|
||||
classes will be considered valid.
|
||||
|
||||
|
||||
"""
|
||||
if valid_classes is None:
|
||||
valid_classes = []
|
||||
@@ -112,7 +129,7 @@ def import_homebrew(filepath: Union[str, Path]):
|
||||
==========
|
||||
filepath
|
||||
The location of the python file containing the homebrew content.
|
||||
|
||||
|
||||
Returns
|
||||
=======
|
||||
mod
|
||||
|
||||
@@ -342,8 +342,9 @@ class InsideInformant(Feature):
|
||||
"""You have connections to your previous employer or other groups you
|
||||
dealt with during your previous employment. You can communicate
|
||||
with your contacts, gaining information at the DM's discretion.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
name = "Inside Informant"
|
||||
source = "Background (Rival Intern)"
|
||||
|
||||
|
||||
@@ -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 "
|
||||
"lines available in spell sheet. Limited to prepared "
|
||||
"spells only.")
|
||||
warnings.warn(
|
||||
f"{character.name} knows more spells than the number of "
|
||||
"lines available in spell sheet. Limited to prepared "
|
||||
"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,2 +1 @@
|
||||
"""Tools useful for defining homebrew content."""
|
||||
|
||||
|
||||
@@ -93,11 +93,15 @@ def create_monsters_tex(
|
||||
|
||||
|
||||
def create_party_summary_tex(
|
||||
party: Sequence[Entity],
|
||||
use_dnd_decorations: bool = False,
|
||||
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,10 +212,12 @@ 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:
|
||||
tex.append(
|
||||
create_party_summary_tex(party, use_dnd_decorations=fancy_decorations)
|
||||
summary = gm_props.get("summary", "")
|
||||
tex.append(
|
||||
create_party_summary_tex(
|
||||
party, summary_rst=summary, use_dnd_decorations=fancy_decorations
|
||||
)
|
||||
)
|
||||
# Add the monsters
|
||||
monsters_ = []
|
||||
for monster in gm_props.get("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:
|
||||
|
||||
@@ -21,7 +21,7 @@ class Monster(Entity):
|
||||
saving_throws = ""
|
||||
# TODO: Consider refactoring stats.Speed to consider all of these
|
||||
# just like we do stats.Ability
|
||||
|
||||
|
||||
swim_speed = 0
|
||||
fly_speed = 0
|
||||
climb_speed = 0
|
||||
|
||||
@@ -48,7 +48,7 @@ class Aboleth(Monster):
|
||||
charmed target is under the aboleth's control and can't take
|
||||
reactions, and the aboleth and the target can communicate
|
||||
telepathically with each other over any distance.
|
||||
|
||||
|
||||
Whenever the charmed target takes damage, the target can repeat
|
||||
the saving throw. On a success, the effect ends. No more than
|
||||
once every 24 hours, the target can also repeat the saving throw
|
||||
@@ -79,7 +79,7 @@ class Aboleth(Monster):
|
||||
|
||||
class Acolyte(Monster):
|
||||
"""Spellcasting
|
||||
|
||||
|
||||
The acolyte is a 1st-level spellcaster. Its spellcasting ability
|
||||
is Wisdom (spell save DC 12, +4 to hit with spell attacks). The
|
||||
acolyte has following cleric spells prepared:
|
||||
@@ -92,6 +92,7 @@ class Acolyte(Monster):
|
||||
(1d4) bludgeoning damage.
|
||||
|
||||
"""
|
||||
|
||||
name = "Acolyte"
|
||||
description = "Medium humanoid, any alignment"
|
||||
challenge_rating = 0.25
|
||||
|
||||
@@ -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
|
||||
@@ -551,8 +553,9 @@ class BlackPudding(Monster):
|
||||
dissolved and takes a permanent and cumulative -1 penalty to the
|
||||
AC it offers. The armor is destroyed if the penalty reduces its
|
||||
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"
|
||||
|
||||
@@ -20,7 +20,7 @@ class Dao(Monster):
|
||||
The dao's innate spellcasting ability is Charisma (spell save DC
|
||||
14, +6 to hit with spell attacks). It can innately cast the
|
||||
following spells, requiring no material components:
|
||||
|
||||
|
||||
At will: detect evil and good, detect magic, stone shape. 3/day
|
||||
each: passwall, move earth, tongues. 1/day each: conjure
|
||||
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
|
||||
a Huge or smaller creature, it must succeed on a DC 18 Strength
|
||||
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.
|
||||
|
||||
@@ -48,8 +48,9 @@ class EarthElemental(Monster):
|
||||
Slam.
|
||||
Melee Weapon Attack: +8 to hit, reach 10 ft., one target. Hit:
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
@@ -322,7 +322,7 @@ class FoundryCharacterReader(JSONCharacterReader):
|
||||
"unarmed strike (monk)",
|
||||
"<no name>",
|
||||
]
|
||||
|
||||
|
||||
def _skill_proficiency_value(self, key: str) -> float:
|
||||
proficiency_labels = {
|
||||
"acrobatics": "acr",
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user