adding weapons with charges
This commit is contained in:
parent
5d9fde949d
commit
5c80565264
|
@ -1,5 +1,6 @@
|
|||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
@ -261,15 +262,16 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
|
||||
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
|
||||
|
||||
_inventories = relationship("Inventory", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
inventories = association_proxy("_inventories", "id", creator=inventory_creator)
|
||||
inventories: Mapped[List["Inventory"]] = relationship(
|
||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
||||
)
|
||||
|
||||
_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")
|
||||
|
||||
@property
|
||||
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]
|
||||
|
||||
@property
|
||||
def prepared_spells(self):
|
||||
|
@ -394,7 +396,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
|
||||
@property
|
||||
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]
|
||||
|
||||
@property
|
||||
def equipped_items(self):
|
||||
|
@ -701,6 +703,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
|
|||
):
|
||||
self.add_skill(skill, proficient=False, expert=False)
|
||||
|
||||
self._inventories.append(Inventory(inventory_type=InventoryType.EQUIPMENT, character_id=self.id))
|
||||
self._inventories.append(Inventory(inventory_type=InventoryType.SPELL, character_id=self.id))
|
||||
self.inventories.append(Inventory(inventory_type=InventoryType.EQUIPMENT, character_id=self.id))
|
||||
self.inventories.append(Inventory(inventory_type=InventoryType.SPELL, character_id=self.id))
|
||||
session.add(self)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from typing import List
|
||||
|
||||
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
|
||||
from ttfrog.db.schema.item import Item, ItemProperty, ItemType
|
||||
|
||||
|
||||
class InventoryType(EnumField):
|
||||
|
@ -36,11 +37,15 @@ class Inventory(BaseObject):
|
|||
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, cascade="all,delete,delete-orphan")
|
||||
inventory_map = association_proxy("_inventory_map", "id", creator=inventory_map_creator)
|
||||
items: Mapped[List["InventoryMap"]] = relationship(
|
||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
||||
)
|
||||
|
||||
def get(self, item):
|
||||
return [mapping for mapping in self._inventory_map if mapping.item == item]
|
||||
return self.get_all(item)[0]
|
||||
|
||||
def get_all(self, item):
|
||||
return [mapping for mapping in self.items if mapping.item == item]
|
||||
|
||||
def add(self, item):
|
||||
if item.item_type not in inventory_type_map[self.inventory_type]:
|
||||
|
@ -48,23 +53,25 @@ class Inventory(BaseObject):
|
|||
mapping = InventoryMap(inventory_id=self.id, item_id=item.id)
|
||||
if item.consumable:
|
||||
mapping.count = item.count
|
||||
self.inventory_map.append(mapping)
|
||||
if item.charges:
|
||||
mapping.charges = [Charge(inventory_map_id=mapping.id) for i in range(item.charges)]
|
||||
self.items.append(mapping)
|
||||
return mapping
|
||||
|
||||
def remove(self, mapping):
|
||||
if mapping.id not in self.inventory_map:
|
||||
if mapping not in self.items:
|
||||
return False
|
||||
self.inventory_map.remove(mapping.id)
|
||||
self.items.remove(mapping)
|
||||
return True
|
||||
|
||||
def __contains__(self, obj):
|
||||
for mapping in self._inventory_map:
|
||||
for mapping in self.items:
|
||||
if mapping.item == obj:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._inventory_map
|
||||
yield from self.items
|
||||
|
||||
|
||||
class InventoryMap(BaseObject):
|
||||
|
@ -81,12 +88,32 @@ class InventoryMap(BaseObject):
|
|||
|
||||
always_prepared: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
charges: Mapped[List["Charge"]] = relationship(
|
||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
||||
)
|
||||
|
||||
@property
|
||||
def charges_available(self):
|
||||
return [charge for charge in self.charges if not charge.expended]
|
||||
|
||||
@property
|
||||
def prepared(self):
|
||||
if self.item.item_type == ItemType.SPELL:
|
||||
return self.equipped or self.always_prepared
|
||||
|
||||
def use(self, count=1):
|
||||
def use(self, item_property: ItemProperty, charges=None):
|
||||
if item_property.charge_cost is None:
|
||||
return True
|
||||
avail = self.charges_available
|
||||
if charges is None:
|
||||
charges = item_property.charge_cost
|
||||
if len(avail) < charges:
|
||||
return False
|
||||
for charge in avail:
|
||||
charge.expended = True
|
||||
return True
|
||||
|
||||
def consume(self, count=1):
|
||||
if count < 0:
|
||||
return False
|
||||
if not self.item.consumable:
|
||||
|
@ -98,3 +125,10 @@ class InventoryMap(BaseObject):
|
|||
self.inventory.remove(self)
|
||||
return 0
|
||||
return self.count
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import List
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
|
@ -21,16 +23,36 @@ ITEM_TYPES = [
|
|||
"SHIELD",
|
||||
]
|
||||
|
||||
RECHARGE_TIMES = [
|
||||
"short rest",
|
||||
"long rest",
|
||||
"dawn",
|
||||
]
|
||||
|
||||
COST_TYPES = ["Action", "Bonus Action", "Reaction"]
|
||||
|
||||
RARITY = ["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"]
|
||||
|
||||
|
||||
ItemType = EnumField("ItemType", ((k, k) for k in ITEM_TYPES))
|
||||
Rarity = EnumField("Rarity", ((k, k) for k in RARITY))
|
||||
RechargeTime = EnumField("RechargeTime", ((k.replace(" ", "_").upper(), k) for k in RECHARGE_TIMES))
|
||||
Cost = EnumField("Cost", ((k, k) for k in COST_TYPES))
|
||||
|
||||
|
||||
def item_property_creator(fields):
|
||||
if isinstance(fields, list):
|
||||
for f in fields:
|
||||
yield f
|
||||
elif isinstance(fields, ItemProperty):
|
||||
return fields
|
||||
return ItemProperty(**fields)
|
||||
|
||||
|
||||
class Item(BaseObject, ModifierMixin):
|
||||
__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)
|
||||
description: Mapped[str] = mapped_column(String, nullable=True, default=None)
|
||||
|
@ -42,12 +64,24 @@ class Item(BaseObject, ModifierMixin):
|
|||
consumable: Mapped[bool] = mapped_column(default=False)
|
||||
count: Mapped[int] = mapped_column(nullable=False, default=1)
|
||||
|
||||
charges: Mapped[int] = mapped_column(nullable=True, info={"min": 0}, default=None)
|
||||
recharge_time: Mapped[RechargeTime] = mapped_column(default=RechargeTime.LONG_REST)
|
||||
recharge_amount: Mapped[str] = mapped_column(String(collation="NOCASE"), default="1")
|
||||
|
||||
_class_restrictions: Mapped[int] = mapped_column(ForeignKey("character_class.id"), nullable=True, default=None)
|
||||
class_restrictions: Mapped["CharacterClass"] = relationship(init=False)
|
||||
|
||||
properties: Mapped[List["ItemProperty"]] = relationship(
|
||||
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: []
|
||||
)
|
||||
|
||||
# _spells: Mapped[int] = mapped_column(ForeignKey("spell.id"), nullable=True, default=None)
|
||||
# spells: Mapped["Spell"] = relationship(init=False)
|
||||
|
||||
@property
|
||||
def has_charges(self):
|
||||
return self.charges is not None
|
||||
|
||||
|
||||
class Spell(Item):
|
||||
__tablename__ = "spell"
|
||||
|
@ -83,6 +117,7 @@ class Weapon(Item):
|
|||
versatile: Mapped[bool] = mapped_column(default=False)
|
||||
silvered: Mapped[bool] = mapped_column(default=False)
|
||||
adamantine: Mapped[bool] = mapped_column(default=False)
|
||||
magical: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
@property
|
||||
def ranged(self):
|
||||
|
@ -101,3 +136,15 @@ class Armor(Item):
|
|||
__mapper_args__ = {"polymorphic_identity": ItemType.ARMOR}
|
||||
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
|
||||
item_type: Mapped[ItemType] = ItemType.ARMOR
|
||||
|
||||
|
||||
class ItemProperty(BaseObject):
|
||||
__tablename__ = "item_property"
|
||||
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)
|
||||
item_id: Mapped[int] = mapped_column(ForeignKey("item.id"), default=0)
|
||||
|
||||
# action/reaction/bonus
|
||||
# modifiers?
|
||||
|
|
|
@ -24,7 +24,7 @@ def test_equipment_inventory(db, carl):
|
|||
carl.equipment.add(ten_foot_pole)
|
||||
db.add_or_update(carl)
|
||||
|
||||
all_carls_poles = carl.equipment.get(ten_foot_pole)
|
||||
all_carls_poles = carl.equipment.get_all(ten_foot_pole)
|
||||
assert len(all_carls_poles) == 3
|
||||
|
||||
# check the "contains" logic
|
||||
|
@ -71,17 +71,17 @@ def test_inventory_bundles(db, carl):
|
|||
assert quiver.count == 20
|
||||
|
||||
# use one
|
||||
assert quiver.use(1) == 19
|
||||
assert quiver.consume(1) == 19
|
||||
assert quiver.count == 19
|
||||
|
||||
# cannot use more than you have
|
||||
assert not quiver.use(20)
|
||||
assert not quiver.consume(20)
|
||||
|
||||
# cannot use a negative amount
|
||||
assert not quiver.use(-1)
|
||||
assert not quiver.consume(-1)
|
||||
|
||||
# consume all remaining arrows
|
||||
assert quiver.use(19) == 0
|
||||
assert quiver.consume(19) == 0
|
||||
assert arrows not in carl.equipment
|
||||
|
||||
|
||||
|
@ -107,12 +107,12 @@ def test_spell_slots(db, carl, wizard):
|
|||
assert fireball not in carl.prepared_spells
|
||||
|
||||
# prepare the cantrip
|
||||
carls_prestidigitation = carl.spells.get(prestidigitation)[0]
|
||||
carls_prestidigitation = carl.spells.get(prestidigitation)
|
||||
assert carl.prepare(carls_prestidigitation)
|
||||
assert carl.cast(carls_prestidigitation)
|
||||
|
||||
# prepare() and cast() require a spell from the spell inventory
|
||||
carls_fireball = carl.spells.get(fireball)[0]
|
||||
carls_fireball = carl.spells.get(fireball)
|
||||
|
||||
# can't prepare a 3rd level spell if you don't have 3rd level slots
|
||||
assert carl.spellcaster_level == 1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from ttfrog.db.schema.constants import DamageType, Defenses
|
||||
from ttfrog.db.schema.item import Armor, Rarity, Shield, Weapon
|
||||
from ttfrog.db.schema.item import Armor, ItemProperty, Rarity, RechargeTime, Shield, Weapon
|
||||
from ttfrog.db.schema.modifiers import Modifier
|
||||
|
||||
|
||||
|
@ -40,6 +40,56 @@ def test_weapons(db):
|
|||
assert dagger.melee
|
||||
|
||||
|
||||
def test_charges(db, carl):
|
||||
with db.transaction():
|
||||
for_the_lulz = ItemProperty(
|
||||
name="For the Lulz",
|
||||
description="""
|
||||
On a hit against a creature with a mouth, spend one charge to force the target to roll a DC 13 Wisdom
|
||||
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,
|
||||
)
|
||||
|
||||
# from sqlalchemy.orm import relationship
|
||||
# help(relationship)
|
||||
|
||||
dagger_of_lulz = Weapon(
|
||||
name="Dagger of Lulz",
|
||||
description="This magical dagger has 6 charges. It regains 1d6 charges after a short rest.",
|
||||
damage_die="1d4",
|
||||
damage_type=DamageType.slashing,
|
||||
melee=True,
|
||||
finesse=True,
|
||||
light=True,
|
||||
thrown=True,
|
||||
attack_range=20,
|
||||
attack_range_long=60,
|
||||
magical=True,
|
||||
charges=6,
|
||||
recharge_time=RechargeTime.SHORT_REST,
|
||||
recharge_amount="1d6",
|
||||
rarity=Rarity["Very Rare"],
|
||||
requires_attunement=True,
|
||||
properties=[for_the_lulz],
|
||||
)
|
||||
db.add_or_update([carl, dagger_of_lulz])
|
||||
|
||||
assert for_the_lulz in dagger_of_lulz.properties
|
||||
|
||||
assert carl.equipment.add(dagger_of_lulz)
|
||||
db.add_or_update(carl)
|
||||
|
||||
carls_dagger = carl.equipment.get(dagger_of_lulz)
|
||||
assert carl.equip(carls_dagger)
|
||||
assert carl.attune(carls_dagger)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_attunement(db, carl):
|
||||
with db.transaction():
|
||||
helm = Armor(
|
||||
|
@ -77,7 +127,7 @@ def test_attunement(db, carl):
|
|||
assert carl.equipment.add(helm)
|
||||
db.add_or_update(carl)
|
||||
|
||||
carls_shield = carl.equipment.get(shield)[0]
|
||||
carls_shield = carl.equipment.get(shield)
|
||||
|
||||
assert carl.armor_class == 10
|
||||
assert len(carl.attuned_items) == 0
|
||||
|
@ -95,7 +145,7 @@ def test_attunement(db, carl):
|
|||
assert ranged_resistance in carl.modifiers[DamageType.ranged_weapon_attacks]
|
||||
assert carls_shield in carl.attuned_items
|
||||
|
||||
assert carl.equip(carl.equipment.get(helm)[0])
|
||||
assert carl.equip(carl.equipment.get(helm))
|
||||
assert carl.armor_class == 13
|
||||
|
||||
assert carl.unattune(carls_shield)
|
||||
|
|
Loading…
Reference in New Issue
Block a user