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 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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user