from typing import List 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 __all__ = [ "Item", "Spell", ] ITEM_TYPES = [ "ITEM", "SPELL", "SCROLL", "WEAPON", "ARMOR", "SHIELD", "CONTAINER", ] RECHARGE_TIMES = [ "short rest", "long rest", "dawn", ] COST_TYPES = ["Action", "Bonus Action", "Reaction"] RARITY = ["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"] ItemType = EnumField("ItemType", ((k, k) for k in ITEM_TYPES)) Rarity = EnumField("Rarity", ((k, k) for k in RARITY)) RechargeTime = EnumField("RechargeTime", ((k.replace(" ", "_").upper(), k) for k in RECHARGE_TIMES)) Cost = EnumField("Cost", ((k, k) for k in COST_TYPES)) def item_property_creator(fields): if isinstance(fields, list): for f in fields: yield f elif isinstance(fields, ItemProperty): return fields return ItemProperty(**fields) 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) charges: Mapped[int] = mapped_column(nullable=True, info={"min": 0}, default=None) recharge_time: Mapped[RechargeTime] = mapped_column(default=RechargeTime.LONG_REST) recharge_amount: Mapped[str] = mapped_column(String(collation="NOCASE"), default="1") _class_restrictions: Mapped[int] = mapped_column(ForeignKey("character_class.id"), nullable=True, default=None) class_restrictions: Mapped["CharacterClass"] = relationship(init=False) properties: Mapped[List["ItemProperty"]] = relationship( uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] ) # _spells: Mapped[int] = mapped_column(ForeignKey("spell.id"), nullable=True, default=None) # spells: Mapped["Spell"] = relationship(init=False) @property def has_charges(self): return self.charges is not None class Spell(Item): __tablename__ = "spell" __mapper_args__ = {"polymorphic_identity": ItemType.SPELL} id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False) item_type: Mapped[ItemType] = ItemType.SPELL level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0) concentration: Mapped[bool] = mapped_column(default=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) item_type: Mapped[ItemType] = ItemType.WEAPON damage_die: Mapped[str] = mapped_column(nullable=False, default="1d6") damage_type: Mapped[DamageType] = mapped_column(nullable=False, default=DamageType.slashing) 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) magical: Mapped[bool] = mapped_column(default=False) @property def ranged(self): return self.attack_range > 0 class Shield(Item): __tablename__ = "shield" __mapper_args__ = {"polymorphic_identity": ItemType.SHIELD} id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False) item_type: Mapped[ItemType] = ItemType.SHIELD class Armor(Item): __tablename__ = "armor" __mapper_args__ = {"polymorphic_identity": ItemType.ARMOR} id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False) item_type: Mapped[ItemType] = ItemType.ARMOR class ItemProperty(BaseObject): __tablename__ = "item_property" 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) charge_cost: Mapped[int] = mapped_column(nullable=True, info={"min": 0}, default=None) item_id: Mapped[int] = mapped_column(ForeignKey("item.id"), default=0) # action/reaction/bonus # modifiers?