adding inventories
This commit is contained in:
parent
b68dda2b77
commit
3f45dbe9b9
|
@ -8,8 +8,10 @@ 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
|
||||||
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
|
||||||
|
from ttfrog.db.schema.inventory import Inventory, InventoryMap, InventoryType
|
||||||
from ttfrog.db.schema.skill import Skill
|
from ttfrog.db.schema.skill import Skill
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Ancestry",
|
"Ancestry",
|
||||||
"AncestryTrait",
|
"AncestryTrait",
|
||||||
|
@ -33,6 +35,12 @@ 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
|
||||||
|
@ -219,6 +227,9 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
_reactions_per_turn: Mapped[int] = mapped_column(
|
_reactions_per_turn: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}
|
nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}
|
||||||
)
|
)
|
||||||
|
_attacks_per_action: Mapped[int] = mapped_column(
|
||||||
|
nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}
|
||||||
|
)
|
||||||
|
|
||||||
vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
|
vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
|
||||||
exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5})
|
exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5})
|
||||||
|
@ -235,11 +246,14 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
conditions = association_proxy("_conditions", "condition", creator=condition_creator)
|
conditions = association_proxy("_conditions", "condition", creator=condition_creator)
|
||||||
|
|
||||||
character_class_feature_map = relationship("CharacterClassFeatureMap", cascade="all,delete,delete-orphan")
|
character_class_feature_map = relationship("CharacterClassFeatureMap", cascade="all,delete,delete-orphan")
|
||||||
feature_list = association_proxy("character_class_feature_map", "id", creator=attr_map_creator)
|
features = association_proxy("character_class_feature_map", "id", creator=attr_map_creator)
|
||||||
|
|
||||||
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 = relationship("Inventory", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
|
||||||
|
inventories = association_proxy("_inventories", "id", creator=inventory_creator)
|
||||||
|
|
||||||
_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")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -323,6 +337,14 @@ 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])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def equipment(self):
|
||||||
|
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.EQUIPMENT][0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spells(self):
|
||||||
|
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.SPELL][0]
|
||||||
|
|
||||||
def level_in_class(self, charclass):
|
def level_in_class(self, charclass):
|
||||||
mapping = [mapping for mapping in self.class_map if mapping.character_class_id == charclass.id]
|
mapping = [mapping for mapping in self.class_map if mapping.character_class_id == charclass.id]
|
||||||
if not mapping:
|
if not mapping:
|
||||||
|
@ -423,7 +445,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
return False
|
return False
|
||||||
if feature not in mapping.character_class.features_at_level(mapping.level):
|
if feature not in mapping.character_class.features_at_level(mapping.level):
|
||||||
return False
|
return False
|
||||||
self.feature_list.append(
|
self.features.append(
|
||||||
CharacterClassFeatureMap(
|
CharacterClassFeatureMap(
|
||||||
character_id=self.id,
|
character_id=self.id,
|
||||||
class_feature_id=feature.id,
|
class_feature_id=feature.id,
|
||||||
|
@ -548,3 +570,9 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
||||||
Skill.name.in_(("strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"))
|
Skill.name.in_(("strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"))
|
||||||
):
|
):
|
||||||
self.add_skill(skill, proficient=False, expert=False)
|
self.add_skill(skill, proficient=False, expert=False)
|
||||||
|
|
||||||
|
for inventory_type in InventoryType:
|
||||||
|
self._inventories.append(
|
||||||
|
Inventory(inventory_type=inventory_type, character_id=self.id)
|
||||||
|
)
|
||||||
|
session.add(self)
|
||||||
|
|
57
src/ttfrog/db/schema/inventory.py
Normal file
57
src/ttfrog/db/schema/inventory.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
from sqlalchemy import ForeignKey, UniqueConstraint
|
||||||
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from ttfrog.db.base import BaseObject, EnumField
|
||||||
|
from ttfrog.db.schema.item import Item, ItemType
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryType(EnumField):
|
||||||
|
EQUIPMENT = "EQUIPMENT"
|
||||||
|
SPELL = "SPELL"
|
||||||
|
|
||||||
|
|
||||||
|
inventory_type_map = {
|
||||||
|
InventoryType.EQUIPMENT: [
|
||||||
|
ItemType.ITEM,
|
||||||
|
ItemType.SPELL,
|
||||||
|
],
|
||||||
|
InventoryType.SPELL: [
|
||||||
|
ItemType.SPELL
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def inventory_map_creator(fields):
|
||||||
|
if isinstance(fields, InventoryMap):
|
||||||
|
return fields
|
||||||
|
return InventoryMap(**fields)
|
||||||
|
|
||||||
|
|
||||||
|
class Inventory(BaseObject):
|
||||||
|
__tablename__ = "inventory"
|
||||||
|
__table_args__ = (UniqueConstraint("character_id", "inventory_type"), )
|
||||||
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||||
|
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
|
||||||
|
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
|
||||||
|
|
||||||
|
_inventory_map = relationship("InventoryMap", lazy="immediate", uselist=True)
|
||||||
|
inventory_map = association_proxy("_inventory_map", "id", creator=inventory_map_creator)
|
||||||
|
|
||||||
|
def add(self, item):
|
||||||
|
if item.item_type not in inventory_type_map[self.inventory_type]:
|
||||||
|
return False
|
||||||
|
self.inventory_map.append(InventoryMap(inventory_id=self.id, item_id=item.id))
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from [mapping.item for mapping in self._inventory_map]
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryMap(BaseObject):
|
||||||
|
__tablename__ = "inventory_map"
|
||||||
|
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)
|
||||||
|
|
||||||
|
equipped: Mapped[bool] = mapped_column(default=False)
|
||||||
|
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
37
src/ttfrog/db/schema/item.py
Normal file
37
src/ttfrog/db/schema/item.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from ttfrog.db.base import BaseObject, EnumField
|
||||||
|
from ttfrog.db.schema.modifiers import ConditionMixin
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Item",
|
||||||
|
"Spell",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ItemType(EnumField):
|
||||||
|
ITEM = "ITEM"
|
||||||
|
SPELL = "SPELL"
|
||||||
|
|
||||||
|
|
||||||
|
class Item(BaseObject, ConditionMixin):
|
||||||
|
__tablename__ = "item"
|
||||||
|
__mapper_args__ = {
|
||||||
|
"polymorphic_identity": ItemType.ITEM,
|
||||||
|
"polymorphic_on": "item_type"
|
||||||
|
}
|
||||||
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True)
|
||||||
|
consumable: Mapped[bool] = mapped_column(default=False)
|
||||||
|
item_type: Mapped[ItemType] = mapped_column(default=ItemType.ITEM, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Spell(Item):
|
||||||
|
__tablename__ = "spell"
|
||||||
|
__mapper_args__ = {
|
||||||
|
"polymorphic_identity": ItemType.SPELL
|
||||||
|
}
|
||||||
|
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True)
|
||||||
|
level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0)
|
||||||
|
concentration: Mapped[bool] = mapped_column(default=False)
|
|
@ -282,15 +282,7 @@ class ModifierMixin:
|
||||||
raise AttributeError(f"No such attribute on {self.__class__.__name__} object: {attr_name}.")
|
raise AttributeError(f"No such attribute on {self.__class__.__name__} object: {attr_name}.")
|
||||||
|
|
||||||
|
|
||||||
class Condition(BaseObject):
|
class ConditionMixin:
|
||||||
__tablename__ = "condition"
|
|
||||||
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(default="")
|
|
||||||
|
|
||||||
_modifiers = relationship("Modifier", uselist=True, cascade="all,delete,delete-orphan")
|
|
||||||
_parent_condition_id: Mapped[int] = mapped_column(ForeignKey("condition.id"), nullable=True, default=None)
|
|
||||||
conditions = relationship("Condition", lazy="immediate", uselist=True)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modifiers(self):
|
def modifiers(self):
|
||||||
|
@ -331,6 +323,17 @@ class Condition(BaseObject):
|
||||||
self.conditions = [c for c in self.conditions if c != condition]
|
self.conditions = [c for c in self.conditions if c != condition]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Condition(BaseObject, ConditionMixin):
|
||||||
|
__tablename__ = "condition"
|
||||||
|
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(default="")
|
||||||
|
|
||||||
|
_modifiers = relationship("Modifier", uselist=True, cascade="all,delete,delete-orphan")
|
||||||
|
_parent_condition_id: Mapped[int] = mapped_column(ForeignKey("condition.id"), nullable=True, default=None)
|
||||||
|
conditions = relationship("Condition", lazy="immediate", uselist=True)
|
||||||
|
|
||||||
def __str___(self):
|
def __str___(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
|
@ -104,3 +104,9 @@ def bootstrap(db):
|
||||||
|
|
||||||
# persist all the records we've created
|
# persist all the records we've created
|
||||||
db.add_or_update([foo, bar])
|
db.add_or_update([foo, bar])
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def carl(db, bootstrap):
|
||||||
|
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||||
|
carl = schema.Character(name="Carl", ancestry=tiefling)
|
||||||
|
return carl
|
||||||
|
|
16
test/test_inventories.py
Normal file
16
test/test_inventories.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from ttfrog.db.schema.item import Item, ItemType
|
||||||
|
|
||||||
|
|
||||||
|
def test_equipment_inventory(db, carl):
|
||||||
|
with db.transaction():
|
||||||
|
# trigger the creation of inventory mappings
|
||||||
|
db.add_or_update(carl)
|
||||||
|
|
||||||
|
# create an item
|
||||||
|
ten_foot_pole = Item(name="10ft. Pole", item_type=ItemType.ITEM, consumable=False)
|
||||||
|
db.add_or_update(ten_foot_pole)
|
||||||
|
|
||||||
|
# add the item to carl's inventory
|
||||||
|
carl.equipment.add(ten_foot_pole)
|
||||||
|
db.add_or_update(carl)
|
||||||
|
assert ten_foot_pole in carl.equipment
|
Loading…
Reference in New Issue
Block a user