move spell management to CharacterSpellInventory
This commit is contained in:
parent
9bece1550d
commit
d5b81dafb4
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user