reimplementing inventories
This commit is contained in:
parent
926d2fdaf6
commit
1b9ff9b393
|
@ -3,7 +3,7 @@ import enum
|
||||||
import nanoid
|
import nanoid
|
||||||
from nanoid_dictionary import human_alphabet
|
from nanoid_dictionary import human_alphabet
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String, inspect
|
||||||
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
|
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +53,17 @@ class BaseObject(MappedAsDataclass, DeclarativeBase):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(dict(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):
|
class EnumField(enum.Enum):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
import itertools
|
import itertools
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import cached_property
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
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.base import BaseObject, SlugMixin
|
||||||
from ttfrog.db.schema.classes import CharacterClass, ClassFeature
|
from ttfrog.db.schema.classes import CharacterClass, ClassFeature
|
||||||
from ttfrog.db.schema.constants import DamageType, Defenses
|
from ttfrog.db.schema.constants import DamageType, Defenses, InventoryType
|
||||||
from ttfrog.db.schema.inventory import Inventory, InventoryMap, InventoryType
|
from ttfrog.db.schema.inventory import Inventory, InventoryMixin
|
||||||
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
||||||
from ttfrog.db.schema.skill import Skill
|
from ttfrog.db.schema.skill import Skill
|
||||||
|
|
||||||
|
@ -37,12 +37,6 @@ def skill_creator(fields):
|
||||||
return CharacterSkillMap(**fields)
|
return CharacterSkillMap(**fields)
|
||||||
|
|
||||||
|
|
||||||
def inventory_creator(fields):
|
|
||||||
if isinstance(fields, InventoryMap):
|
|
||||||
return fields
|
|
||||||
return InventoryMap(**fields)
|
|
||||||
|
|
||||||
|
|
||||||
def condition_creator(fields):
|
def condition_creator(fields):
|
||||||
if isinstance(fields, CharacterConditionMap):
|
if isinstance(fields, CharacterConditionMap):
|
||||||
return fields
|
return fields
|
||||||
|
@ -195,6 +189,32 @@ class CharacterConditionMap(BaseObject):
|
||||||
condition = relationship("Condition", lazy="immediate")
|
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):
|
class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
__tablename__ = "character"
|
__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_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
|
||||||
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
|
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
|
||||||
|
|
||||||
_inventories: Mapped[List["Inventory"]] = relationship(
|
_equipment = relationship(
|
||||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
"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")
|
_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")
|
_spell_slots = relationship("SpellSlot", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def inventories(self):
|
def equipment(self):
|
||||||
return dict([(inventory.inventory_type, inventory) for inventory in self._inventories])
|
return self._equipment.inventory
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def spells(self):
|
def spells(self):
|
||||||
return self.inventories[InventoryType.SPELL]
|
return self._spells.inventory
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prepared_spells(self):
|
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())
|
return list(hashmap.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -333,10 +360,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
merge_modifiers(self.traits)
|
merge_modifiers(self.traits)
|
||||||
merge_modifiers(self.conditions)
|
merge_modifiers(self.conditions)
|
||||||
|
|
||||||
for mapping in self.equipped_items:
|
for item in self.equipped_items:
|
||||||
for target, mods in mapping.item.modifiers.items():
|
for target, mods in item.modifiers.items():
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
if mod.requires_attunement and not mapping.attuned:
|
if mod.requires_attunement and not item.attuned:
|
||||||
continue
|
continue
|
||||||
unified[target].append(mod)
|
unified[target].append(mod)
|
||||||
|
|
||||||
|
@ -398,17 +425,13 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
def class_features(self):
|
def class_features(self):
|
||||||
return dict([(mapping.class_feature.name, mapping.option) for mapping in self.character_class_feature_map])
|
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
|
@property
|
||||||
def equipped_items(self):
|
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
|
@property
|
||||||
def attuned_items(self):
|
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):
|
def attune(self, mapping):
|
||||||
if mapping.attuned:
|
if mapping.attuned:
|
||||||
|
@ -666,6 +689,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
):
|
):
|
||||||
self.add_skill(skill, proficient=False, expert=False)
|
self.add_skill(skill, proficient=False, expert=False)
|
||||||
|
|
||||||
self._inventories.append(Inventory(inventory_type=InventoryType.EQUIPMENT, character_id=self.id))
|
self._equipment = CharacterItemInventory(character_id=self.id)
|
||||||
self._inventories.append(Inventory(inventory_type=InventoryType.SPELL, character_id=self.id))
|
self._spells = CharacterSpellInventory(character_id=self.id)
|
||||||
session.add(self)
|
session.add(self)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from enum import StrEnum, auto
|
from enum import StrEnum, auto
|
||||||
|
from ttfrog.db.base import EnumField
|
||||||
|
|
||||||
|
|
||||||
class Conditions(StrEnum):
|
class Conditions(StrEnum):
|
||||||
|
@ -52,3 +53,8 @@ class Defenses(StrEnum):
|
||||||
resistant = auto()
|
resistant = auto()
|
||||||
immune = auto()
|
immune = auto()
|
||||||
absorbs = auto()
|
absorbs = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryType(EnumField):
|
||||||
|
EQUIPMENT = "EQUIPMENT"
|
||||||
|
SPELL = "SPELL"
|
||||||
|
|
|
@ -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)
|
|
|
@ -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 Mapped
|
||||||
from sqlalchemy.orm import base as sa_base
|
from sqlalchemy.orm import base as sa_base
|
||||||
from sqlalchemy.orm import mapped_column, relationship
|
from sqlalchemy.orm import mapped_column, relationship
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject, EnumField
|
from ttfrog.db.base import BaseObject
|
||||||
from ttfrog.db.schema.item import Item, ItemProperty, ItemType
|
from ttfrog.db.schema import prototypes
|
||||||
|
from ttfrog.db.schema.constants import InventoryType
|
||||||
|
from ttfrog.db.schema.modifiers import ModifierMixin
|
||||||
class InventoryType(EnumField):
|
|
||||||
EQUIPMENT = "EQUIPMENT"
|
|
||||||
SPELL = "SPELL"
|
|
||||||
|
|
||||||
|
|
||||||
inventory_type_map = {
|
inventory_type_map = {
|
||||||
InventoryType.EQUIPMENT: [
|
InventoryType.EQUIPMENT: [
|
||||||
ItemType.WEAPON,
|
prototypes.ItemType.WEAPON,
|
||||||
ItemType.ARMOR,
|
prototypes.ItemType.ARMOR,
|
||||||
ItemType.SHIELD,
|
prototypes.ItemType.SHIELD,
|
||||||
ItemType.ITEM,
|
prototypes.ItemType.ITEM,
|
||||||
ItemType.SCROLL,
|
prototypes.ItemType.SCROLL,
|
||||||
ItemType.CONTAINER,
|
prototypes.ItemType.CONTAINER,
|
||||||
],
|
],
|
||||||
InventoryType.SPELL: [ItemType.SPELL],
|
InventoryType.SPELL: [prototypes.ItemType.SPELL],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def inventory_map_creator(fields):
|
def inventory_map_creator(fields):
|
||||||
# if isinstance(fields, InventoryMap):
|
# if isinstance(fields, Item):
|
||||||
# return fields
|
# return fields
|
||||||
# return InventoryMap(**fields)
|
# return Item(**fields)
|
||||||
return InventoryMap(**fields)
|
return Item(**fields)
|
||||||
|
|
||||||
|
|
||||||
class InventoryMap(BaseObject):
|
class Inventory(BaseObject):
|
||||||
__tablename__ = "inventory_map"
|
"""
|
||||||
|
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)
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||||
inventory_id: Mapped[int] = mapped_column(ForeignKey("inventory.id"))
|
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
|
||||||
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)
|
|
||||||
|
|
||||||
|
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)
|
equipped: Mapped[bool] = mapped_column(default=False)
|
||||||
attuned: Mapped[bool] = mapped_column(default=False)
|
attuned: Mapped[bool] = mapped_column(default=False)
|
||||||
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
||||||
|
|
||||||
always_prepared: Mapped[bool] = mapped_column(default=False)
|
|
||||||
|
|
||||||
charges: Mapped[List["Charge"]] = relationship(
|
charges: Mapped[List["Charge"]] = relationship(
|
||||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
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
|
@property
|
||||||
def charges_available(self):
|
def charges_available(self):
|
||||||
return [charge for charge in self.charges if not charge.expended]
|
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):
|
def equip(self):
|
||||||
if self.equipped:
|
if self.equipped:
|
||||||
return False
|
return False
|
||||||
|
@ -73,19 +264,7 @@ class InventoryMap(BaseObject):
|
||||||
self.equipped = False
|
self.equipped = False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def prepare(self):
|
def use(self, item_property: prototypes.ItemProperty, charges=None):
|
||||||
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):
|
|
||||||
if item_property.charge_cost is None:
|
if item_property.charge_cost is None:
|
||||||
return True
|
return True
|
||||||
avail = self.charges_available
|
avail = self.charges_available
|
||||||
|
@ -100,42 +279,22 @@ class InventoryMap(BaseObject):
|
||||||
def consume(self, count=1):
|
def consume(self, count=1):
|
||||||
if count < 0:
|
if count < 0:
|
||||||
return False
|
return False
|
||||||
if not self.item.consumable:
|
if not self.prototype.consumable:
|
||||||
return False
|
return False
|
||||||
if self.count < count:
|
if self.count < count:
|
||||||
return False
|
return False
|
||||||
self.count -= count
|
self.count -= count
|
||||||
if self.count == 0:
|
if self.count == 0:
|
||||||
self.inventory.remove(self)
|
self.container.remove(self)
|
||||||
return 0
|
return 0
|
||||||
return self.count
|
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):
|
def attune(self):
|
||||||
if self.attuned:
|
if self.attuned:
|
||||||
return False
|
return False
|
||||||
if not self.item.requires_attunement:
|
if not self.requires_attunement:
|
||||||
return False
|
return False
|
||||||
if len(self.inventory.character.attuned_items) >= 3:
|
if len(self.container.character.attuned_items) >= 3:
|
||||||
return False
|
return False
|
||||||
self.attuned = True
|
self.attuned = True
|
||||||
return True
|
return True
|
||||||
|
@ -146,101 +305,17 @@ class InventoryMap(BaseObject):
|
||||||
self.attuned = False
|
self.attuned = False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def move_to(self, inventory):
|
def move_to(self, target):
|
||||||
if inventory == self.inventory:
|
target_inventory = getattr(target, 'inventory', target)
|
||||||
|
if self.container == target_inventory:
|
||||||
return False
|
return False
|
||||||
self.inventory.remove(self)
|
self.container.contents.remove(self)
|
||||||
self.inventory = inventory
|
self.container = target_inventory
|
||||||
inventory.item_map.append(self)
|
self.container.id = target_inventory.id
|
||||||
|
target_inventory.contents.append(self)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __getattr__(self, name: str):
|
def __getattr__(self, name: str):
|
||||||
if name == sa_base.DEFAULT_STATE_ATTR:
|
if name == sa_base.DEFAULT_STATE_ATTR:
|
||||||
raise AttributeError()
|
raise AttributeError()
|
||||||
return getattr(self.item, name)
|
return getattr(self.prototype, 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)
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ class ModifierMixin:
|
||||||
col = getattr(self.__table__.columns, f"_{attr_name}", None)
|
col = getattr(self.__table__.columns, f"_{attr_name}", None)
|
||||||
if col is None:
|
if col is None:
|
||||||
return None
|
return None
|
||||||
for key in col.info.keys():
|
for key in getattr(col, "info", {}).keys():
|
||||||
if key.startswith("modifiable"):
|
if key.startswith("modifiable"):
|
||||||
return col
|
return col
|
||||||
return None # pragma: no cover
|
return None # pragma: no cover
|
||||||
|
@ -282,7 +282,9 @@ class ModifierMixin:
|
||||||
self._get_modifiable_base(col.info.get("modifiable_base", col.name)),
|
self._get_modifiable_base(col.info.get("modifiable_base", col.name)),
|
||||||
modifiable_class=col.info.get("modifiable_class", None),
|
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:
|
class ConditionMixin:
|
||||||
|
|
|
@ -5,12 +5,20 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject, EnumField
|
from ttfrog.db.base import BaseObject, EnumField
|
||||||
from ttfrog.db.schema.classes import CharacterClass
|
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
|
from ttfrog.db.schema.modifiers import ModifierMixin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Item",
|
"ItemType",
|
||||||
"Spell",
|
"ItemProperty",
|
||||||
|
"Rarity",
|
||||||
|
"RechargeTime",
|
||||||
|
"Cost",
|
||||||
|
"BaseItem",
|
||||||
|
"BaseSpell",
|
||||||
|
"Armor",
|
||||||
|
"Shield",
|
||||||
|
"Weapon",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +30,7 @@ ITEM_TYPES = [
|
||||||
"ARMOR",
|
"ARMOR",
|
||||||
"SHIELD",
|
"SHIELD",
|
||||||
"CONTAINER",
|
"CONTAINER",
|
||||||
|
"SPELLBOOK",
|
||||||
]
|
]
|
||||||
|
|
||||||
RECHARGE_TIMES = [
|
RECHARGE_TIMES = [
|
||||||
|
@ -50,12 +59,13 @@ def item_property_creator(fields):
|
||||||
return ItemProperty(**fields)
|
return ItemProperty(**fields)
|
||||||
|
|
||||||
|
|
||||||
class Item(BaseObject, ModifierMixin):
|
class BaseItem(BaseObject, ModifierMixin):
|
||||||
__tablename__ = "item"
|
__tablename__ = "item_prototype"
|
||||||
|
__inventory_item_class__ = "Item"
|
||||||
__mapper_args__ = {"polymorphic_identity": ItemType.ITEM, "polymorphic_on": "item_type"}
|
__mapper_args__ = {"polymorphic_identity": ItemType.ITEM, "polymorphic_on": "item_type"}
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
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)
|
description: Mapped[str] = mapped_column(String, nullable=True, default=None)
|
||||||
|
|
||||||
item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False)
|
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[int] = mapped_column(ForeignKey("spell.id"), nullable=True, default=None)
|
||||||
# spells: Mapped["Spell"] = relationship(init=False)
|
# 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
|
@property
|
||||||
def has_charges(self):
|
def has_charges(self):
|
||||||
return self.charges is not None
|
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}
|
__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
|
item_type: Mapped[ItemType] = ItemType.SPELL
|
||||||
|
|
||||||
level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0)
|
level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0)
|
||||||
concentration: Mapped[bool] = mapped_column(default=False)
|
concentration: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
|
|
||||||
class Weapon(Item):
|
class Weapon(BaseItem):
|
||||||
__tablename__ = "weapon"
|
__tablename__ = "weapon"
|
||||||
__mapper_args__ = {"polymorphic_identity": ItemType.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
|
item_type: Mapped[ItemType] = ItemType.WEAPON
|
||||||
|
|
||||||
damage_die: Mapped[str] = mapped_column(nullable=False, default="1d6")
|
damage_die: Mapped[str] = mapped_column(nullable=False, default="1d6")
|
||||||
|
@ -125,17 +142,17 @@ class Weapon(Item):
|
||||||
return self.attack_range > 0
|
return self.attack_range > 0
|
||||||
|
|
||||||
|
|
||||||
class Shield(Item):
|
class Shield(BaseItem):
|
||||||
__tablename__ = "shield"
|
__tablename__ = "shield"
|
||||||
__mapper_args__ = {"polymorphic_identity": ItemType.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
|
item_type: Mapped[ItemType] = ItemType.SHIELD
|
||||||
|
|
||||||
|
|
||||||
class Armor(Item):
|
class Armor(BaseItem):
|
||||||
__tablename__ = "armor"
|
__tablename__ = "armor"
|
||||||
__mapper_args__ = {"polymorphic_identity": ItemType.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
|
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)
|
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True)
|
||||||
description: Mapped[str] = mapped_column(String, nullable=True, default=None)
|
description: Mapped[str] = mapped_column(String, nullable=True, default=None)
|
||||||
charge_cost: Mapped[int] = mapped_column(nullable=True, info={"min": 1}, 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
|
# action/reaction/bonus
|
||||||
# modifiers?
|
# modifiers?
|
|
@ -1,5 +1,19 @@
|
||||||
from ttfrog.db.schema.container import Container
|
from ttfrog.db.schema import prototypes
|
||||||
from ttfrog.db.schema.item import Item, ItemType, Spell
|
from ttfrog.db.schema.inventory import InventoryType
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
def test_spell_inventory(db, carl):
|
||||||
|
with db.transaction():
|
||||||
|
fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False)
|
||||||
|
db.add_or_update([fireball, carl])
|
||||||
|
|
||||||
|
assert carl.spells.add(fireball)
|
||||||
|
db.add_or_update(carl)
|
||||||
|
|
||||||
|
assert not carl.equipment.add(fireball)
|
||||||
|
assert fireball in carl.spells
|
||||||
|
assert fireball not in carl.equipment
|
||||||
|
|
||||||
|
|
||||||
def test_equipment_inventory(db, carl):
|
def test_equipment_inventory(db, carl):
|
||||||
|
@ -8,21 +22,18 @@ def test_equipment_inventory(db, carl):
|
||||||
db.add_or_update(carl)
|
db.add_or_update(carl)
|
||||||
|
|
||||||
# create some items
|
# create some items
|
||||||
ten_foot_pole = Item(name="10ft. Pole", item_type=ItemType.ITEM, consumable=False)
|
ten_foot_pole = prototypes.BaseItem(name="10ft. Pole", item_type=prototypes.ItemType.ITEM, consumable=False)
|
||||||
fireball = Spell(name="Fireball", level=3, concentration=False)
|
db.add_or_update(ten_foot_pole)
|
||||||
db.add_or_update([ten_foot_pole, fireball])
|
|
||||||
|
|
||||||
# add the pole to carl's equipment, and the spell to his spell list.
|
# add the pole to carl's equipment, and the spell to his spell list.
|
||||||
assert carl.equipment.add(ten_foot_pole)
|
assert carl.equipment.add(ten_foot_pole)
|
||||||
assert carl.spells.add(fireball)
|
|
||||||
|
|
||||||
# can't mix and match inventory item types
|
# can't mix and match inventory item types
|
||||||
assert not carl.equipment.add(fireball)
|
|
||||||
assert not carl.spells.add(ten_foot_pole)
|
assert not carl.spells.add(ten_foot_pole)
|
||||||
|
|
||||||
# add two more 10 foot poles. You can never have too many.
|
# add two more 10 foot poles. You can never have too many.
|
||||||
carl.equipment.add(ten_foot_pole)
|
assert carl.equipment.add(ten_foot_pole)
|
||||||
carl.equipment.add(ten_foot_pole)
|
assert carl.equipment.add(ten_foot_pole)
|
||||||
db.add_or_update(carl)
|
db.add_or_update(carl)
|
||||||
|
|
||||||
all_carls_poles = carl.equipment.get_all(ten_foot_pole)
|
all_carls_poles = carl.equipment.get_all(ten_foot_pole)
|
||||||
|
@ -31,8 +42,6 @@ def test_equipment_inventory(db, carl):
|
||||||
# check the "contains" logic
|
# check the "contains" logic
|
||||||
assert ten_foot_pole in carl.equipment
|
assert ten_foot_pole in carl.equipment
|
||||||
assert ten_foot_pole not in carl.spells
|
assert ten_foot_pole not in carl.spells
|
||||||
assert fireball in carl.spells
|
|
||||||
assert fireball not in carl.equipment
|
|
||||||
|
|
||||||
pole_one = all_carls_poles[0]
|
pole_one = all_carls_poles[0]
|
||||||
|
|
||||||
|
@ -42,11 +51,6 @@ def test_equipment_inventory(db, carl):
|
||||||
# can't equip it twice
|
# can't equip it twice
|
||||||
assert not pole_one.equip()
|
assert not pole_one.equip()
|
||||||
|
|
||||||
# can't prepare or cast an item
|
|
||||||
assert not pole_one.prepare()
|
|
||||||
assert not pole_one.unprepare()
|
|
||||||
assert not pole_one.cast()
|
|
||||||
|
|
||||||
# not consumable or attunable
|
# not consumable or attunable
|
||||||
assert not pole_one.consume()
|
assert not pole_one.consume()
|
||||||
assert not pole_one.attune()
|
assert not pole_one.attune()
|
||||||
|
@ -60,6 +64,7 @@ def test_equipment_inventory(db, carl):
|
||||||
# drop one pole
|
# drop one pole
|
||||||
assert carl.equipment.remove(pole_one)
|
assert carl.equipment.remove(pole_one)
|
||||||
assert ten_foot_pole in carl.equipment
|
assert ten_foot_pole in carl.equipment
|
||||||
|
return
|
||||||
|
|
||||||
# drop the remaining poles
|
# drop the remaining poles
|
||||||
assert carl.equipment.remove(all_carls_poles[1])
|
assert carl.equipment.remove(all_carls_poles[1])
|
||||||
|
@ -72,10 +77,12 @@ def test_equipment_inventory(db, carl):
|
||||||
|
|
||||||
def test_inventory_bundles(db, carl):
|
def test_inventory_bundles(db, carl):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
arrows = Item(name="Arrows", item_type=ItemType.ITEM, consumable=True, count=20)
|
arrows = prototypes.BaseItem(name="Arrows", item_type=prototypes.ItemType.ITEM, consumable=True, count=20)
|
||||||
db.add_or_update([carl, arrows])
|
db.add_or_update([carl, arrows])
|
||||||
quiver = carl.equipment.add(arrows)
|
quiver = carl.equipment.add(arrows)
|
||||||
db.add_or_update(carl)
|
db.add_or_update([carl, quiver])
|
||||||
|
|
||||||
|
assert quiver.container == carl.equipment
|
||||||
|
|
||||||
# full quiver
|
# full quiver
|
||||||
assert arrows in carl.equipment
|
assert arrows in carl.equipment
|
||||||
|
@ -98,8 +105,8 @@ def test_inventory_bundles(db, carl):
|
||||||
|
|
||||||
def test_spell_slots(db, carl, wizard):
|
def test_spell_slots(db, carl, wizard):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
prestidigitation = Spell(name="Prestidigitation", level=0, concentration=False)
|
prestidigitation = prototypes.BaseSpell(name="Prestidigitation", level=0, concentration=False)
|
||||||
fireball = Spell(name="Fireball", level=3, concentration=False)
|
fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False)
|
||||||
db.add_or_update([carl, prestidigitation, fireball])
|
db.add_or_update([carl, prestidigitation, fireball])
|
||||||
|
|
||||||
carl.spells.add(prestidigitation)
|
carl.spells.add(prestidigitation)
|
||||||
|
@ -160,45 +167,79 @@ def test_spell_slots(db, carl, wizard):
|
||||||
|
|
||||||
def test_containers(db, carl):
|
def test_containers(db, carl):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
ten_foot_pole = Item(name="10ft. Pole")
|
ten_foot_pole = prototypes.BaseItem(name="10ft. Pole")
|
||||||
rope = Item(name="50 ft. of Rope", consumable=True, count=50)
|
coil_of_rope = prototypes.BaseItem(name="50 ft. of Rope", consumable=True, count=50)
|
||||||
bag_of_holding = Container(name="Bag of Holding")
|
bag_of_holding = prototypes.BaseItem(name="Bag of Holding", inventory_type=InventoryType.EQUIPMENT)
|
||||||
db.add_or_update([carl, ten_foot_pole, rope, bag_of_holding])
|
db.add_or_update([ten_foot_pole, coil_of_rope, bag_of_holding])
|
||||||
|
|
||||||
|
pole = carl.equipment.add(ten_foot_pole)
|
||||||
|
rope = carl.equipment.add(coil_of_rope)
|
||||||
|
bag = carl.equipment.add(bag_of_holding)
|
||||||
|
db.add_or_update(carl)
|
||||||
|
|
||||||
|
# verify the bag of holding's inventory is created automatically.
|
||||||
|
assert bag.inventory_type is not None
|
||||||
|
assert bag.inventory is not None
|
||||||
|
|
||||||
|
# the existing instances are found using the get() method
|
||||||
|
assert carl.equipment.get(bag_of_holding) == bag
|
||||||
|
assert carl.equipment.get(ten_foot_pole) == pole
|
||||||
|
assert carl.equipment.get(coil_of_rope) == rope
|
||||||
|
|
||||||
|
# backreferences are populated correctly
|
||||||
|
assert pole.container == carl.equipment
|
||||||
|
|
||||||
# add some items to the bag of holding
|
# add some items to the bag of holding
|
||||||
assert bag_of_holding.add(ten_foot_pole)
|
assert pole.move_to(bag)
|
||||||
assert bag_of_holding.add(rope)
|
assert rope.move_to(bag)
|
||||||
db.add_or_update(bag_of_holding)
|
|
||||||
|
|
||||||
pole_from_bag = bag_of_holding.get(ten_foot_pole)
|
assert pole.container.id == bag.inventory.id
|
||||||
rope_from_bag = bag_of_holding.get(rope)
|
assert pole.container == bag.inventory
|
||||||
|
assert pole in bag.inventory
|
||||||
|
assert pole in bag
|
||||||
|
|
||||||
assert pole_from_bag.item == ten_foot_pole
|
assert pole not in carl.equipment.contents
|
||||||
assert pole_from_bag in bag_of_holding
|
assert pole.container == bag.inventory
|
||||||
assert pole_from_bag not in carl.equipment
|
|
||||||
|
|
||||||
# add the bag of holding to carl's equipment
|
pole_from_bag = bag.inventory.get(ten_foot_pole)
|
||||||
assert carl.equipment.add(bag_of_holding)
|
rope_from_bag = bag.inventory.get(coil_of_rope)
|
||||||
db.add_or_update(bag_of_holding)
|
|
||||||
|
assert pole_from_bag.prototype == ten_foot_pole
|
||||||
|
assert pole_from_bag in bag
|
||||||
|
|
||||||
|
bag_inventory_size = 2 # one pole, one rope
|
||||||
|
equipment_size = 1 # one bag
|
||||||
|
|
||||||
|
# one bag, one pole, one rope
|
||||||
|
total_inventory_size = bag_inventory_size + equipment_size
|
||||||
|
|
||||||
|
pprint(list(carl.equipment.all_contents))
|
||||||
|
|
||||||
|
assert len(list(bag.inventory.contents)) == bag_inventory_size
|
||||||
|
assert len(list(carl.equipment.contents)) == equipment_size
|
||||||
|
assert len(list(carl.equipment.all_contents)) == total_inventory_size
|
||||||
|
|
||||||
|
# nested containers!
|
||||||
assert pole_from_bag in carl.equipment
|
assert pole_from_bag in carl.equipment
|
||||||
assert rope_from_bag in carl.equipment
|
assert rope_from_bag in carl.equipment
|
||||||
|
|
||||||
# test equality of mappings
|
# test equality of mappings
|
||||||
carls_bag = carl.equipment.get(bag_of_holding)
|
carls_bag = carl.equipment.get(bag_of_holding)
|
||||||
carls_pole = carl.equipment.get(ten_foot_pole)
|
carls_pole = carl.equipment.get(ten_foot_pole)
|
||||||
carls_rope = carl.equipment.get(rope)
|
carls_rope = carl.equipment.get(coil_of_rope)
|
||||||
assert carls_pole == pole_from_bag
|
assert carls_pole == pole_from_bag
|
||||||
assert carls_rope == rope_from_bag
|
assert carls_rope == rope_from_bag
|
||||||
|
|
||||||
# use some rope
|
# use some rope
|
||||||
carls_rope.consume(10)
|
assert carls_rope.consume(10)
|
||||||
assert carls_rope.count == 40
|
assert carls_rope.count == 40
|
||||||
|
|
||||||
# move the rope out of the bag of holding, but not the pole
|
# move the rope out of the bag of holding, but not the pole
|
||||||
|
assert carls_rope in carls_bag
|
||||||
assert carls_rope.move_to(carl.equipment)
|
assert carls_rope.move_to(carl.equipment)
|
||||||
assert carls_rope not in carls_bag
|
assert carls_rope not in carls_bag
|
||||||
|
|
||||||
assert carls_pole in carls_bag
|
assert carls_pole in carls_bag
|
||||||
db.add_or_update(carl)
|
|
||||||
|
|
||||||
# get the db record anew, in case the in-memory representation isn't
|
# get the db record anew, in case the in-memory representation isn't
|
||||||
# what's recorded in the database. Then make sure we didn't break
|
# what's recorded in the database. Then make sure we didn't break
|
||||||
|
@ -213,6 +254,5 @@ def test_containers(db, carl):
|
||||||
|
|
||||||
# use the rest of the rope
|
# use the rest of the rope
|
||||||
assert carls_rope.consume(40) == 0
|
assert carls_rope.consume(40) == 0
|
||||||
print(rope_from_bag.inventory)
|
|
||||||
assert rope_from_bag not in carl.equipment
|
assert rope_from_bag not in carl.equipment
|
||||||
assert rope_from_bag not in carl.equipment.get(bag_of_holding)
|
assert rope_from_bag not in carl.equipment.get(bag_of_holding)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
from ttfrog.db.schema import prototypes
|
||||||
from ttfrog.db.schema.constants import DamageType, Defenses
|
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||||
from ttfrog.db.schema.item import Armor, Item, ItemProperty, Rarity, RechargeTime, Shield, Weapon
|
|
||||||
from ttfrog.db.schema.modifiers import Modifier
|
from ttfrog.db.schema.modifiers import Modifier
|
||||||
|
from ttfrog.db.schema.prototypes import Weapon
|
||||||
|
|
||||||
|
|
||||||
def test_weapons(db):
|
def test_weapons(db):
|
||||||
|
@ -28,7 +29,6 @@ def test_weapons(db):
|
||||||
attack_range=20,
|
attack_range=20,
|
||||||
attack_range_long=60,
|
attack_range_long=60,
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add_or_update([longbow, dagger])
|
db.add_or_update([longbow, dagger])
|
||||||
|
|
||||||
assert longbow.martial
|
assert longbow.martial
|
||||||
|
@ -42,7 +42,7 @@ def test_weapons(db):
|
||||||
|
|
||||||
def test_charges(db, carl):
|
def test_charges(db, carl):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
for_the_lulz = ItemProperty(
|
for_the_lulz = prototypes.ItemProperty(
|
||||||
name="For the Lulz",
|
name="For the Lulz",
|
||||||
description="""
|
description="""
|
||||||
On a hit against a creature with a mouth, spend one charge to force the target to roll a DC 13 Wisdom
|
On a hit against a creature with a mouth, spend one charge to force the target to roll a DC 13 Wisdom
|
||||||
|
@ -52,10 +52,7 @@ def test_charges(db, carl):
|
||||||
charge_cost=2,
|
charge_cost=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# from sqlalchemy.orm import relationship
|
dagger_of_lulz = prototypes.Weapon(
|
||||||
# help(relationship)
|
|
||||||
|
|
||||||
dagger_of_lulz = Weapon(
|
|
||||||
name="Dagger of Lulz",
|
name="Dagger of Lulz",
|
||||||
description="This magical dagger has 6 charges. It regains 1d6 charges after a short rest.",
|
description="This magical dagger has 6 charges. It regains 1d6 charges after a short rest.",
|
||||||
damage_die="1d4",
|
damage_die="1d4",
|
||||||
|
@ -68,9 +65,9 @@ def test_charges(db, carl):
|
||||||
attack_range_long=60,
|
attack_range_long=60,
|
||||||
magical=True,
|
magical=True,
|
||||||
charges=6,
|
charges=6,
|
||||||
recharge_time=RechargeTime.SHORT_REST,
|
recharge_time=prototypes.RechargeTime.SHORT_REST,
|
||||||
recharge_amount="1d6",
|
recharge_amount="1d6",
|
||||||
rarity=Rarity["Very Rare"],
|
rarity=prototypes.Rarity["Very Rare"],
|
||||||
requires_attunement=True,
|
requires_attunement=True,
|
||||||
properties=[for_the_lulz],
|
properties=[for_the_lulz],
|
||||||
)
|
)
|
||||||
|
@ -100,8 +97,10 @@ def test_charges(db, carl):
|
||||||
|
|
||||||
|
|
||||||
def test_nocharges(db, carl):
|
def test_nocharges(db, carl):
|
||||||
smiles = ItemProperty(name="Smile!", description="The target grins for one minute.", charge_cost=None)
|
smiles = prototypes.ItemProperty(name="Smile!", description="The target grins for one minute.", charge_cost=None)
|
||||||
wand_of_unlimited_smiles = Item(name="Wand of Unlimited Smiles", description="description", properties=[smiles])
|
wand_of_unlimited_smiles = prototypes.BaseItem(
|
||||||
|
name="Wand of Unlimited Smiles", description="description", properties=[smiles]
|
||||||
|
)
|
||||||
db.add_or_update(wand_of_unlimited_smiles)
|
db.add_or_update(wand_of_unlimited_smiles)
|
||||||
|
|
||||||
carl.equipment.add(wand_of_unlimited_smiles)
|
carl.equipment.add(wand_of_unlimited_smiles)
|
||||||
|
@ -115,13 +114,13 @@ def test_nocharges(db, carl):
|
||||||
|
|
||||||
def test_attunement(db, carl):
|
def test_attunement(db, carl):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
helm = Armor(
|
helm = prototypes.Armor(
|
||||||
name="Iron Helm",
|
name="Iron Helm",
|
||||||
rarity=Rarity.Common,
|
rarity=prototypes.Rarity.Common,
|
||||||
)
|
)
|
||||||
helm.add_modifier(Modifier("+1 AC (helmet)", target="armor_class", relative_value=1, stacks=True))
|
helm.add_modifier(Modifier("+1 AC (helmet)", target="armor_class", relative_value=1, stacks=True))
|
||||||
|
|
||||||
shield = Shield(
|
shield = prototypes.Shield(
|
||||||
name="Shield of Missile Attraction",
|
name="Shield of Missile Attraction",
|
||||||
description="""
|
description="""
|
||||||
While holding this shield, you have resistance to damage from ranged weapon attacks.
|
While holding this shield, you have resistance to damage from ranged weapon attacks.
|
||||||
|
@ -130,7 +129,7 @@ def test_attunement(db, carl):
|
||||||
or similar magic. Removing the shield fails to end the curse on you. Whenever a ranged weapon attack is made
|
or similar magic. Removing the shield fails to end the curse on you. Whenever a ranged weapon attack is made
|
||||||
against a target within 10 feet of you, the curse causes you to become the target instead.
|
against a target within 10 feet of you, the curse causes you to become the target instead.
|
||||||
""",
|
""",
|
||||||
rarity=Rarity.Rare,
|
rarity=prototypes.Rarity.Rare,
|
||||||
requires_attunement=True,
|
requires_attunement=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user