add spell inventory ui, refined level up ui, fixed bugs

This commit is contained in:
evilchili
2024-07-30 23:17:20 -07:00
parent 26bb645d22
commit 708d6fe9e9
9 changed files with 260 additions and 75 deletions
+4 -2
View File
@@ -20,8 +20,10 @@ def load(data: str = ""):
rogue = db.CharacterClass.filter_by(name="rogue").one()
sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14)
sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3)
sabetha.add_class(fighter)
sabetha.add_class(rogue)
sabetha.level_up(fighter, 2)
sabetha.level_up(rogue, 3)
bob = schema.Character("Bob", ancestry=human)
+115 -35
View File
@@ -1,3 +1,4 @@
import itertools
from collections import defaultdict
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
@@ -7,11 +8,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, SlugMixin
from ttfrog.db.schema.classes import CharacterClass, ClassFeature
from ttfrog.db.schema.constants import DamageType, Defenses
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
from ttfrog.db.schema.inventory import Inventory, InventoryMap, InventoryType
from ttfrog.db.schema.item import ItemType
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
from ttfrog.db.schema.skill import Skill
__all__ = [
"Ancestry",
"AncestryTrait",
@@ -53,6 +54,17 @@ def attr_map_creator(fields):
return CharacterClassFeatureMap(**fields)
class SpellSlot(BaseObject):
__tablename__ = "spell_slot"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"))
character_class = relationship("CharacterClass", lazy="immediate")
spell_level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 9})
expended: Mapped[bool] = mapped_column(nullable=False, default=False)
class HitDie(BaseObject):
__tablename__ = "hit_die"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
@@ -145,14 +157,12 @@ class CharacterClassMap(BaseObject):
__tablename__ = "class_map"
__table_args__ = (UniqueConstraint("character_id", "character_class_id"),)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character: Mapped["Character"] = relationship(uselist=False, viewonly=True)
character_class: Mapped["CharacterClass"] = relationship(lazy="immediate")
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), init=False, nullable=False)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), init=False, nullable=False)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=False)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), nullable=False)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
character_class: Mapped["CharacterClass"] = relationship(lazy="immediate", init=False, viewonly=True)
class CharacterClassFeatureMap(BaseObject):
__tablename__ = "character_class_feature_map"
@@ -255,6 +265,35 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
inventories = association_proxy("_inventories", "id", creator=inventory_creator)
_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]
@property
def prepared_spells(self):
hashmap = dict([(mapping.item.name, mapping) for mapping in self.spells 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()]))
@property
def spell_slots_by_level(self):
pool = defaultdict(list)
for slot in self._spell_slots:
pool[slot.spell_level].append(slot)
return pool
@property
def spell_slots_available(self):
available = defaultdict(list)
for slot in self._spell_slots:
if not slot.expended:
available[slot.spell_level].append(slot)
return available
@property
def hit_dice(self):
@@ -333,6 +372,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def levels(self):
return dict([(mapping.character_class.name, mapping.level) for mapping in self.class_map])
@property
def spellcaster_level(self):
return max(slot.spell_level for slot in self.spell_slots)
@property
def class_features(self):
return dict([(mapping.class_feature.name, mapping.option) for mapping in self.character_class_feature_map])
@@ -353,9 +396,34 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
mapping.equipped = False
return True
@property
def spells(self):
return [inv for inv in self._inventories if inv.inventory_type == InventoryType.SPELL][0]
def prepare(self, mapping):
if mapping.item.item_type != ItemType.SPELL:
return False
if mapping.item.level > 0 and not self.spell_slots_by_level[mapping.item.level]:
return False
return self.equip(mapping)
def unprepare(self, mapping):
if mapping.item.item_type != ItemType.SPELL:
return False
return self.unequip(mapping)
def cast(self, mapping: InventoryMap, level=0):
if not mapping.prepared:
return False
if not level:
level = mapping.item.level
# cantrips
if level == 0:
return True
# expend the spell slot
avail = self.spell_slots_available[level]
if not avail:
return False
avail[0].expended = True
return True
def level_in_class(self, charclass):
mapping = [mapping for mapping in self.class_map if mapping.character_class_id == charclass.id]
@@ -407,30 +475,40 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
# return the initial value plus any modifiers.
return self._apply_modifiers(f"{attr}_{'save' if save else 'check'}", initial)
def add_class(self, newclass, level=1):
if level == 0:
return self.remove_class(newclass)
def level_up(self, charclass, num_levels=1):
for _ in range(num_levels):
self._level_up_once(charclass)
return self.level_in_class(charclass)
# add the class mapping and/or set the character's level in the class
mapping = self.level_in_class(newclass)
if not mapping:
self.class_list.append(CharacterClassMap(character=self, character_class=newclass, level=level))
else:
mapping.level = level
def _level_up_once(self, charclass):
current = self.level_in_class(charclass)
if not current:
return False
current.level += 1
# add class features with default values
for lvl in range(1, level + 1):
for attr in newclass.features_at_level(lvl):
self.add_class_feature(newclass, attr, attr.options[0])
# add new features
for feature in charclass.features_at_level(current.level):
self.add_class_feature(charclass, feature, feature.options[0])
# add default class skills
# add new spell slots
for slot in charclass.spell_slots_by_level[current.level]:
self._spell_slots.append(
SpellSlot(character_id=self.id, character_class_id=charclass.id, spell_level=slot.spell_level)
)
# add a new hit die
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=charclass.id))
return current
def add_class(self, newclass):
if self.level_in_class(newclass):
return False
self.class_list.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=0))
self._level_up_once(newclass)
for skill in newclass.skills[: newclass.starting_skills]:
self.add_skill(skill, proficient=True, character_class=newclass)
# add hit dice
existing = len([die for die in self._hit_dice if die.character_class_id == newclass.id])
for lvl in range(level - existing):
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
return True
def remove_class(self, target):
self.class_map = [m for m in self.class_map if m.character_class != target]
@@ -440,6 +518,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
for skill in target.skills:
self.remove_skill(skill, proficient=True, expert=False, character_class=target)
self._hit_dice = [die for die in self._hit_dice if die.character_class != target]
self._spell_slots = [slot for slot in self._spell_slots if slot.character_class != target]
def remove_class_feature(self, feature):
self.character_class_feature_map = [
@@ -455,7 +534,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
mapping = self.level_in_class(character_class)
if not mapping:
return False
if feature not in mapping.character_class.features_at_level(mapping.level):
if feature not in character_class.features_at_level(mapping.level):
return False
self.features.append(
CharacterClassFeatureMap(
@@ -574,6 +653,9 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def spend_hit_die(self, die):
die.spent = True
def expend_sell_splot(self, slot):
slot.expended = True
def __after_insert__(self, session):
"""
Called by the session after_flush event listener to add default joins in other tables.
@@ -583,8 +665,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
):
self.add_skill(skill, proficient=False, expert=False)
for inventory_type in InventoryType:
self._inventories.append(
Inventory(inventory_type=inventory_type, 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)
+20
View File
@@ -12,6 +12,7 @@ __all__ = [
"ClassFeatureMap",
"ClassFeature",
"ClassFeatureOption",
"ClassSpellSlotMap",
"CharacterClass",
"Skill",
"ClassSkillMap",
@@ -44,6 +45,14 @@ class ClassFeatureMap(BaseObject):
feature = relationship("ClassFeature", uselist=False, viewonly=True, lazy="immediate")
class ClassSpellSlotMap(BaseObject):
__tablename__ = "class_spell_slot_map"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"))
class_level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
spell_level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 9}, default=1)
class ClassFeature(BaseObject):
__tablename__ = "class_feature"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
@@ -81,6 +90,7 @@ class CharacterClass(BaseObject):
starting_skills: int = mapped_column(nullable=False, default=0)
features = relationship("ClassFeatureMap", cascade="all,delete,delete-orphan", lazy="immediate")
spell_slots = relationship("ClassSpellSlotMap", cascade="all,delete,delete-orphan", lazy="immediate")
_skills = relationship("ClassSkillMap", cascade="all,delete,delete-orphan", lazy="immediate")
skills = association_proxy("_skills", "skill", creator=skill_creator)
@@ -104,6 +114,16 @@ class CharacterClass(BaseObject):
return True
return False
@property
def spell_slots_by_level(self):
by_level = defaultdict(list)
for mapping in self.spell_slots:
by_level[mapping.class_level].append(mapping)
return by_level
def spell_slots_at_level(self, level: int):
return list(itertools.chain(*[mapping for lvl, mapping in self.spell_slots_by_level.items() if lvl <= level]))
@property
def features_by_level(self):
by_level = defaultdict(list)
+10 -4
View File
@@ -1,6 +1,7 @@
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
@@ -15,9 +16,7 @@ inventory_type_map = {
ItemType.ITEM,
ItemType.SCROLL,
],
InventoryType.SPELL: [
ItemType.SPELL
]
InventoryType.SPELL: [ItemType.SPELL],
}
@@ -29,7 +28,7 @@ def inventory_map_creator(fields):
class Inventory(BaseObject):
__tablename__ = "inventory"
__table_args__ = (UniqueConstraint("character_id", "inventory_type"), )
__table_args__ = (UniqueConstraint("character_id", "inventory_type"),)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"))
inventory_type: Mapped[InventoryType] = mapped_column(nullable=False)
@@ -88,6 +87,13 @@ class InventoryMap(BaseObject):
equipped: Mapped[bool] = mapped_column(default=False)
count: Mapped[int] = mapped_column(nullable=False, default=1)
always_prepared: Mapped[bool] = mapped_column(default=False)
@property
def prepared(self):
if self.item.item_type == ItemType.SPELL:
return self.equipped or self.always_prepared
def use(self, count=1):
if count < 0:
return False
+2 -7
View File
@@ -18,10 +18,7 @@ class ItemType(EnumField):
class Item(BaseObject, ConditionMixin):
__tablename__ = "item"
__mapper_args__ = {
"polymorphic_identity": ItemType.ITEM,
"polymorphic_on": "item_type"
}
__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)
consumable: Mapped[bool] = mapped_column(default=False)
@@ -31,9 +28,7 @@ class Item(BaseObject, ConditionMixin):
class Spell(Item):
__tablename__ = "spell"
__mapper_args__ = {
"polymorphic_identity": ItemType.SPELL
}
__mapper_args__ = {"polymorphic_identity": ItemType.SPELL}
id: Mapped[int] = mapped_column(ForeignKey("item.id"), primary_key=True, init=False)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 0, "max": 9}, default=0)
concentration: Mapped[bool] = mapped_column(default=False)
-1
View File
@@ -283,7 +283,6 @@ class ModifierMixin:
class ConditionMixin:
@property
def modifiers(self):
"""