move spell management to CharacterSpellInventory

This commit is contained in:
evilchili 2024-09-21 20:45:18 -07:00
parent 9bece1550d
commit d5b81dafb4
4 changed files with 118 additions and 70 deletions

View File

@ -2,6 +2,8 @@ import itertools
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pprint import pprint
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declared_attr 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.classes import CharacterClass, ClassFeature
from ttfrog.db.schema.constants import DamageType, Defenses, InventoryType from ttfrog.db.schema.constants import DamageType, Defenses, InventoryType
from ttfrog.db.schema.inventory import InventoryMixin 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.modifiers import Modifier, ModifierMixin, Stat
from ttfrog.db.schema.skill import Skill from ttfrog.db.schema.skill import Skill
@ -214,6 +217,69 @@ class CharacterSpellInventory(BaseObject, InventoryMap):
__item_class__ = "Spell" __item_class__ = "Spell"
inventory_type: InventoryType = InventoryType.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): class Character(BaseObject, SlugMixin, ModifierMixin):
__tablename__ = "character" __tablename__ = "character"
@ -289,7 +355,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
lazy="immediate", lazy="immediate",
back_populates="character", back_populates="character",
) )
_spells = relationship( spells = relationship(
"CharacterSpellInventory", "CharacterSpellInventory",
uselist=False, uselist=False,
cascade="all,delete,delete-orphan", cascade="all,delete,delete-orphan",
@ -304,15 +370,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def equipment(self): def equipment(self):
return self._equipment.inventory 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 @property
def spell_slots(self): def spell_slots(self):
return list(itertools.chain(*[slot for lvl, slot in self.spell_slots_by_level.items()])) 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.add_skill(skill, proficient=False, expert=False)
self._equipment = CharacterItemInventory(character_id=self.id) self._equipment = CharacterItemInventory(character_id=self.id)
self._spells = CharacterSpellInventory(character_id=self.id) self.spells = CharacterSpellInventory(character_id=self.id)
session.add(self) session.add(self)

View File

@ -1,8 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List
from pprint import pprint
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import Mapped from sqlalchemy.orm import Mapped
from sqlalchemy.orm import base as sa_base from sqlalchemy.orm import base as sa_base
from sqlalchemy.orm import mapped_column, relationship 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) prototype: Mapped["prototypes.BaseSpell"] = relationship(uselist=False, lazy="immediate", init=False)
@property
def spell(self):
return self.prototype
@property @property
def prepared(self): def prepared(self):
return self._prepared or self.always_prepared 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): class Charge(BaseObject):
__tablename__ = "charge" __tablename__ = "charge"

View File

@ -86,9 +86,6 @@ class BaseItem(BaseObject, ModifierMixin):
uselist=True, cascade="all,delete,delete-orphan", lazy="immediate", default_factory=lambda: [] 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 # if this item is a container, set the inventory type
inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None) inventory_type: Mapped[InventoryType] = mapped_column(nullable=True, default=None)

View File

@ -7,12 +7,37 @@ def test_spell_inventory(db, carl):
fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False) fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False)
db.add_or_update([fireball, carl]) 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) db.add_or_update(carl)
assert not carl.equipment.add(fireball)
assert fireball in carl.spells 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): def test_equipment_inventory(db, carl):
@ -28,7 +53,7 @@ def test_equipment_inventory(db, carl):
assert carl.equipment.add(ten_foot_pole) assert carl.equipment.add(ten_foot_pole)
# can't mix and match inventory item types # 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. # add two more 10 foot poles. You can never have too many.
assert carl.equipment.add(ten_foot_pole) 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) fireball = prototypes.BaseSpell(name="Fireball", level=3, concentration=False)
db.add_or_update([carl, prestidigitation, fireball]) db.add_or_update([carl, prestidigitation, fireball])
carl.spells.add(prestidigitation) carl.spells.learn(prestidigitation)
carl.spells.add(fireball) carl.spells.learn(fireball)
db.add_or_update(carl) db.add_or_update(carl)
# verify carl has the spell slots granted by wizard at 1st level # 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 # carl knows the spells but hasn't prepared them
assert prestidigitation in carl.spells assert prestidigitation in carl.spells
assert fireball in carl.spells assert fireball in carl.spells
assert prestidigitation not in carl.prepared_spells assert prestidigitation not in carl.spells.prepared
assert fireball not in carl.prepared_spells assert fireball not in carl.spells.prepared
# prepare the cantrip # prepare the cantrip
carls_prestidigitation = carl.spells.get(prestidigitation) assert carl.spells.prepare(prestidigitation)
assert carls_prestidigitation.prepare() assert carl.spells.cast(prestidigitation)
assert carls_prestidigitation.cast()
# can't prepare a 3rd level spell if you don't have 3rd level slots # can't prepare a 3rd level spell if you don't have 3rd level slots
assert carl.spellcaster_level == 1 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 # make carl a 5th level wizard so he gets a 3rd level spell slot
carl.level_up(wizard, num_levels=4) carl.level_up(wizard, num_levels=4)
@ -138,11 +162,11 @@ def test_spell_slots(db, carl, wizard):
assert carl.spellcaster_level == 3 assert carl.spellcaster_level == 3
# cast fireball until he's out of 3rd level slots # cast fireball until he's out of 3rd level slots
assert not carl.spells.get(fireball).cast() assert not carl.spells.cast(fireball)
assert carl.spells.get(fireball).prepare() assert carl.spells.prepare(fireball)
assert carl.spells.get(fireball).cast() assert carl.spells.cast(fireball)
assert carl.spells.get(fireball).cast() assert carl.spells.cast(fireball)
assert not carl.spells.get(fireball).cast() assert not carl.spells.cast(fireball)
# level up to 7th level, gaining 1 4th level slot and 1 more 3rd level slot # level up to 7th level, gaining 1 4th level slot and 1 more 3rd level slot
carl.add_class(wizard) carl.add_class(wizard)
@ -152,16 +176,16 @@ def test_spell_slots(db, carl, wizard):
assert len(carl.spell_slots_available[3]) == 1 assert len(carl.spell_slots_available[3]) == 1
# cast at 4th level # cast at 4th level
assert carl.spells.get(fireball).cast(level=4) assert carl.spells.cast(fireball, level=4)
assert not carl.spells.get(fireball).cast(level=4) assert not carl.spells.cast(fireball, level=4)
# use the last 3rd level slot # use the last 3rd level slot
assert carl.spells.get(fireball).cast() assert carl.spells.cast(fireball)
assert not carl.spells.get(fireball).cast() assert not carl.spells.cast(fireball)
# unprepare it # unprepare it
assert carl.spells.get(fireball).unprepare() assert carl.spells.unprepare(fireball)
assert not carl.spells.get(fireball).unprepare() assert not carl.spells.unprepare(fireball)
def test_containers(db, carl): def test_containers(db, carl):