reimplementing inventories

This commit is contained in:
evilchili
2024-09-21 15:59:40 -07:00
parent 926d2fdaf6
commit 1b9ff9b393
9 changed files with 438 additions and 301 deletions
+12 -1
View File
@@ -3,7 +3,7 @@ import enum
import nanoid
from nanoid_dictionary import human_alphabet
from slugify import slugify
from sqlalchemy import Column, String
from sqlalchemy import Column, String, inspect
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
@@ -53,6 +53,17 @@ class BaseObject(MappedAsDataclass, DeclarativeBase):
def __repr__(self):
return str(dict(self))
def copy(self):
self_as_dict = dict(self.__dict__)
self_as_dict.pop("_sa_instance_state")
mapper = inspect(self).mapper
for primary_key in mapper.primary_key:
self_as_dict.pop(primary_key.name)
for key in mapper.relationships.keys():
if key in self_as_dict:
self_as_dict.pop(key)
return self.__class__(**self_as_dict)
class EnumField(enum.Enum):
"""
+52 -29
View File
@@ -1,16 +1,16 @@
from dataclasses import dataclass
import itertools
from collections import defaultdict
from functools import cached_property
from typing import List
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.ext.declarative import declared_attr
from ttfrog.db.base import BaseObject, SlugMixin
from ttfrog.db.schema.classes import CharacterClass, ClassFeature
from ttfrog.db.schema.constants import DamageType, Defenses
from ttfrog.db.schema.inventory import Inventory, InventoryMap, InventoryType
from ttfrog.db.schema.constants import DamageType, Defenses, InventoryType
from ttfrog.db.schema.inventory import Inventory, InventoryMixin
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
from ttfrog.db.schema.skill import Skill
@@ -37,12 +37,6 @@ def skill_creator(fields):
return CharacterSkillMap(**fields)
def inventory_creator(fields):
if isinstance(fields, InventoryMap):
return fields
return InventoryMap(**fields)
def condition_creator(fields):
if isinstance(fields, CharacterConditionMap):
return fields
@@ -195,6 +189,32 @@ class CharacterConditionMap(BaseObject):
condition = relationship("Condition", lazy="immediate")
@dataclass
class InventoryMap(InventoryMixin):
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), unique=True)
@declared_attr
def character(cls) -> Mapped["Character"]:
return relationship("Character", default=None)
@property
def contents(self):
return self.inventory.contents
class CharacterItemInventory(BaseObject, InventoryMap):
__tablename__ = "character_item_inventory"
__item_class__ = "Item"
inventory_type: InventoryType = InventoryType.EQUIPMENT
class CharacterSpellInventory(BaseObject, InventoryMap):
__tablename__ = "character_spell_inventory"
__item_class__ = "Spell"
inventory_type: InventoryType = InventoryType.SPELL
class Character(BaseObject, SlugMixin, ModifierMixin):
__tablename__ = "character"
@@ -262,24 +282,31 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
_inventories: Mapped[List["Inventory"]] = relationship(
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
_equipment = relationship(
"CharacterItemInventory", uselist=False, cascade="all,delete,delete-orphan", lazy="immediate", back_populates="character"
)
_spells = relationship(
"CharacterSpellInventory",
uselist=False,
cascade="all,delete,delete-orphan",
lazy="immediate",
back_populates="character",
)
_hit_dice = relationship("HitDie", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
_spell_slots = relationship("SpellSlot", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
@cached_property
def inventories(self):
return dict([(inventory.inventory_type, inventory) for inventory in self._inventories])
@property
def equipment(self):
return self._equipment.inventory
@cached_property
@property
def spells(self):
return self.inventories[InventoryType.SPELL]
return self._spells.inventory
@property
def prepared_spells(self):
hashmap = dict([(mapping.item.name, mapping) for mapping in self.spells if mapping.prepared])
hashmap = dict([(mapping.item.name, mapping) for mapping in self.spells.contents if mapping.prepared])
return list(hashmap.values())
@property
@@ -333,10 +360,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
merge_modifiers(self.traits)
merge_modifiers(self.conditions)
for mapping in self.equipped_items:
for target, mods in mapping.item.modifiers.items():
for item in self.equipped_items:
for target, mods in item.modifiers.items():
for mod in mods:
if mod.requires_attunement and not mapping.attuned:
if mod.requires_attunement and not item.attuned:
continue
unified[target].append(mod)
@@ -398,17 +425,13 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def class_features(self):
return dict([(mapping.class_feature.name, mapping.option) for mapping in self.character_class_feature_map])
@cached_property
def equipment(self):
return self.inventories[InventoryType.EQUIPMENT]
@property
def equipped_items(self):
return [item for item in self.equipment if item.equipped]
return [item for item in self.equipment.contents if item.equipped]
@property
def attuned_items(self):
return [item for item in self.equipment if item.attuned]
return [item for item in self.equipment.contents if item.attuned]
def attune(self, mapping):
if mapping.attuned:
@@ -666,6 +689,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
):
self.add_skill(skill, proficient=False, expert=False)
self._inventories.append(Inventory(inventory_type=InventoryType.EQUIPMENT, character_id=self.id))
self._inventories.append(Inventory(inventory_type=InventoryType.SPELL, character_id=self.id))
self._equipment = CharacterItemInventory(character_id=self.id)
self._spells = CharacterSpellInventory(character_id=self.id)
session.add(self)
+6
View File
@@ -1,4 +1,5 @@
from enum import StrEnum, auto
from ttfrog.db.base import EnumField
class Conditions(StrEnum):
@@ -52,3 +53,8 @@ class Defenses(StrEnum):
resistant = auto()
immune = auto()
absorbs = auto()
class InventoryType(EnumField):
EQUIPMENT = "EQUIPMENT"
SPELL = "SPELL"
-36
View File
@@ -1,36 +0,0 @@
from typing import Union
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import base as sa_base
from sqlalchemy.orm import mapped_column, relationship
from ttfrog.db.schema.inventory import Inventory, InventoryMap, InventoryType
from ttfrog.db.schema.item import Item, ItemType
__all__ = [
"Container",
]
class Container(Item):
__tablename__ = "container"
__mapper_args__ = {"polymorphic_identity": ItemType.CONTAINER}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
item_type: Mapped[ItemType] = ItemType.CONTAINER
inventory: Mapped["Inventory"] = relationship(
cascade="all,delete,delete-orphan",
lazy="immediate",
default_factory=lambda: Inventory(inventory_type=InventoryType.EQUIPMENT),
)
def __contains__(self, obj: Union[InventoryMap, Item]):
return obj in self.inventory
def __iter__(self):
yield from self.inventory
def __getattr__(self, name: str):
if name == sa_base.DEFAULT_STATE_ATTR:
raise AttributeError()
return getattr(self.inventory, name)
+238 -163
View File
@@ -1,66 +1,257 @@
from typing import List, Union
from dataclasses import dataclass
from typing import List
from sqlalchemy import ForeignKey, UniqueConstraint
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, EnumField
from ttfrog.db.schema.item import Item, ItemProperty, ItemType
class InventoryType(EnumField):
EQUIPMENT = "EQUIPMENT"
SPELL = "SPELL"
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: [
ItemType.WEAPON,
ItemType.ARMOR,
ItemType.SHIELD,
ItemType.ITEM,
ItemType.SCROLL,
ItemType.CONTAINER,
prototypes.ItemType.WEAPON,
prototypes.ItemType.ARMOR,
prototypes.ItemType.SHIELD,
prototypes.ItemType.ITEM,
prototypes.ItemType.SCROLL,
prototypes.ItemType.CONTAINER,
],
InventoryType.SPELL: [ItemType.SPELL],
InventoryType.SPELL: [prototypes.ItemType.SPELL],
}
def inventory_map_creator(fields):
# if isinstance(fields, InventoryMap):
# if isinstance(fields, Item):
# return fields
# return InventoryMap(**fields)
return InventoryMap(**fields)
# return Item(**fields)
return Item(**fields)
class InventoryMap(BaseObject):
__tablename__ = "inventory_map"
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_id: Mapped[int] = mapped_column(ForeignKey("inventory.id"))
item_id: Mapped[int] = mapped_column(ForeignKey("item.id"))
item: Mapped["Item"] = relationship(uselist=False, lazy="immediate", viewonly=True, init=False)
inventory: Mapped["Inventory"] = relationship(uselist=False, viewonly=True, init=False)
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)
always_prepared: Mapped[bool] = mapped_column(default=False)
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]
@property
def prepared(self):
if self.item.item_type == ItemType.SPELL:
return self.equipped or self.always_prepared
def equip(self):
if self.equipped:
return False
@@ -73,19 +264,7 @@ class InventoryMap(BaseObject):
self.equipped = False
return True
def prepare(self):
if self.item.item_type != ItemType.SPELL:
return False
if self.item.level > 0 and not self.inventory.character.spell_slots_by_level[self.item.level]:
return False
return self.equip()
def unprepare(self):
if self.item.item_type != ItemType.SPELL:
return False
return self.unequip()
def use(self, item_property: ItemProperty, charges=None):
def use(self, item_property: prototypes.ItemProperty, charges=None):
if item_property.charge_cost is None:
return True
avail = self.charges_available
@@ -100,42 +279,22 @@ class InventoryMap(BaseObject):
def consume(self, count=1):
if count < 0:
return False
if not self.item.consumable:
if not self.prototype.consumable:
return False
if self.count < count:
return False
self.count -= count
if self.count == 0:
self.inventory.remove(self)
self.container.remove(self)
return 0
return self.count
def cast(self, level=0):
if self.item.item_type != ItemType.SPELL:
return False
if not self.prepared:
return False
if not level:
level = self.item.level
# cantrips
if level == 0:
return True
# expend the spell slot
avail = self.inventory.character.spell_slots_available[level]
if not avail:
return False
avail[0].expended = True
return True
def attune(self):
if self.attuned:
return False
if not self.item.requires_attunement:
if not self.requires_attunement:
return False
if len(self.inventory.character.attuned_items) >= 3:
if len(self.container.character.attuned_items) >= 3:
return False
self.attuned = True
return True
@@ -146,101 +305,17 @@ class InventoryMap(BaseObject):
self.attuned = False
return True
def move_to(self, inventory):
if inventory == self.inventory:
def move_to(self, target):
target_inventory = getattr(target, 'inventory', target)
if self.container == target_inventory:
return False
self.inventory.remove(self)
self.inventory = inventory
inventory.item_map.append(self)
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.item, name)
def __contains__(self, obj):
if self.item.item_type == ItemType.CONTAINER:
return obj in self.item.inventory
raise RuntimeException("Item {self.item.name} is not a container.")
class Inventory(BaseObject):
__tablename__ = "inventory"
__table_args__ = (UniqueConstraint("character_id", "container_id", "inventory_type"),)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
item_map: Mapped[List["InventoryMap"]] = 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)
container_id: Mapped[int] = mapped_column(ForeignKey("item.id"), nullable=True, default=None)
character = relationship("Character", init=False, viewonly=True, lazy="immediate")
container = relationship("Item", init=False, viewonly=True, lazy="immediate")
@property
def items(self):
return [mapping.item for mapping in self.item_map]
@property
def all_items(self):
def inventory_contents(inventory):
for mapping in inventory.item_map:
yield mapping
if mapping.item.item_type == ItemType.CONTAINER:
yield from inventory_contents(mapping.item.inventory)
yield from inventory_contents(self)
@property
def all_item_maps(self):
def inventory_map(inventory):
for mapping in inventory.item_map:
yield mapping
if mapping.item.item_type == ItemType.CONTAINER:
yield from inventory_map(mapping.item.inventory)
yield from inventory_map(self)
def get(self, item):
return self.get_all(item)[0]
def get_all(self, item):
return [mapping for mapping in self.all_item_maps if mapping.item == item]
def add(self, item):
if item.item_type not in inventory_type_map[self.inventory_type]:
return False
mapping = InventoryMap(inventory_id=self.id, item_id=item.id)
if item.consumable:
mapping.count = item.count
if item.charges:
mapping.charges = [Charge(inventory_map_id=mapping.id) for i in range(item.charges)]
self.item_map.append(mapping)
return mapping
def remove(self, mapping):
if mapping in self.item_map:
self.item_map.remove(mapping)
return True
return False
def __contains__(self, obj: Union[InventoryMap, Item]):
if isinstance(obj, InventoryMap):
item = obj.item
else:
item = obj
return item in [mapping.item for mapping in self.all_items]
def __iter__(self):
yield from self.all_items
class Charge(BaseObject):
__tablename__ = "charge"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
inventory_map_id: Mapped[int] = mapped_column(ForeignKey("inventory_map.id"))
expended: Mapped[bool] = mapped_column(nullable=False, default=False)
return getattr(self.prototype, name)
+4 -2
View File
@@ -180,7 +180,7 @@ class ModifierMixin:
col = getattr(self.__table__.columns, f"_{attr_name}", None)
if col is None:
return None
for key in col.info.keys():
for key in getattr(col, "info", {}).keys():
if key.startswith("modifiable"):
return col
return None # pragma: no cover
@@ -282,7 +282,9 @@ class ModifierMixin:
self._get_modifiable_base(col.info.get("modifiable_base", col.name)),
modifiable_class=col.info.get("modifiable_class", None),
)
raise AttributeError(f"{self.__class__.__name__} object does not have the attribute '{attr_name}'")
raise AttributeError(
f"{self.__class__.__name__} object either does not have the attribute '{attr_name}', or an error occurred when accessing it."
)
class ConditionMixin:
@@ -5,12 +5,20 @@ 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.constants import DamageType, InventoryType
from ttfrog.db.schema.modifiers import ModifierMixin
__all__ = [
"Item",
"Spell",
"ItemType",
"ItemProperty",
"Rarity",
"RechargeTime",
"Cost",
"BaseItem",
"BaseSpell",
"Armor",
"Shield",
"Weapon",
]
@@ -22,6 +30,7 @@ ITEM_TYPES = [
"ARMOR",
"SHIELD",
"CONTAINER",
"SPELLBOOK",
]
RECHARGE_TIMES = [
@@ -50,12 +59,13 @@ def item_property_creator(fields):
return ItemProperty(**fields)
class Item(BaseObject, ModifierMixin):
__tablename__ = "item"
class BaseItem(BaseObject, ModifierMixin):
__tablename__ = "item_prototype"
__inventory_item_class__ = "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)
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False)
description: Mapped[str] = mapped_column(String, nullable=True, default=None)
item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False)
@@ -79,25 +89,32 @@ class Item(BaseObject, ModifierMixin):
# _spells: Mapped[int] = mapped_column(ForeignKey("spell.id"), nullable=True, default=None)
# spells: Mapped["Spell"] = relationship(init=False)
# if this item is a container, set the inventory type
inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None)
@property
def has_charges(self):
return self.charges is not None
def __repr__(self):
return f"{self.__class__.__name__}(id={self.id}, name={self.name})"
class Spell(Item):
__tablename__ = "spell"
class BaseSpell(BaseItem):
__tablename__ = "spell_prototype"
__inventory_item_class__ = "Spell"
__mapper_args__ = {"polymorphic_identity": ItemType.SPELL}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
id: Mapped[int] = mapped_column(ForeignKey("item_prototype.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):
class Weapon(BaseItem):
__tablename__ = "weapon"
__mapper_args__ = {"polymorphic_identity": ItemType.WEAPON}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id"), primary_key=True, init=False)
item_type: Mapped[ItemType] = ItemType.WEAPON
damage_die: Mapped[str] = mapped_column(nullable=False, default="1d6")
@@ -125,17 +142,17 @@ class Weapon(Item):
return self.attack_range > 0
class Shield(Item):
class Shield(BaseItem):
__tablename__ = "shield"
__mapper_args__ = {"polymorphic_identity": ItemType.SHIELD}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id"), primary_key=True, init=False)
item_type: Mapped[ItemType] = ItemType.SHIELD
class Armor(Item):
class Armor(BaseItem):
__tablename__ = "armor"
__mapper_args__ = {"polymorphic_identity": ItemType.ARMOR}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id"), primary_key=True, init=False)
item_type: Mapped[ItemType] = ItemType.ARMOR
@@ -145,7 +162,7 @@ class ItemProperty(BaseObject):
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": 1}, default=None)
item_id: Mapped[int] = mapped_column(ForeignKey("item.id"), default=0)
item_prototype_id: Mapped[int] = mapped_column(ForeignKey("item_prototype.id"), default=0)
# action/reaction/bonus
# modifiers?