diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index 4e47686..6886ad1 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -2,6 +2,8 @@ import itertools from collections import defaultdict from dataclasses import dataclass +from pprint import pprint + from sqlalchemy import ForeignKey, String, Text, UniqueConstraint from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declared_attr @@ -11,6 +13,7 @@ from ttfrog.db.base import BaseObject, SlugMixin from ttfrog.db.schema.classes import CharacterClass, ClassFeature from ttfrog.db.schema.constants import DamageType, Defenses, InventoryType from ttfrog.db.schema.inventory import InventoryMixin +from ttfrog.db.schema.prototypes import ItemType from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat from ttfrog.db.schema.skill import Skill @@ -214,6 +217,69 @@ class CharacterSpellInventory(BaseObject, InventoryMap): __item_class__ = "Spell" inventory_type: InventoryType = InventoryType.SPELL + @property + def all_contents(self): + yield from self.inventory.contents + for item in self.character.equipment.all_contents: + if item.prototype.inventory_type == InventoryType.SPELL: + yield from item.inventory.contents + + @property + def available(self): + yield from [spell.prototype for spell in self.all_contents] + + @property + def known(self): + yield from [spell.prototype for spell in self.inventory.contents] + + @property + def prepared(self): + yield from [spell.prototype for spell in self.all_contents if spell.prepared] + + def get_all(self, prototype): + return [mapping for mapping in self.all_contents if mapping.prototype == prototype] + + def get(self, prototype): + return self.get_all(prototype)[0] + + def learn(self, prototype): + return self.inventory.add(prototype) + + def forget(self, spell): + return self.inventory.remove(spell) + + def prepare(self, prototype): + spell = self.get(prototype) + if spell.prototype.level > 0 and not self.character.spell_slots_by_level[spell.prototype.level]: + return False + spell._prepared = True + return True + + def unprepare(self, prototype): + spell = self.get(prototype) + if spell.prepared: + spell._prepared = False + return True + return False + + def cast(self, prototype, level=0): + spell = self.get(prototype) + if not spell.prepared: + return False + if not level: + level = spell.prototype.level + + # cantrips + if level == 0: + return True + + # expend the spell slot + avail = self.character.spell_slots_available[level] + if not avail: + return False + avail[0].expended = True + return True + class Character(BaseObject, SlugMixin, ModifierMixin): __tablename__ = "character" @@ -289,7 +355,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin): lazy="immediate", back_populates="character", ) - _spells = relationship( + spells = relationship( "CharacterSpellInventory", uselist=False, cascade="all,delete,delete-orphan", @@ -304,15 +370,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin): def equipment(self): return self._equipment.inventory - @property - def spells(self): - return self._spells.inventory - - @property - def prepared_spells(self): - hashmap = dict([(mapping.item.name, mapping) for mapping in self.spells.contents if mapping.prepared]) - return list(hashmap.values()) - @property def spell_slots(self): return list(itertools.chain(*[slot for lvl, slot in self.spell_slots_by_level.items()])) @@ -694,5 +751,5 @@ class Character(BaseObject, SlugMixin, ModifierMixin): self.add_skill(skill, proficient=False, expert=False) self._equipment = CharacterItemInventory(character_id=self.id) - self._spells = CharacterSpellInventory(character_id=self.id) + self.spells = CharacterSpellInventory(character_id=self.id) session.add(self) diff --git a/src/ttfrog/db/schema/inventory.py b/src/ttfrog/db/schema/inventory.py index 9eefd18..5457c39 100644 --- a/src/ttfrog/db/schema/inventory.py +++ b/src/ttfrog/db/schema/inventory.py @@ -1,8 +1,11 @@ from dataclasses import dataclass from typing import List +from pprint import pprint + from sqlalchemy import ForeignKey from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import Mapped from sqlalchemy.orm import base as sa_base from sqlalchemy.orm import mapped_column, relationship @@ -172,43 +175,10 @@ class Spell(BaseObject, InventoryItemMixin): 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" diff --git a/src/ttfrog/db/schema/prototypes.py b/src/ttfrog/db/schema/prototypes.py index f50b8b1..4492e54 100644 --- a/src/ttfrog/db/schema/prototypes.py +++ b/src/ttfrog/db/schema/prototypes.py @@ -86,9 +86,6 @@ class BaseItem(BaseObject, ModifierMixin): 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) - # if this item is a container, set the inventory type inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None) diff --git a/test/test_inventories.py b/test/test_inventories.py index 0d5083d..b38e04c 100644 --- a/test/test_inventories.py +++ b/test/test_inventories.py @@ -7,12 +7,37 @@ def test_spell_inventory(db, carl): fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False) db.add_or_update([fireball, carl]) - assert carl.spells.add(fireball) + assert fireball not in carl.spells + assert carl.spells.learn(fireball) db.add_or_update(carl) - assert not carl.equipment.add(fireball) assert fireball in carl.spells - assert fireball not in carl.equipment + assert fireball in carl.spells.known + assert fireball in carl.spells.available + assert fireball not in carl.spells.prepared + + prestidigitation = prototypes.BaseSpell(name="Prestidigitation", level=0, concentration=False) + wish = prototypes.BaseSpell(name="Wish", level=9, concentration=False) + + spellbook = prototypes.BaseItem(name="Spell Book", inventory_type=InventoryType.SPELL) + db.add_or_update([wish, spellbook]) + + assert wish not in carl.spells + + grimoire = carl.equipment.add(spellbook) + db.add_or_update(carl) + grimoire.inventory.add(wish) + grimoire.inventory.add(prestidigitation) + db.add_or_update(carl) + + assert wish in carl.spells.available + assert wish not in carl.spells.known + + assert prestidigitation in carl.spells.available + assert prestidigitation not in carl.spells.known + + assert carl.spells.get(wish) + assert carl.spells.get(prestidigitation) def test_equipment_inventory(db, carl): @@ -28,7 +53,7 @@ def test_equipment_inventory(db, carl): assert carl.equipment.add(ten_foot_pole) # can't mix and match inventory item types - assert not carl.spells.add(ten_foot_pole) + assert not carl.spells.learn(ten_foot_pole) # add two more 10 foot poles. You can never have too many. assert carl.equipment.add(ten_foot_pole) @@ -108,8 +133,8 @@ def test_spell_slots(db, carl, wizard): fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False) db.add_or_update([carl, prestidigitation, fireball]) - carl.spells.add(prestidigitation) - carl.spells.add(fireball) + carl.spells.learn(prestidigitation) + carl.spells.learn(fireball) db.add_or_update(carl) # verify carl has the spell slots granted by wizard at 1st level @@ -120,17 +145,16 @@ def test_spell_slots(db, carl, wizard): # carl knows the spells but hasn't prepared them assert prestidigitation in carl.spells assert fireball in carl.spells - assert prestidigitation not in carl.prepared_spells - assert fireball not in carl.prepared_spells + assert prestidigitation not in carl.spells.prepared + assert fireball not in carl.spells.prepared # prepare the cantrip - carls_prestidigitation = carl.spells.get(prestidigitation) - assert carls_prestidigitation.prepare() - assert carls_prestidigitation.cast() + assert carl.spells.prepare(prestidigitation) + assert carl.spells.cast(prestidigitation) # can't prepare a 3rd level spell if you don't have 3rd level slots assert carl.spellcaster_level == 1 - assert not carl.spells.get(fireball).prepare() + assert not carl.spells.prepare(fireball) # make carl a 5th level wizard so he gets a 3rd level spell slot carl.level_up(wizard, num_levels=4) @@ -138,11 +162,11 @@ 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() - assert not carl.spells.get(fireball).cast() + assert not carl.spells.cast(fireball) + assert carl.spells.prepare(fireball) + assert carl.spells.cast(fireball) + assert carl.spells.cast(fireball) + assert not carl.spells.cast(fireball) # level up to 7th level, gaining 1 4th level slot and 1 more 3rd level slot carl.add_class(wizard) @@ -152,16 +176,16 @@ def test_spell_slots(db, carl, wizard): assert len(carl.spell_slots_available[3]) == 1 # cast at 4th level - assert carl.spells.get(fireball).cast(level=4) - assert not carl.spells.get(fireball).cast(level=4) + assert carl.spells.cast(fireball, level=4) + assert not carl.spells.cast(fireball, level=4) # use the last 3rd level slot - assert carl.spells.get(fireball).cast() - assert not carl.spells.get(fireball).cast() + assert carl.spells.cast(fireball) + assert not carl.spells.cast(fireball) # unprepare it - assert carl.spells.get(fireball).unprepare() - assert not carl.spells.get(fireball).unprepare() + assert carl.spells.unprepare(fireball) + assert not carl.spells.unprepare(fireball) def test_containers(db, carl):