From b09b07d17263ad14f0e86827d22f0b091d12fa25 Mon Sep 17 00:00:00 2001 From: evilchili Date: Mon, 2 Sep 2024 12:39:59 -0700 Subject: [PATCH] make inventories recursive --- src/ttfrog/db/schema/inventory.py | 50 +++++++++++++++++++++++-------- src/ttfrog/db/schema/item.py | 2 +- test/test_inventories.py | 44 ++++++++++++++++++++++++++- test/test_items.py | 43 ++++++++++++++++++++++++-- 4 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/ttfrog/db/schema/inventory.py b/src/ttfrog/db/schema/inventory.py index cfdb870..c942003 100644 --- a/src/ttfrog/db/schema/inventory.py +++ b/src/ttfrog/db/schema/inventory.py @@ -19,14 +19,16 @@ inventory_type_map = { ItemType.SHIELD, ItemType.ITEM, ItemType.SCROLL, + ItemType.CONTAINER, ], InventoryType.SPELL: [ItemType.SPELL], } def inventory_map_creator(fields): - if isinstance(fields, InventoryMap): - return fields + # if isinstance(fields, InventoryMap): + # return fields + # return InventoryMap(**fields) return InventoryMap(**fields) @@ -36,7 +38,7 @@ class Inventory(BaseObject): id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) inventory_type: Mapped[InventoryType] = mapped_column(nullable=False) - items: Mapped[List["InventoryMap"]] = relationship( + item_map: Mapped[List["InventoryMap"]] = relationship( uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] ) @@ -46,11 +48,33 @@ class Inventory(BaseObject): 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.items if mapping.item == 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]: @@ -60,23 +84,23 @@ class Inventory(BaseObject): mapping.count = item.count if item.charges: mapping.charges = [Charge(inventory_map_id=mapping.id) for i in range(item.charges)] - self.items.append(mapping) + self.item_map.append(mapping) return mapping def remove(self, mapping): - if mapping not in self.items: - return False - self.items.remove(mapping) - return True + if mapping in self.item_map: + self.item_map.remove(mapping) + return True + return False def __contains__(self, obj): - for mapping in self.items: - if mapping.item == obj: + for item in self.all_items: + if item == obj: return True return False def __iter__(self): - yield from self.items + yield from self.all_items class InventoryMap(BaseObject): @@ -138,7 +162,7 @@ class InventoryMap(BaseObject): charges = item_property.charge_cost if len(avail) < charges: return False - for charge in avail: + for charge in avail[:charges]: charge.expended = True return True diff --git a/src/ttfrog/db/schema/item.py b/src/ttfrog/db/schema/item.py index dcae91e..1f05cdc 100644 --- a/src/ttfrog/db/schema/item.py +++ b/src/ttfrog/db/schema/item.py @@ -144,7 +144,7 @@ class ItemProperty(BaseObject): 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) + charge_cost: Mapped[int] = mapped_column(nullable=True, info={"min": 1}, default=None) item_id: Mapped[int] = mapped_column(ForeignKey("item.id"), default=0) # action/reaction/bonus diff --git a/test/test_inventories.py b/test/test_inventories.py index 6fd1134..94c2829 100644 --- a/test/test_inventories.py +++ b/test/test_inventories.py @@ -1,3 +1,4 @@ +from ttfrog.db.schema.container import Container from ttfrog.db.schema.item import Item, ItemType, Spell @@ -41,7 +42,15 @@ def test_equipment_inventory(db, carl): # can't equip it twice assert not pole_one.equip() - # unequip it + # 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 + assert not pole_one.consume() + assert not pole_one.attune() + assert pole_one.unequip() # can't unequip the unequipped ones @@ -123,6 +132,7 @@ def test_spell_slots(db, carl, wizard): assert carl.spellcaster_level == 3 # cast fireball until he's out of 3rd level slots + assert not carl.spells.get(fireball).cast() assert carl.spells.get(fireball).prepare() assert carl.spells.get(fireball).cast() assert carl.spells.get(fireball).cast() @@ -142,3 +152,35 @@ def test_spell_slots(db, carl, wizard): # use the last 3rd level slot assert carl.spells.get(fireball).cast() assert not carl.spells.get(fireball).cast() + + # unprepare it + assert carl.spells.get(fireball).unprepare() + assert not carl.spells.get(fireball).unprepare() + + +def test_containers(db, carl): + with db.transaction(): + ten_foot_pole = Item(name="10ft. Pole", item_type=ItemType.ITEM, consumable=False) + bag_of_holding = Container(name="Bag of Holding") + db.add_or_update([carl, ten_foot_pole, bag_of_holding]) + + # add the ten_foot_pole to the bag of holding + assert bag_of_holding.inventory.add(ten_foot_pole) + db.add_or_update(bag_of_holding) + pole_from_bag = bag_of_holding.inventory.get(ten_foot_pole) + assert pole_from_bag + assert pole_from_bag in bag_of_holding.inventory + assert pole_from_bag not in carl.equipment + + # add the bag of holding to carl's equipment + assert carl.equipment.add(bag_of_holding) + db.add_or_update(bag_of_holding) + assert pole_from_bag in carl.equipment + + # test equality of mappings + carls_bag = carl.equipment.get(bag_of_holding) + carls_pole = carl.equipment.get(ten_foot_pole) + assert carls_pole == pole_from_bag + + # remove the pole from the bag + assert carls_bag.item.inventory.remove(pole_from_bag) diff --git a/test/test_items.py b/test/test_items.py index 4efcb16..0f04870 100644 --- a/test/test_items.py +++ b/test/test_items.py @@ -1,5 +1,5 @@ from ttfrog.db.schema.constants import DamageType, Defenses -from ttfrog.db.schema.item import Armor, ItemProperty, Rarity, RechargeTime, Shield, Weapon +from ttfrog.db.schema.item import Armor, Item, ItemProperty, Rarity, RechargeTime, Shield, Weapon from ttfrog.db.schema.modifiers import Modifier @@ -49,7 +49,7 @@ def test_charges(db, carl): saving throw. On a failure, the target is forced to grin for one minute. While grinning, the target cannot speak. The target can repeat the saving throw at the start of their turn." """, - charge_cost=1, + charge_cost=2, ) # from sqlalchemy.orm import relationship @@ -88,6 +88,29 @@ def test_charges(db, carl): assert len(carls_dagger.charges) == dagger_of_lulz.charges == 6 assert len(carls_dagger.charges_available) == dagger_of_lulz.charges == 6 assert carls_dagger.use(for_the_lulz) + assert len(carls_dagger.charges_available) == 4 + + # use the remaining charges + assert carls_dagger.use(for_the_lulz) + assert carls_dagger.use(for_the_lulz) + + # all out of charges + assert len(carls_dagger.charges_available) == 0 + assert not carls_dagger.use(for_the_lulz) + + +def test_nocharges(db, carl): + smiles = 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]) + db.add_or_update(wand_of_unlimited_smiles) + + carl.equipment.add(wand_of_unlimited_smiles) + db.add_or_update(carl) + + # no charges means you can use it at will + assert carl.equipment.get(wand_of_unlimited_smiles).use(smiles) + assert carl.equipment.get(wand_of_unlimited_smiles).use(smiles) + assert carl.equipment.get(wand_of_unlimited_smiles).use(smiles) def test_attunement(db, carl): @@ -139,7 +162,8 @@ def test_attunement(db, carl): assert carl.armor_class == 12 assert carls_shield not in carl.attuned_items - carls_shield.attune() + assert carls_shield.attune() + assert not carls_shield.attune() assert carl.armor_class == 12 assert plus_two_ac in carl.modifiers["armor_class"] assert ranged_resistance in carl.modifiers[DamageType.ranged_weapon_attacks] @@ -149,7 +173,20 @@ def test_attunement(db, carl): assert carl.armor_class == 13 assert carls_shield.unattune() + assert not carls_shield.unattune() assert carl.armor_class == 13 assert ranged_resistance not in carl.modifiers[DamageType.ranged_weapon_attacks] assert carls_shield.unequip() assert carl.armor_class == 11 + + # can only attune 3 items + assert carl.equipment.add(shield) + assert carl.equipment.add(shield) + assert carl.equipment.add(shield) + db.add_or_update(carl) + + assert carl.equipment.get_all(shield)[0].attune() + assert carl.equipment.get_all(shield)[1].attune() + assert carl.equipment.get_all(shield)[2].attune() + assert len(carl.attuned_items) == 3 + assert not carl.equipment.get_all(shield)[3].attune()