Added ability to define magic weapons by multiple inheritence.

This commit is contained in:
Mark Wolfman
2022-03-10 14:17:26 -06:00
parent 23b4bd2559
commit c9a0eb1f0e
41 changed files with 142 additions and 42 deletions
+27
View File
@@ -67,6 +67,33 @@ the global content manager, so in the above example ``weapons =
[my_homebrew.DullSword]`` and ``weapons = ["dull sword"]`` are
equivalent. See the :ref:`homebrew example` example for more examples.
Magic Weapons
-------------
A common situation is the creation of homebrew weapons. With multiple
inheritance, it is possible to include such a magic weapon as both a
weapon and magic item:
.. code:: python
from dungeonsheets import mechanics
class DullSword(mechanics.Weapon, mechanics.MagicItem):
"""This magical sword does remarkably little damage."""
name = "dull sword"
# Weapon attributes, e.g.
damage_bonus = -1
attack_bonus = -1
# Magical item attributes, e.g.
item_type = "weapon"
st_bonus_all = -1
weapons = [DullSword]
magic_items = [DullSword]
Strings
-------
+1 -1
View File
@@ -562,7 +562,7 @@ class Character(Creature):
SuperClass=magic_items.MagicItem,
warning_message=msg,
)
self.magic_items.append(ThisMagicItem(owner=self))
self.magic_items.append(ThisMagicItem(wielder=self))
elif attr == "weapon_proficiencies":
self.other_weapon_proficiencies = ()
msg = 'Magic Item "{}" not defined. Please add it to ``weapons.py``'
+7
View File
@@ -5,6 +5,7 @@ import warnings
from abc import ABC
from pathlib import Path
from dungeonsheets import exceptions
from dungeonsheets.stats import Ability, ArmorClass, Initiative, Speed, Skill
from dungeonsheets.content_registry import find_content
@@ -100,7 +101,13 @@ class Content(ABC):
# Create a generic message so we can make a docstring later.
msg = f'Mechanic "{mechanic}" not defined. Please add it.'
# Create generic mechanic from the factory
try:
class_name = "".join([s.title() for s in mechanic.split("_")])
except AttributeError:
raise exceptions.InvalidContentType(
f"``{mechanic}`` must either be string-like, "
f"or inherit from {SuperClass}. "
)
mechanic_name = mechanic.replace("_", " ").title()
attrs = {"name": mechanic_name, "__doc__": msg, "source": "Unknown"}
Mechanic = type(class_name, (SuperClass,), attrs)
+4
View File
@@ -35,3 +35,7 @@ class ContentNotFound(ValueError):
class AmbiguousContent(ValueError):
"""Multiple valid content entries were found."""
class InvalidContentType(ValueError):
"""Trying to resolve content that is either not a string, or not
related to the acceptable classes."""
+12 -2
View File
@@ -61,8 +61,8 @@ class MagicItem:
st_bonus_wisdom: Optional[int] = None
st_bonus_charisma: Optional[int] = None
def __init__(self, owner=None):
self.owner = owner
def __init__(self, wielder=None):
self.wielder = wielder
def __str__(self):
return self.name
@@ -430,7 +430,17 @@ class ShieldOfFaces(MagicItem):
name = "Shield of Faces"
class GlovesOfThievery(MagicItem):
"""These gloves are invisible while worn. While wearing them, you gain
a +5 bonus to Dexterity (Sleight of Hand) checks and Dexterity
checks made to pick locks."""
name = "Gloves of Thievery"
rarity = "Uncommon"
item_type = "Wondrous item"
class GlowingSword(MagicItem):
"""
This strange longsword glows at odd times.
"""
+7 -3
View File
@@ -192,13 +192,15 @@ class Skill:
@property
def is_proficient(self):
# Check for proficiency
proficiencies = [p.replace("_", " ") for p in self.actor.skill_proficiencies]
is_proficient = self.skill_name in proficiencies
proficiencies = [p.replace("_", " ").lower() for p in self.actor.skill_proficiencies]
is_proficient = self.skill_name.lower() in proficiencies
return is_proficient
@property
def is_expertise(self):
return self.skill_name in self.actor.skill_expertise
expertises = [p.replace("_", " ").lower() for p in self.actor.skill_expertise]
is_expertise = self.skill_name.lower() in expertises
return is_expertise
@property
def proficiency_modifier(self):
@@ -218,6 +220,8 @@ class Skill:
def modifier(self):
ability = getattr(self.actor, self.ability_name)
modifier = ability.modifier + self.proficiency_modifier
# if self.skill_name == "deception":
# import pdb; pdb.set_trace()
log.info("%s modifier for '%s': %d", self, self.actor.name, modifier)
return modifier
+11
View File
@@ -5,6 +5,17 @@ default_content_registry.add_module(__name__)
class Weapon:
"""A weapon that be used to deal damage.
Attributes
==========
Parameters
==========
wielder
The character (or NPC) that is using the weapon.
"""
name = ""
cost = "0 gp"
base_damage = "1d4"
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Gnomish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Draconic, Elvish, Common, Dwarvish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('bear aspect', 'tiger spirit', 'elk attunement')
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Dwarvish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish, [choose one]"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish"""
+1 -1
View File
@@ -57,7 +57,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Elvish, Common, Draconic"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Halfling"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Halfling"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], [choose one], Common, [choose one]"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('underdark',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish"""
+1 -1
View File
@@ -57,7 +57,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Elvish, Common, Draconic"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Gnomish"""
+5 -3
View File
@@ -60,13 +60,15 @@ features = (Juggler, "master_of_ceremonies")
feature_choices = ()
class DullSword(mechanics.Weapon):
class DullSword(mechanics.Weapon, mechanics.MagicItem):
"""Bonk things with it."""
name = "Dullsword"
damage_bonus = -1
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = (DullSword,) # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = ("dull sword",) # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Primoridal"""
@@ -97,7 +99,7 @@ class LegoShield(mechanics.Shield):
weapons = (DullSword, "rusty_shiv", _campaign.BrightSword) # Example: ('shortsword', 'longsword')
magic_items = (RobeOfBreadSummoning, "staff_of_the_arbor_abode")
magic_items = (RobeOfBreadSummoning, "staff_of_the_arbor_abode", DullSword)
armor = PlasticArmor # Eg "leather armor"
shield = LegoShield # Eg "shield"
+1 -1
View File
@@ -57,7 +57,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Gnomish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish, [choose one]"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('dueling',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Orc"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('great-weapon master', )
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Dwarvish, Common, Infernal"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('defense',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Dwarvish, Common, Infernal"""
+1 -1
View File
@@ -56,7 +56,7 @@ feature_choices = ('escape the horde', 'archery', 'giant killer', 'volley',
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Elvish"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('dueling',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Dwarvish, Common, Draconic"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ('two-weapon fighting',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """celestial, elvish, Common, Primoridal"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Aarakocra, Auran"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Gnomish, Undercommon"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Primordial"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], [choose one], Common, Celestial"""
+1 -1
View File
@@ -57,7 +57,7 @@ feature_choices = ('pact of the blade',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Elvish, Common, Auran"""
+1 -1
View File
@@ -56,7 +56,7 @@ feature_choices = ('pact of the tome',)
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Celestial, Dwarvish, Common, Elvish, Giant"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """[choose one], Common, Primoridal"""
+1 -1
View File
@@ -55,7 +55,7 @@ feature_choices = ()
# Weapons/other proficiencies not given by class/race/background
weapon_proficiencies = () # ex: ('shortsword', 'quarterstaff')
_proficiencies_text = () # ex: ("thieves' tools",)
proficiencies_text = () # ex: ("thieves' tools",)
# Proficiencies and languages
languages = """Common, Orc"""
+9
View File
@@ -10,6 +10,7 @@ from dungeonsheets.character import (
Druid,
)
from dungeonsheets.weapons import Weapon, Shortsword
from dungeonsheets.magic_items import MagicItem
from dungeonsheets.armor import Armor, LeatherArmor, Shield
@@ -43,6 +44,11 @@ class TestCharacter(TestCase):
char.set_attrs(armor="leather armor", shield="shield")
self.assertFalse(isinstance(char.armor, str))
self.assertFalse(isinstance(char.shield, str))
# Check that magic item gets set_attrs
MagicWeapon = type("MagicWeapon", (Weapon, MagicItem),
dict(damage_bonus=2, attack_bonus=2,
st_bonus_all=3))
char.set_attrs(magic_items=[MagicWeapon])
# Check that race gets set to an object
char.set_attrs(race="high elf")
self.assertIsInstance(char.race, race.HighElf)
@@ -51,6 +57,9 @@ class TestCharacter(TestCase):
self.assertTrue(char.inspiration)
char.set_attrs(inspiration=False)
self.assertFalse(char.inspiration)
# Check that proficiencies text gets included
char.set_attrs(proficiencies_text=("dull sword",))
self.assertIn("dull sword", char.proficiencies_text.lower())
def test_homebrew_spells(self):
char = Character()
+2 -2
View File
@@ -13,7 +13,7 @@ class MyMagicItem(magic_items.MagicItem):
class MagicItemTests(unittest.TestCase):
def test_st_bonus_all(self):
char = Character()
my_item = MyMagicItem(owner=char)
my_item = MyMagicItem(wielder=char)
char.magic_items = [my_item]
# Test an item that confers no saving throw bonus
bonus = my_item.st_bonus()
@@ -25,7 +25,7 @@ class MagicItemTests(unittest.TestCase):
def test_st_bonus_by_ability(self):
char = Character(strength=10)
my_item = MyMagicItem(owner=char)
my_item = MyMagicItem(wielder=char)
char.magic_items = [my_item]
# Test an item with nonsense ability
with self.assertRaises(AttributeError):
+26
View File
@@ -1,5 +1,6 @@
import unittest
from dungeonsheets.magic_items import MagicItem
from dungeonsheets.weapons import Weapon
@@ -11,3 +12,28 @@ class WeaponTestCase(unittest.TestCase):
# Now add some bonus damage
weapon.damage_bonus = 2
self.assertEqual(weapon.damage, "1d6+2")
class MagicWeaponTestCase(unittest.TestCase):
"""Check that a magic weapon works as intended."""
def test_class_inheritance_weapon_first(self):
"""Test that the class inheritance works correctly for multiclassing."""
MagicWeapon = type("MagicWeapon", (Weapon, MagicItem),
dict(damage_bonus=2, attack_bonus=2,
st_bonus_all=3))
weapon = MagicWeapon(wielder=None)
# CHeck some weapon traits
self.assertEqual(weapon.damage, "1d4+2")
# Check some magic item traits
self.assertEqual(weapon.st_bonus_all, 3)
def test_class_inheritance_magic_item_first(self):
"""Test that the class inheritance works correctly for multiclassing."""
MagicWeapon = type("MagicWeapon", (MagicItem, Weapon),
dict(damage_bonus=2, attack_bonus=2,
st_bonus_all=3))
weapon = MagicWeapon(wielder=None)
# CHeck some weapon traits
self.assertEqual(weapon.damage, "1d4+2")
# Check some magic item traits
self.assertEqual(weapon.st_bonus_all, 3)