from dataclasses import dataclass from typing import List from pprint import pprint from sqlalchemy import ForeignKey from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import Mapped from sqlalchemy.orm import base as sa_base from sqlalchemy.orm import mapped_column, relationship from ttfrog.db.base import BaseObject from ttfrog.db.schema import prototypes from ttfrog.db.schema.constants import InventoryType from ttfrog.db.schema.modifiers import ModifierMixin inventory_type_map = { InventoryType.EQUIPMENT: [ prototypes.ItemType.WEAPON, prototypes.ItemType.ARMOR, prototypes.ItemType.SHIELD, prototypes.ItemType.ITEM, prototypes.ItemType.SCROLL, prototypes.ItemType.CONTAINER, ], InventoryType.SPELL: [prototypes.ItemType.SPELL], } def inventory_map_creator(fields): # if isinstance(fields, Item): # return fields # return Item(**fields) return Item(**fields) class Inventory(BaseObject): """ Creates a many-to-many between Items or Spells and any model inheriting from the InventoryMixin. """ __tablename__ = "inventory" id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) inventory_type: Mapped[InventoryType] = mapped_column(nullable=False) primary_table_name: Mapped[str] = mapped_column(nullable=False) primary_table_id: Mapped[int] = mapped_column(nullable=False) _item_contents: Mapped[List["Item"]] = relationship( uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] ) _spell_contents: Mapped[List["Spell"]] = relationship( uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] ) character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=True, default=None) character = relationship("Character", uselist=False, default=None) @property def contents(self): if self.inventory_type == InventoryType.SPELL: return self._spell_contents return self._item_contents @property def all_contents(self): def nested(obj): if hasattr(obj, "contents"): for mapping in obj.contents: yield mapping yield from nested(mapping) elif hasattr(obj, "inventory"): yield from nested(obj.inventory) yield from nested(self) def get(self, prototype): return self.get_all(prototype)[0] def get_all(self, prototype): return [mapping for mapping in self.all_contents if mapping.prototype == prototype] def add(self, prototype): if prototype.item_type not in inventory_type_map[self.inventory_type]: return False mapping = globals()[prototype.__inventory_item_class__](prototype_id=prototype.id) mapping.prototype = prototype if prototype.consumable: mapping.count = prototype.count if prototype.charges: mapping.charges = [Charge(item_id=mapping.id) for i in range(prototype.charges)] self.contents.append(mapping) return mapping def remove(self, mapping): if mapping in self.contents: self.contents.remove(mapping) return mapping return False def __contains__(self, obj): if isinstance(obj, prototypes.BaseItem): return obj in [mapping.prototype for mapping in self.all_contents] elif isinstance(obj, Item): return obj in self.all_contents def __iter__(self): yield from self.all_contents @dataclass class InventoryItemMixin: @declared_attr def container(cls) -> Mapped["Inventory"]: return relationship(uselist=False, viewonly=True, init=False) @declared_attr def _inventory_id(cls) -> Mapped[int]: return mapped_column(ForeignKey("inventory.id"), init=False) @dataclass class InventoryMixin: """ Add to a class to make it an inventory. """ @declared_attr def inventory(cls): """ Create the join between the current model and the ModifierMap table. """ return relationship( "Inventory", primaryjoin=( "and_(" f"foreign(Inventory.primary_table_name)=='{cls.__tablename__}', " f"foreign(Inventory.primary_table_id)=={cls.__name__}.id" ")" ), cascade="all,delete,delete-orphan", overlaps="inventory,inventory", single_parent=True, uselist=False, lazy="immediate", ) def __after_insert__(self, session): if self.inventory_type: self.inventory = Inventory( inventory_type=self.inventory_type, primary_table_name=self.__tablename__, primary_table_id=self.id, character_id=getattr(self, "character_id", None) ) session.add(self) def __contains__(self, obj): return obj in self.inventory class Spell(BaseObject, InventoryItemMixin): __tablename__ = "spell" id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) prototype_id: Mapped[int] = mapped_column(ForeignKey("spell_prototype.id")) always_prepared: Mapped[bool] = mapped_column(default=False) _prepared: Mapped[bool] = mapped_column(init=False, default=False) prototype: Mapped["prototypes.BaseSpell"] = relationship(uselist=False, lazy="immediate", init=False) @property def spell(self): return self.prototype @property def prepared(self): return self._prepared or self.always_prepared def prepare(self): if self.prototype.level > 0 and not self.container.character.spell_slots_by_level[self.prototype.level]: return False self._prepared = True return True def unprepare(self): if self.prepared: self._prepared = False return True return False def cast(self, level=0): if not self.prepared: return False if not level: level = self.prototype.level # cantrips if level == 0: return True # expend the spell slot avail = self.container.character.spell_slots_available[level] if not avail: return False avail[0].expended = True return True class Charge(BaseObject): __tablename__ = "charge" id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) item_id: Mapped[int] = mapped_column(ForeignKey("item.id")) expended: Mapped[bool] = mapped_column(nullable=False, default=False) class Item(BaseObject, InventoryMixin, InventoryItemMixin, ModifierMixin): __tablename__ = "item" id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) prototype_id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id")) equipped: Mapped[bool] = mapped_column(default=False) attuned: Mapped[bool] = mapped_column(default=False) count: Mapped[int] = mapped_column(nullable=False, default=1) charges: Mapped[List["Charge"]] = relationship( uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] ) _inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None) @property def inventory_type(self): if self._inventory_type: return self._inventory_type elif self.prototype: return self.prototype.inventory_type prototype: Mapped["prototypes.BaseItem"] = relationship(uselist=False, lazy="immediate", init=False) @property def modifiers(self): return self.prototype.modifiers @property def charges_available(self): return [charge for charge in self.charges if not charge.expended] def equip(self): if self.equipped: return False self.equipped = True return True def unequip(self): if not self.equipped: return False self.equipped = False return True def use(self, item_property: prototypes.ItemProperty, charges=None): if item_property.charge_cost is None: return True avail = self.charges_available if charges is None: charges = item_property.charge_cost if len(avail) < charges: return False for charge in avail[:charges]: charge.expended = True return True def consume(self, count=1): if count < 0: return False if not self.prototype.consumable: return False if self.count < count: return False self.count -= count if self.count == 0: self.container.remove(self) return 0 return self.count def attune(self): if self.attuned: return False if not self.requires_attunement: return False if len(self.container.character.attuned_items) >= 3: return False self.attuned = True return True def unattune(self): if not self.attuned: return False self.attuned = False return True def move_to(self, target): target_inventory = getattr(target, 'inventory', target) if self.container == target_inventory: return False self.container.contents.remove(self) self.container = target_inventory self.container.id = target_inventory.id target_inventory.contents.append(self) return True def __getattr__(self, name: str): if name == sa_base.DEFAULT_STATE_ATTR: raise AttributeError() return getattr(self.prototype, name)