tabletop-frog/src/ttfrog/db/schema/inventory.py
2024-09-21 15:59:40 -07:00

322 lines
9.8 KiB
Python

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)