added inventory management ui
This commit is contained in:
parent
3f45dbe9b9
commit
26bb645d22
|
@ -341,6 +341,18 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
def equipment(self):
|
def equipment(self):
|
||||||
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.EQUIPMENT][0]
|
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.EQUIPMENT][0]
|
||||||
|
|
||||||
|
def equip(self, mapping):
|
||||||
|
if mapping.equipped:
|
||||||
|
return False
|
||||||
|
mapping.equipped = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unequip(self, mapping):
|
||||||
|
if not mapping.equipped:
|
||||||
|
return False
|
||||||
|
mapping.equipped = False
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def spells(self):
|
def spells(self):
|
||||||
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.SPELL][0]
|
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.SPELL][0]
|
||||||
|
|
|
@ -13,7 +13,7 @@ class InventoryType(EnumField):
|
||||||
inventory_type_map = {
|
inventory_type_map = {
|
||||||
InventoryType.EQUIPMENT: [
|
InventoryType.EQUIPMENT: [
|
||||||
ItemType.ITEM,
|
ItemType.ITEM,
|
||||||
ItemType.SPELL,
|
ItemType.SCROLL,
|
||||||
],
|
],
|
||||||
InventoryType.SPELL: [
|
InventoryType.SPELL: [
|
||||||
ItemType.SPELL
|
ItemType.SPELL
|
||||||
|
@ -34,16 +34,47 @@ class Inventory(BaseObject):
|
||||||
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
|
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
|
||||||
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
|
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
|
||||||
|
|
||||||
_inventory_map = relationship("InventoryMap", lazy="immediate", uselist=True)
|
_inventory_map = relationship("InventoryMap", lazy="immediate", uselist=True, cascade="all,delete,delete-orphan")
|
||||||
inventory_map = association_proxy("_inventory_map", "id", creator=inventory_map_creator)
|
inventory_map = association_proxy("_inventory_map", "id", creator=inventory_map_creator)
|
||||||
|
|
||||||
|
def get(self, item):
|
||||||
|
return [mapping for mapping in self._inventory_map if mapping.item == item]
|
||||||
|
|
||||||
def add(self, item):
|
def add(self, item):
|
||||||
if item.item_type not in inventory_type_map[self.inventory_type]:
|
if item.item_type not in inventory_type_map[self.inventory_type]:
|
||||||
return False
|
return False
|
||||||
self.inventory_map.append(InventoryMap(inventory_id=self.id, item_id=item.id))
|
mapping = InventoryMap(inventory_id=self.id, item_id=item.id)
|
||||||
|
if item.consumable:
|
||||||
|
mapping.count = item.count
|
||||||
|
self.inventory_map.append(mapping)
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
def remove(self, mapping):
|
||||||
|
if mapping.id not in self.inventory_map:
|
||||||
|
return False
|
||||||
|
self.inventory_map.remove(mapping.id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def equip(self, mapping):
|
||||||
|
if mapping.equipped:
|
||||||
|
return False
|
||||||
|
mapping.equipped = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unequip(self, mapping):
|
||||||
|
if not mapping.equipped:
|
||||||
|
return False
|
||||||
|
mapping.equipped = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __contains__(self, obj):
|
||||||
|
for mapping in self._inventory_map:
|
||||||
|
if mapping.item == obj:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from [mapping.item for mapping in self._inventory_map]
|
yield from self._inventory_map
|
||||||
|
|
||||||
|
|
||||||
class InventoryMap(BaseObject):
|
class InventoryMap(BaseObject):
|
||||||
|
@ -52,6 +83,20 @@ class InventoryMap(BaseObject):
|
||||||
inventory_id: Mapped[int] = mapped_column(ForeignKey("inventory.id"))
|
inventory_id: Mapped[int] = mapped_column(ForeignKey("inventory.id"))
|
||||||
item_id: Mapped[int] = mapped_column(ForeignKey("item.id"))
|
item_id: Mapped[int] = mapped_column(ForeignKey("item.id"))
|
||||||
item: Mapped["Item"] = relationship(uselist=False, lazy="immediate", viewonly=True, init=False)
|
item: Mapped["Item"] = relationship(uselist=False, lazy="immediate", viewonly=True, init=False)
|
||||||
|
inventory: Mapped["Inventory"] = relationship(uselist=False, viewonly=True, init=False)
|
||||||
|
|
||||||
equipped: Mapped[bool] = mapped_column(default=False)
|
equipped: Mapped[bool] = mapped_column(default=False)
|
||||||
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
||||||
|
|
||||||
|
def use(self, count=1):
|
||||||
|
if count < 0:
|
||||||
|
return False
|
||||||
|
if not self.item.consumable:
|
||||||
|
return False
|
||||||
|
if self.count < count:
|
||||||
|
return False
|
||||||
|
self.count -= count
|
||||||
|
if self.count == 0:
|
||||||
|
self.inventory.remove(self)
|
||||||
|
return 0
|
||||||
|
return self.count
|
||||||
|
|
|
@ -13,6 +13,7 @@ __all__ = [
|
||||||
class ItemType(EnumField):
|
class ItemType(EnumField):
|
||||||
ITEM = "ITEM"
|
ITEM = "ITEM"
|
||||||
SPELL = "SPELL"
|
SPELL = "SPELL"
|
||||||
|
SCROLL = "SCROLL"
|
||||||
|
|
||||||
|
|
||||||
class Item(BaseObject, ConditionMixin):
|
class Item(BaseObject, ConditionMixin):
|
||||||
|
@ -24,6 +25,7 @@ class Item(BaseObject, ConditionMixin):
|
||||||
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, unique=True)
|
||||||
consumable: Mapped[bool] = mapped_column(default=False)
|
consumable: Mapped[bool] = mapped_column(default=False)
|
||||||
|
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
||||||
item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False)
|
item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +34,7 @@ class Spell(Item):
|
||||||
__mapper_args__ = {
|
__mapper_args__ = {
|
||||||
"polymorphic_identity": ItemType.SPELL
|
"polymorphic_identity": ItemType.SPELL
|
||||||
}
|
}
|
||||||
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True)
|
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
|
||||||
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)
|
||||||
|
item_type: Mapped[ItemType] = mapped_column(default=ItemType.SPELL, init=False)
|
||||||
|
|
|
@ -106,7 +106,13 @@ def bootstrap(db):
|
||||||
db.add_or_update([foo, bar])
|
db.add_or_update([foo, bar])
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def carl(db, bootstrap):
|
def carl(db, bootstrap, tiefling):
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
return schema.Character(name="Carl", ancestry=tiefling)
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
|
||||||
return carl
|
@pytest.fixture
|
||||||
|
def tiefling(db, bootstrap):
|
||||||
|
return db.Ancestry.filter_by(name="tiefling").one()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def human(db, bootstrap):
|
||||||
|
return db.Ancestry.filter_by(name="human").one()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from ttfrog.db.schema.item import Item, ItemType
|
from ttfrog.db.schema.item import Item, Spell, ItemType
|
||||||
|
|
||||||
|
|
||||||
def test_equipment_inventory(db, carl):
|
def test_equipment_inventory(db, carl):
|
||||||
|
@ -6,11 +6,80 @@ def test_equipment_inventory(db, carl):
|
||||||
# trigger the creation of inventory mappings
|
# trigger the creation of inventory mappings
|
||||||
db.add_or_update(carl)
|
db.add_or_update(carl)
|
||||||
|
|
||||||
# create an item
|
# create some items
|
||||||
ten_foot_pole = Item(name="10ft. Pole", item_type=ItemType.ITEM, consumable=False)
|
ten_foot_pole = Item(name="10ft. Pole", item_type=ItemType.ITEM, consumable=False)
|
||||||
db.add_or_update(ten_foot_pole)
|
fireball = Spell(name="Fireball", level=3, concentration=False)
|
||||||
|
db.add_or_update([ten_foot_pole, fireball])
|
||||||
|
|
||||||
# add the item to carl's inventory
|
# add the pole to carl's equipment, and the spell to his spell list.
|
||||||
|
assert carl.equipment.add(ten_foot_pole)
|
||||||
|
assert carl.spells.add(fireball)
|
||||||
|
|
||||||
|
# can't mix and match inventory item types
|
||||||
|
assert not carl.equipment.add(fireball)
|
||||||
|
assert not carl.spells.add(ten_foot_pole)
|
||||||
|
|
||||||
|
# add two more 10 foot poles. You can never have too many.
|
||||||
|
carl.equipment.add(ten_foot_pole)
|
||||||
carl.equipment.add(ten_foot_pole)
|
carl.equipment.add(ten_foot_pole)
|
||||||
db.add_or_update(carl)
|
db.add_or_update(carl)
|
||||||
|
|
||||||
|
all_carls_poles = carl.equipment.get(ten_foot_pole)
|
||||||
|
assert len(all_carls_poles) == 3
|
||||||
|
|
||||||
|
# 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 fireball in carl.spells
|
||||||
|
assert fireball not in carl.equipment
|
||||||
|
|
||||||
|
# equip one pole
|
||||||
|
assert carl.equip(all_carls_poles[0])
|
||||||
|
|
||||||
|
# can't equip it twice
|
||||||
|
assert not carl.equip(all_carls_poles[0])
|
||||||
|
|
||||||
|
# unequip it
|
||||||
|
assert carl.unequip(all_carls_poles[0])
|
||||||
|
|
||||||
|
# can't unequip the unequipped ones
|
||||||
|
assert not carl.unequip(all_carls_poles[1])
|
||||||
|
assert not carl.unequip(all_carls_poles[2])
|
||||||
|
|
||||||
|
# drop one pole
|
||||||
|
assert carl.equipment.remove(all_carls_poles[0])
|
||||||
|
assert ten_foot_pole in carl.equipment
|
||||||
|
|
||||||
|
# drop the remaining poles
|
||||||
|
assert carl.equipment.remove(all_carls_poles[1])
|
||||||
|
assert carl.equipment.remove(all_carls_poles[2])
|
||||||
|
assert ten_foot_pole not in carl.equipment
|
||||||
|
|
||||||
|
# can't drop what you don't have
|
||||||
|
assert not carl.equipment.remove(all_carls_poles[0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_bundles(db, carl):
|
||||||
|
with db.transaction():
|
||||||
|
arrows = Item(name="Arrows", item_type=ItemType.ITEM, consumable=True, count=20)
|
||||||
|
db.add_or_update([carl, arrows])
|
||||||
|
quiver = carl.equipment.add(arrows)
|
||||||
|
db.add_or_update(carl)
|
||||||
|
|
||||||
|
# full quiver
|
||||||
|
assert arrows in carl.equipment
|
||||||
|
assert quiver.count == 20
|
||||||
|
|
||||||
|
# use one
|
||||||
|
assert quiver.use(1) == 19
|
||||||
|
assert quiver.count == 19
|
||||||
|
|
||||||
|
# cannot use more than you have
|
||||||
|
assert not quiver.use(20)
|
||||||
|
|
||||||
|
# cannot use a negative amount
|
||||||
|
assert not quiver.use(-1)
|
||||||
|
|
||||||
|
# consume all remaining arrows
|
||||||
|
assert quiver.use(19) == 0
|
||||||
|
assert arrows not in carl.equipment
|
||||||
|
|
|
@ -205,13 +205,10 @@ def test_ancestries(db, bootstrap):
|
||||||
assert grognak.check_modifier(strength, save=True) == 1
|
assert grognak.check_modifier(strength, save=True) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_modifiers(db, bootstrap):
|
def test_modifiers(db, bootstrap, carl, tiefling, human):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
human = db.Ancestry.filter_by(name="human").one()
|
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
|
||||||
|
|
||||||
# no modifiers; speed is ancestry speed
|
# no modifiers; speed is ancestry speed
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
|
||||||
marx = schema.Character(name="Marx", ancestry=human)
|
marx = schema.Character(name="Marx", ancestry=human)
|
||||||
db.add_or_update([carl, marx])
|
db.add_or_update([carl, marx])
|
||||||
assert carl.speed == carl.ancestry.speed == 30
|
assert carl.speed == carl.ancestry.speed == 30
|
||||||
|
@ -335,10 +332,9 @@ def test_modifiers(db, bootstrap):
|
||||||
assert not carl.remove_skill(athletics, proficient=True, expert=False, character_class=None)
|
assert not carl.remove_skill(athletics, proficient=True, expert=False, character_class=None)
|
||||||
|
|
||||||
|
|
||||||
def test_defenses(db, bootstrap):
|
def test_defenses(db, bootstrap, tiefling, carl):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
db.add_or_update(carl)
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
|
||||||
assert carl.resistant(DamageType.fire)
|
assert carl.resistant(DamageType.fire)
|
||||||
carl.apply_damage(5, DamageType.fire)
|
carl.apply_damage(5, DamageType.fire)
|
||||||
assert carl.hit_points == 8 # half damage
|
assert carl.hit_points == 8 # half damage
|
||||||
|
@ -387,13 +383,12 @@ def test_defenses(db, bootstrap):
|
||||||
assert carl.hit_points == 8 # half damage
|
assert carl.hit_points == 8 # half damage
|
||||||
|
|
||||||
|
|
||||||
def test_condition_immunity(db, bootstrap):
|
def test_condition_immunity(db, bootstrap, carl, tiefling):
|
||||||
"""
|
"""
|
||||||
Test immunities prevent conditions from being applied
|
Test immunities prevent conditions from being applied
|
||||||
"""
|
"""
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
db.add_or_update(carl)
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
|
||||||
poisoned = schema.Condition(name=DamageType.poison)
|
poisoned = schema.Condition(name=DamageType.poison)
|
||||||
poison_immunity = schema.Modifier("Poison Immunity", target=DamageType.poison, new_value=Defenses.immune)
|
poison_immunity = schema.Modifier("Poison Immunity", target=DamageType.poison, new_value=Defenses.immune)
|
||||||
db.add_or_update([carl, poisoned, poison_immunity])
|
db.add_or_update([carl, poisoned, poison_immunity])
|
||||||
|
@ -423,11 +418,13 @@ def test_condition_immunity(db, bootstrap):
|
||||||
assert carl.has_condition(poisoned)
|
assert carl.has_condition(poisoned)
|
||||||
|
|
||||||
|
|
||||||
def test_partial_immunities(db, bootstrap):
|
def test_partial_immunities(db, bootstrap, carl, tiefling):
|
||||||
"""
|
"""
|
||||||
Test that individual modifiers applied by a condition can be negated even if not immune to the condition.
|
Test that individual modifiers applied by a condition can be negated even if not immune to the condition.
|
||||||
"""
|
"""
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
|
db.add_or_update(carl)
|
||||||
|
|
||||||
# Create some modifiers and conditions for this test
|
# Create some modifiers and conditions for this test
|
||||||
fly = schema.Modifier(target="fly_speed", absolute_value=30, name="Fly Spell")
|
fly = schema.Modifier(target="fly_speed", absolute_value=30, name="Fly Spell")
|
||||||
cannot_move = schema.Modifier(name="Cannot Move (Petrified", target="speed", absolute_value=0)
|
cannot_move = schema.Modifier(name="Cannot Move (Petrified", target="speed", absolute_value=0)
|
||||||
|
@ -449,10 +446,6 @@ def test_partial_immunities(db, bootstrap):
|
||||||
assert petrified.add_condition(incapacitated)
|
assert petrified.add_condition(incapacitated)
|
||||||
db.add_or_update([fly, cannot_move, poisoned, poison_immunity, incapacitated, petrified])
|
db.add_or_update([fly, cannot_move, poisoned, poison_immunity, incapacitated, petrified])
|
||||||
|
|
||||||
# hi carl
|
|
||||||
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
|
||||||
carl = schema.Character(name="Carl", ancestry=tiefling)
|
|
||||||
|
|
||||||
# carl casts fly!
|
# carl casts fly!
|
||||||
assert carl.fly_speed is None
|
assert carl.fly_speed is None
|
||||||
assert carl.add_modifier(fly)
|
assert carl.add_modifier(fly)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user