diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index aedc554..9cb1a0e 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -7,7 +7,7 @@ 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 Condition, Modifier, ModifierMixin, Stat +from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat from ttfrog.db.schema.skill import Skill __all__ = [ @@ -32,6 +32,7 @@ def skill_creator(fields): return fields return CharacterSkillMap(**fields) + def condition_creator(fields): if isinstance(fields, CharacterConditionMap): return fields @@ -165,14 +166,16 @@ class CharacterClassFeatureMap(BaseObject): uselist=False, ) + class CharacterConditionMap(BaseObject): __tablename__ = "character_condition_map" - __table_args__ = (UniqueConstraint("condition_id", "character_id"), ) + __table_args__ = (UniqueConstraint("condition_id", "character_id"),) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) condition_id: Mapped[int] = mapped_column(ForeignKey("condition.id")) character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=True, default=None) condition = relationship("Condition", lazy="immediate") + class Character(BaseObject, SlugMixin, ModifierMixin): __tablename__ = "character" @@ -207,9 +210,15 @@ class Character(BaseObject, SlugMixin, ModifierMixin): nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat} ) - _actions_per_turn: Mapped[int] = mapped_column(nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}) - _bonus_actions_per_turn: Mapped[int] = mapped_column(nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}) - _reactions_per_turn: Mapped[int] = mapped_column(nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True}) + _actions_per_turn: Mapped[int] = mapped_column( + nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True} + ) + _bonus_actions_per_turn: Mapped[int] = mapped_column( + nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True} + ) + _reactions_per_turn: Mapped[int] = mapped_column( + nullable=False, default=1, info={"min": 0, "max": 99, "modifiable": True} + ) vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True}) exhaustion: Mapped[int] = mapped_column(nullable=False, default=0, info={"min": 0, "max": 5}) @@ -220,7 +229,9 @@ class Character(BaseObject, SlugMixin, ModifierMixin): _skills = relationship("CharacterSkillMap", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate") skills = association_proxy("_skills", "skill", creator=skill_creator) - _conditions = relationship("CharacterConditionMap", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate") + _conditions = relationship( + "CharacterConditionMap", uselist=True, cascade="all,delete,delete-orphan", lazy="immediate" + ) conditions = association_proxy("_conditions", "condition", creator=condition_creator) character_class_feature_map = relationship("CharacterClassFeatureMap", cascade="all,delete,delete-orphan") diff --git a/src/ttfrog/db/schema/modifiers.py b/src/ttfrog/db/schema/modifiers.py index 20cba7c..b314f92 100644 --- a/src/ttfrog/db/schema/modifiers.py +++ b/src/ttfrog/db/schema/modifiers.py @@ -1,13 +1,14 @@ from collections import defaultdict from typing import Any, Union -from sqlalchemy import ForeignKey, UniqueConstraint, String +from sqlalchemy import ForeignKey, String, UniqueConstraint from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import Mapped, mapped_column, relationship -# from sqlalchemy.ext.associationproxy import association_proxy from ttfrog.db.base import BaseObject +# from sqlalchemy.ext.associationproxy import association_proxy + class Modifiable: def __new__(cls, base, modified=None): @@ -334,7 +335,7 @@ class Condition(BaseObject): return self.name def __repr__(self): - mods = '' + mods = "" if self._modifiers: mods = "\n" + "\n".join([f" - {mod}" for mod in self._modifiers]) return f"{self.name}{mods}" diff --git a/test/test_schema.py b/test/test_schema.py index 70089a9..c4781e5 100644 --- a/test/test_schema.py +++ b/test/test_schema.py @@ -1,7 +1,6 @@ import json from ttfrog.db import schema -from ttfrog.db.schema.conditions import conditions from ttfrog.db.schema.constants import Conditions, DamageType, Defenses @@ -227,7 +226,9 @@ def test_modifiers(db, bootstrap): # test sets of modifiers from conditions are applied when a condition is active incapacitated = schema.Condition(name="incapacitated") incapacitated.add_modifier(schema.Modifier(target="actions_per_turn", absolute_value=0, name="Incapacitated")) - incapacitated.add_modifier(schema.Modifier(target="bonus_actions_per_turn", absolute_value=0, name="Incapacitated")) + incapacitated.add_modifier( + schema.Modifier(target="bonus_actions_per_turn", absolute_value=0, name="Incapacitated") + ) incapacitated.add_modifier(schema.Modifier(target="reactions_per_turn", absolute_value=0, name="Incapacitated")) db.add_or_update(incapacitated) assert carl.actions_per_turn == 1 @@ -421,32 +422,47 @@ def test_condition_immunity(db, bootstrap): db.add_or_update(carl) assert carl.has_condition(poisoned) + def test_partial_immunities(db, bootstrap): """ Test that individual modifiers applied by a condition can be negated even if not immune to the condition. """ with db.transaction(): - tiefling = db.Ancestry.filter_by(name="tiefling").one() - carl = schema.Character(name="Carl", ancestry=tiefling) - + # Create some modifiers and conditions for this test + fly = schema.Modifier(target="fly_speed", absolute_value=30, name="Fly Spell") + cannot_move = schema.Modifier(name="Cannot Move (Petrified", target="speed", absolute_value=0) poisoned = schema.Condition(name=DamageType.poison) poison_immunity = schema.Modifier("Poison Immunity", target=DamageType.poison, new_value=Defenses.immune) + # incapacitated applies several modifiers incapacitated = schema.Condition(name=Conditions.incapacitated) incapacitated.add_modifier(schema.Modifier(target="actions_per_turn", absolute_value=0, name="Incapacitated")) - incapacitated.add_modifier(schema.Modifier(target="bonus_actions_per_turn", absolute_value=0, name="Incapacitated")) + incapacitated.add_modifier( + schema.Modifier(target="bonus_actions_per_turn", absolute_value=0, name="Incapacitated") + ) incapacitated.add_modifier(schema.Modifier(target="reactions_per_turn", absolute_value=0, name="Incapacitated")) - # petrified incapacitates you, and makes you immune to poison + # petrified applies several modifiers but also incapacitates you! petrified = schema.Condition(name=Conditions.petrified) assert petrified.add_modifier(poison_immunity) + assert petrified.add_modifier(cannot_move) assert petrified.add_condition(incapacitated) - db.add_or_update([poisoned, poison_immunity, incapacitated, petrified]) + db.add_or_update([fly, cannot_move, poisoned, poison_immunity, incapacitated, petrified]) + + # hi carl + tiefling = db.Ancestry.filter_by(name="tiefling").one() + carl = schema.Character(name="Carl", ancestry=tiefling) + + # carl casts fly! + assert carl.fly_speed is None + assert carl.add_modifier(fly) + assert carl.fly_speed == carl.speed == carl.ancestry.speed + db.add_or_update(carl) # poison carl - carl.add_condition(poisoned) - db.add_or_update(carl) assert not carl.immune(DamageType.poison) + assert carl.add_condition(poisoned) + db.add_or_update(carl) # petrify carl assert not carl.immune(Conditions.petrified) @@ -458,6 +474,8 @@ def test_partial_immunities(db, bootstrap): # but carl still can't move assert carl.actions_per_turn == 0 + assert carl.speed == 0 + assert carl.fly_speed == 0 # greater restoration ftw! assert carl.remove_condition(petrified)