322 lines
9.8 KiB
Python
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)
|