diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index f67c2fa..90314c1 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -316,13 +316,25 @@ class Character(BaseObject, SlugMixin, ModifierMixin): @property def modifiers(self): - unified = {} - unified.update(**self.ancestry.modifiers) - for trait in self.traits: - unified.update(**trait.modifiers) - for condition in self.conditions: - unified.update(**condition.modifiers) - unified.update(**super().modifiers) + unified = defaultdict(list) + + def merge_modifiers(object_list): + for obj in object_list: + for target, mods in obj.modifiers.items(): + unified[target] += mods + + merge_modifiers([self.ancestry]) + merge_modifiers(self.traits) + merge_modifiers(self.conditions) + + for mapping in self.equipped_items: + for (target, mods) in mapping.item.modifiers.items(): + for mod in mods: + if mod.requires_attunement and not mapping.attuned: + continue + unified[target].append(mod) + + merge_modifiers([super()]) return unified @property @@ -384,6 +396,30 @@ class Character(BaseObject, SlugMixin, ModifierMixin): def equipment(self): return [inv for inv in self._inventories if inv.inventory_type == InventoryType.EQUIPMENT][0] + @property + def equipped_items(self): + return [item for item in self.equipment if item.equipped] + + @property + def attuned_items(self): + return [item for item in self.equipment if item.attuned] + + def attune(self, mapping): + if mapping.attuned: + return False + if not mapping.item.requires_attunement: + return False + if len(self.attuned_items) >= 3: + return False + mapping.attuned = True + return True + + def unattune(self, mapping): + if not mapping.attuned: + return False + mapping.attuned = False + return True + def equip(self, mapping): if mapping.equipped: return False diff --git a/src/ttfrog/db/schema/constants.py b/src/ttfrog/db/schema/constants.py index e62ce8d..7db11e7 100644 --- a/src/ttfrog/db/schema/constants.py +++ b/src/ttfrog/db/schema/constants.py @@ -43,6 +43,8 @@ class DamageType(StrEnum): adamantium_piercing = auto() adamantium_slashing = auto() adamantium_bludgeoning = auto() + ranged_weapon_attacks = auto() + melee_weapon_attacks = auto() class Defenses(StrEnum): diff --git a/src/ttfrog/db/schema/inventory.py b/src/ttfrog/db/schema/inventory.py index ca5441e..1dca61d 100644 --- a/src/ttfrog/db/schema/inventory.py +++ b/src/ttfrog/db/schema/inventory.py @@ -13,6 +13,9 @@ class InventoryType(EnumField): inventory_type_map = { InventoryType.EQUIPMENT: [ + ItemType.WEAPON, + ItemType.ARMOR, + ItemType.SHIELD, ItemType.ITEM, ItemType.SCROLL, ], @@ -54,18 +57,6 @@ class Inventory(BaseObject): self.inventory_map.remove(mapping.id) return True - def equip(self, mapping): - if mapping.equipped: - return False - mapping.equipped = True - return True - - def unequip(self, mapping): - if not mapping.equipped: - return False - mapping.equipped = False - return True - def __contains__(self, obj): for mapping in self._inventory_map: if mapping.item == obj: @@ -85,6 +76,7 @@ class InventoryMap(BaseObject): inventory: Mapped["Inventory"] = relationship(uselist=False, viewonly=True, init=False) equipped: Mapped[bool] = mapped_column(default=False) + attuned: Mapped[bool] = mapped_column(default=False) count: Mapped[int] = mapped_column(nullable=False, default=1) always_prepared: Mapped[bool] = mapped_column(default=False) diff --git a/src/ttfrog/db/schema/item.py b/src/ttfrog/db/schema/item.py index 9725244..c469e22 100644 --- a/src/ttfrog/db/schema/item.py +++ b/src/ttfrog/db/schema/item.py @@ -1,8 +1,10 @@ from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm import Mapped, mapped_column, relationship from ttfrog.db.base import BaseObject, EnumField -from ttfrog.db.schema.modifiers import ConditionMixin +from ttfrog.db.schema.constants import DamageType +from ttfrog.db.schema.modifiers import ModifierMixin +from ttfrog.db.schema.classes import CharacterClass __all__ = [ "Item", @@ -10,20 +12,48 @@ __all__ = [ ] -class ItemType(EnumField): - ITEM = "ITEM" - SPELL = "SPELL" - SCROLL = "SCROLL" +ITEM_TYPES = [ + "ITEM", + "SPELL", + "SCROLL", + "WEAPON", + "ARMOR", + "SHIELD", +] + +RARITY = [ + "Common", + "Uncommon", + "Rare", + "Very Rare", + "Legendary", + "Artifact" +] -class Item(BaseObject, ConditionMixin): +ItemType = EnumField("ItemType", ((k, k) for k in ITEM_TYPES)) +Rarity = EnumField("Rarity", ((k, k) for k in RARITY)) + + +class Item(BaseObject, ModifierMixin): __tablename__ = "item" __mapper_args__ = {"polymorphic_identity": ItemType.ITEM, "polymorphic_on": "item_type"} id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True) + description: Mapped[str] = mapped_column(String, nullable=True, default=None) + + item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False) + rarity: Mapped[Rarity] = mapped_column(default=Rarity.Common, nullable=False) + requires_attunement: Mapped[bool] = mapped_column(nullable=False, default=False) + consumable: Mapped[bool] = mapped_column(default=False) count: Mapped[int] = mapped_column(nullable=False, default=1) - item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False) + + _class_restrictions: Mapped[int] = mapped_column(ForeignKey("character_class.id"), nullable=True, default=None) + class_restrictions: Mapped["CharacterClass"] = relationship(init=False) + + # _spells: Mapped[int] = mapped_column(ForeignKey("spell.id"), nullable=True, default=None) + # spells: Mapped["Spell"] = relationship(init=False) class Spell(Item): @@ -33,3 +63,32 @@ class Spell(Item): level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0) concentration: Mapped[bool] = mapped_column(default=False) item_type: Mapped[ItemType] = mapped_column(default=ItemType.SPELL, init=False) + + +class Weapon(Item): + __tablename__ = "weapon" + __mapper_args__ = {"polymorphic_identity": ItemType.WEAPON} + id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False) + damage_die: Mapped[str] = mapped_column(nullable=False, default="1d6") + damage_type: Mapped[DamageType] = mapped_column(nullable=False, default=DamageType.slashing) + item_type: Mapped[ItemType] = mapped_column(default=ItemType.WEAPON) + attack_range: Mapped[int] = mapped_column(nullable=False, info={"min": 0}, default=0) + attack_range_long: Mapped[int] = mapped_column(nullable=True, info={"min": 0}, default=None) + targets: Mapped[int] = mapped_column(nullable=False, info={"min": 1}, default=1) + martial: Mapped[bool] = mapped_column(default=False) + melee: Mapped[bool] = mapped_column(default=False) + ammunition: Mapped[bool] = mapped_column(default=False) + finesse: Mapped[bool] = mapped_column(default=False) + heavy: Mapped[bool] = mapped_column(default=False) + light: Mapped[bool] = mapped_column(default=False) + loading: Mapped[bool] = mapped_column(default=False) + reach: Mapped[bool] = mapped_column(default=False) + thrown: Mapped[bool] = mapped_column(default=False) + two_handed: Mapped[bool] = mapped_column(default=False) + versatile: Mapped[bool] = mapped_column(default=False) + silvered: Mapped[bool] = mapped_column(default=False) + adamantine: Mapped[bool] = mapped_column(default=False) + + @property + def ranged(self): + return self.attack_range > 0 diff --git a/src/ttfrog/db/schema/modifiers.py b/src/ttfrog/db/schema/modifiers.py index fc0632b..f16e2b5 100644 --- a/src/ttfrog/db/schema/modifiers.py +++ b/src/ttfrog/db/schema/modifiers.py @@ -66,6 +66,7 @@ class Modifier(BaseObject): name: Mapped[str] = mapped_column(nullable=False) target: Mapped[str] = mapped_column(nullable=False) stacks: Mapped[bool] = mapped_column(nullable=False, default=False) + requires_attunement: Mapped[bool] = mapped_column(nullable=False, default=False) absolute_value: Mapped[int] = mapped_column(nullable=True, default=None) multiply_value: Mapped[float] = mapped_column(nullable=True, default=None) multiply_attribute: Mapped[str] = mapped_column(nullable=True, default=None) @@ -174,6 +175,8 @@ class ModifierMixin: Returns the matching column if it was found, or None. """ + if attr_name.startswith('_'): + return None col = getattr(self.__table__.columns, f"_{attr_name}", None) if col is None: return None @@ -279,7 +282,7 @@ class ModifierMixin: self._get_modifiable_base(col.info.get("modifiable_base", col.name)), modifiable_class=col.info.get("modifiable_class", None), ) - raise AttributeError(f"No such attribute on {self.__class__.__name__} object: {attr_name}.") + raise AttributeError(f"{self.__class__.__name__} object does not have the attribute '{attr_name}'") class ConditionMixin: