diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index 90314c1..b1c528e 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -328,7 +328,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin): merge_modifiers(self.conditions) for mapping in self.equipped_items: - for (target, mods) in mapping.item.modifiers.items(): + for target, mods in mapping.item.modifiers.items(): for mod in mods: if mod.requires_attunement and not mapping.attuned: continue diff --git a/src/ttfrog/db/schema/item.py b/src/ttfrog/db/schema/item.py index c469e22..557a502 100644 --- a/src/ttfrog/db/schema/item.py +++ b/src/ttfrog/db/schema/item.py @@ -2,9 +2,9 @@ from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship from ttfrog.db.base import BaseObject, EnumField +from ttfrog.db.schema.classes import CharacterClass from ttfrog.db.schema.constants import DamageType from ttfrog.db.schema.modifiers import ModifierMixin -from ttfrog.db.schema.classes import CharacterClass __all__ = [ "Item", @@ -21,14 +21,7 @@ ITEM_TYPES = [ "SHIELD", ] -RARITY = [ - "Common", - "Uncommon", - "Rare", - "Very Rare", - "Legendary", - "Artifact" -] +RARITY = ["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"] ItemType = EnumField("ItemType", ((k, k) for k in ITEM_TYPES)) diff --git a/src/ttfrog/db/schema/modifiers.py b/src/ttfrog/db/schema/modifiers.py index f16e2b5..5a16cb6 100644 --- a/src/ttfrog/db/schema/modifiers.py +++ b/src/ttfrog/db/schema/modifiers.py @@ -175,7 +175,7 @@ class ModifierMixin: Returns the matching column if it was found, or None. """ - if attr_name.startswith('_'): + if attr_name.startswith("_"): return None col = getattr(self.__table__.columns, f"_{attr_name}", None) if col is None: diff --git a/test/test_items.py b/test/test_items.py new file mode 100644 index 0000000..fe12130 --- /dev/null +++ b/test/test_items.py @@ -0,0 +1,107 @@ +from ttfrog.db.schema.constants import DamageType, Defenses +from ttfrog.db.schema.item import Item, ItemType, Rarity, Weapon +from ttfrog.db.schema.modifiers import Modifier + + +def test_weapons(db): + with db.transaction(): + longbow = Weapon( + name="longbow", + damage_die="1d6", + damage_type=DamageType.piercing, + martial=True, + two_handed=True, + ammunition=True, + targets=1, + attack_range=150, + attack_range_long=600, + ) + + dagger = Weapon( + name="Dagger", + damage_die="1d4", + damage_type=DamageType.slashing, + melee=True, + finesse=True, + light=True, + thrown=True, + attack_range=20, + attack_range_long=60, + ) + + db.add_or_update([longbow, dagger]) + + assert longbow.martial + assert longbow.ranged + assert not longbow.melee + + assert not dagger.martial + assert dagger.ranged + assert dagger.melee + + +def test_attunement(db, carl): + with db.transaction(): + helm = Item( + name="Iron Helm", + item_type=ItemType.ARMOR, + rarity=Rarity.Common, + ) + helm.add_modifier(Modifier("+1 AC (helmet)", target="armor_class", relative_value=1, stacks=True)) + + shield = Item( + name="Shield of Missile Attraction", + description=""" + While holding this shield, you have resistance to damage from ranged weapon attacks. + + **Curse.** This shield is cursed. Attuning to it curses you until you are targeted by the remove curse spell + or similar magic. Removing the shield fails to end the curse on you. Whenever a ranged weapon attack is made + against a target within 10 feet of you, the curse causes you to become the target instead. + """, + item_type=ItemType.SHIELD, + rarity=Rarity.Rare, + requires_attunement=True, + ) + + plus_two_ac = Modifier("+2 AC (Shield", target="armor_class", relative_value=2, stacks=True) + ranged_resistance = Modifier( + "Resistance to Damage From Ranged Weapon Attacks (Shield of Missle Attraction)", + target=DamageType.ranged_weapon_attacks, + new_value=Defenses.resistant, + requires_attunement=True, + ) + shield.add_modifier(plus_two_ac) + shield.add_modifier(ranged_resistance) + + db.add_or_update([carl, helm, shield]) + + assert carl.equipment.add(shield) + assert carl.equipment.add(helm) + db.add_or_update(carl) + + carls_shield = carl.equipment.get(shield)[0] + + assert carl.armor_class == 10 + assert len(carl.attuned_items) == 0 + + carl.equip(carls_shield) + + assert plus_two_ac in carl.modifiers["armor_class"] + assert ranged_resistance not in carl.modifiers[DamageType.ranged_weapon_attacks] + assert carl.armor_class == 12 + assert carls_shield not in carl.attuned_items + + carl.attune(carls_shield) + assert carl.armor_class == 12 + assert plus_two_ac in carl.modifiers["armor_class"] + assert ranged_resistance in carl.modifiers[DamageType.ranged_weapon_attacks] + assert carls_shield in carl.attuned_items + + assert carl.equip(carl.equipment.get(helm)[0]) + assert carl.armor_class == 13 + + assert carl.unattune(carls_shield) + assert carl.armor_class == 13 + assert ranged_resistance not in carl.modifiers[DamageType.ranged_weapon_attacks] + assert carl.unequip(carls_shield) + assert carl.armor_class == 11