Implement conditions

This commit is contained in:
evilchili
2024-07-13 12:30:43 -07:00
parent da1b4223ea
commit e2ff1eb027
6 changed files with 295 additions and 88 deletions
+4 -4
View File
@@ -44,7 +44,7 @@
"starting_skills": 0
}
],
"class_attribute": [],
"class_feature": [],
"modifier": [
{
"id": 1,
@@ -126,8 +126,8 @@
"slug": "PjPdM"
}
],
"class_attribute_map": [],
"class_attribute_option": [],
"class_feature_map": [],
"class_feature_option": [],
"class_skill_map": [],
"modifier_map": [
{
@@ -157,7 +157,7 @@
"level": 1
}
],
"character_class_attribute_map": [],
"character_class_feature_map": [],
"character_skill_map": [],
"class_map": [
{
+85 -36
View File
@@ -6,8 +6,8 @@ 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 Conditions, DamageType, Defenses
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin, Stat
from ttfrog.db.schema.constants import DamageType, Defenses
from ttfrog.db.schema.modifiers import Condition, Modifier, ModifierMixin, Stat
from ttfrog.db.schema.skill import Skill
__all__ = [
@@ -32,6 +32,11 @@ def skill_creator(fields):
return fields
return CharacterSkillMap(**fields)
def condition_creator(fields):
if isinstance(fields, CharacterConditionMap):
return fields
return CharacterConditionMap(**fields)
def attr_map_creator(fields):
if isinstance(fields, CharacterClassFeatureMap):
@@ -107,6 +112,7 @@ class AncestryTrait(BaseObject, ModifierMixin):
"""
A trait granted to a character via its Ancestry.
"""
__tablename__ = "ancestry_trait"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True)
@@ -140,14 +146,14 @@ class CharacterClassMap(BaseObject):
class CharacterClassFeatureMap(BaseObject):
__tablename__ = "character_class_attribute_map"
__table_args__ = (UniqueConstraint("character_id", "class_attribute_id"),)
__tablename__ = "character_class_feature_map"
__table_args__ = (UniqueConstraint("character_id", "class_feature_id"),)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=False)
class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
option_id: Mapped[int] = mapped_column(ForeignKey("class_attribute_option.id"), nullable=False)
class_feature_id: Mapped[int] = mapped_column(ForeignKey("class_feature.id"), nullable=False)
option_id: Mapped[int] = mapped_column(ForeignKey("class_feature_option.id"), nullable=False)
class_attribute: Mapped["ClassFeature"] = relationship(lazy="immediate")
class_feature: Mapped["ClassFeature"] = relationship(lazy="immediate")
option = relationship("ClassFeatureOption", lazy="immediate")
character_class = relationship(
@@ -159,6 +165,13 @@ class CharacterClassFeatureMap(BaseObject):
uselist=False,
)
class CharacterConditionMap(BaseObject):
__tablename__ = "character_condition_map"
__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"
@@ -194,6 +207,10 @@ 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})
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})
@@ -203,8 +220,11 @@ 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)
character_class_attribute_map = relationship("CharacterClassFeatureMap", cascade="all,delete,delete-orphan")
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
_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")
feature_list = association_proxy("character_class_feature_map", "id", creator=attr_map_creator)
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
@@ -236,6 +256,8 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
unified.update(**self.ancestry.modifiers)
for trait in self.traits:
unified.update(**trait.modifiers)
for condition in self.conditions:
unified.update(**condition.modifiers)
unified.update(**super().modifiers)
return unified
@@ -253,7 +275,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
@property
def speed(self):
return self._apply_modifiers('speed', self._apply_modifiers("walking_speed", self.ancestry.speed))
return self._apply_modifiers("speed", self._apply_modifiers("walking_speed", self.ancestry.speed))
@property
def climb_speed(self):
@@ -287,8 +309,8 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
return dict([(mapping.character_class.name, mapping.level) for mapping in self.class_map])
@property
def class_attributes(self):
return dict([(mapping.class_attribute.name, mapping.option) for mapping in self.character_class_attribute_map])
def class_features(self):
return dict([(mapping.class_feature.name, mapping.option) for mapping in self.character_class_feature_map])
def level_in_class(self, charclass):
mapping = [mapping for mapping in self.class_map if mapping.character_class_id == charclass.id]
@@ -308,14 +330,6 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def absorbs(self, damage_type: DamageType):
return self.defense(damage_type) == Defenses.absorbs
def condition(self, condition):
if not self.immune(condition):
return self._apply_modifiers(condition, False)
return False
def add_condition(self, condition):
self.add_modifier(Modifier(condition, target=condition, new_value=True))
def defense(self, damage_type: DamageType):
return self._apply_modifiers(damage_type, None)
@@ -359,10 +373,10 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
else:
mapping.level = level
# add class attributes with default values
# add class features with default values
for lvl in range(1, level + 1):
for attr in newclass.attributes_at_level(lvl):
self.add_class_attribute(newclass, attr, attr.options[0])
for attr in newclass.features_at_level(lvl):
self.add_class_feature(newclass, attr, attr.options[0])
# add default class skills
for skill in newclass.skills[: newclass.starting_skills]:
@@ -375,39 +389,74 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
def remove_class(self, target):
self.class_map = [m for m in self.class_map if m.character_class != target]
for mapping in self.character_class_attribute_map:
for mapping in self.character_class_feature_map:
if mapping.character_class == target:
self.remove_class_attribute(mapping.class_attribute)
self.remove_class_feature(mapping.class_feature)
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]
def remove_class_attribute(self, attribute):
self.character_class_attribute_map = [
m for m in self.character_class_attribute_map if m.class_attribute.id != attribute.id
def remove_class_feature(self, feature):
self.character_class_feature_map = [
m for m in self.character_class_feature_map if m.class_feature.id != feature.id
]
def has_class_attribute(self, attribute):
return attribute in [m.class_attribute for m in self.character_class_attribute_map]
def has_class_feature(self, feature):
return feature in [m.class_feature for m in self.character_class_feature_map]
def add_class_attribute(self, character_class, attribute, option):
if self.has_class_attribute(attribute):
def add_class_feature(self, character_class, feature, option):
if self.has_class_feature(feature):
return False
mapping = self.level_in_class(character_class)
if not mapping:
return False
if attribute not in mapping.character_class.attributes_at_level(mapping.level):
if feature not in mapping.character_class.features_at_level(mapping.level):
return False
self.attribute_list.append(
self.feature_list.append(
CharacterClassFeatureMap(
character_id=self.id,
class_attribute_id=attribute.id,
class_feature_id=feature.id,
option_id=option.id,
class_attribute=attribute,
class_feature=feature,
)
)
return True
def add_modifier(self, modifier):
if not super().add_modifier(modifier):
return False
if modifier.new_value != Defenses.immune:
return True
modified_condition = None
for cond in self.conditions:
if modifier.target == cond.name:
modified_condition = cond
break
if not modified_condition:
return True
return self.remove_condition(modified_condition)
def has_condition(self, condition):
return condition in self.conditions
def add_condition(self, condition):
if self.immune(condition.name):
return False
if self.has_condition(condition):
return False
self._conditions.append(CharacterConditionMap(condition_id=condition.id, character_id=self.id))
return True
def remove_condition(self, condition):
if not self.has_condition(condition):
return False
mappings = [mapping for mapping in self._conditions if mapping.condition_id != condition.id]
self._conditions = mappings
return True
def add_skill(self, skill, proficient=False, expert=False, character_class=None):
skillmap = None
exists = False
+24 -24
View File
@@ -37,23 +37,23 @@ class ClassSkillMap(BaseObject):
class ClassFeatureMap(BaseObject):
__tablename__ = "class_attribute_map"
class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), primary_key=True)
__tablename__ = "class_feature_map"
class_feature_id: Mapped[int] = mapped_column(ForeignKey("class_feature.id"), primary_key=True)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), primary_key=True)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
attribute = relationship("ClassFeature", uselist=False, viewonly=True, lazy="immediate")
feature = relationship("ClassFeature", uselist=False, viewonly=True, lazy="immediate")
class ClassFeature(BaseObject):
__tablename__ = "class_attribute"
__tablename__ = "class_feature"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
options = relationship("ClassFeatureOption", cascade="all,delete,delete-orphan", lazy="immediate")
def add_option(self, **kwargs):
option = ClassFeatureOption(attribute_id=self.id, **kwargs)
option = ClassFeatureOption(feature_id=self.id, **kwargs)
if not self.options or option not in self.options:
option.attribute_id = self.id
option.feature_id = self.id
if not self.options:
self.options = [option]
else:
@@ -66,10 +66,10 @@ class ClassFeature(BaseObject):
class ClassFeatureOption(BaseObject):
__tablename__ = "class_attribute_option"
__tablename__ = "class_feature_option"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=True)
feature_id: Mapped[int] = mapped_column(ForeignKey("class_feature.id"), nullable=True)
class CharacterClass(BaseObject):
@@ -80,7 +80,7 @@ class CharacterClass(BaseObject):
hit_die_stat_name: Mapped[str] = mapped_column(default="")
starting_skills: int = mapped_column(nullable=False, default=0)
attributes = relationship("ClassFeatureMap", cascade="all,delete,delete-orphan", lazy="immediate")
features = relationship("ClassFeatureMap", 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)
@@ -94,28 +94,28 @@ class CharacterClass(BaseObject):
return True
return False
def add_attribute(self, attribute, level=1):
if not self.attributes or attribute not in self.attributes:
mapping = ClassFeatureMap(character_class_id=self.id, class_attribute_id=attribute.id, level=level)
if not self.attributes:
self.attributes = [mapping]
def add_feature(self, feature, level=1):
if not self.features or feature not in self.features:
mapping = ClassFeatureMap(character_class_id=self.id, class_feature_id=feature.id, level=level)
if not self.features:
self.features = [mapping]
else:
self.attributes.append(mapping)
self.features.append(mapping)
return True
return False
@property
def attributes_by_level(self):
def features_by_level(self):
by_level = defaultdict(list)
for mapping in self.attributes:
by_level[mapping.level].append(mapping.attribute)
for mapping in self.features:
by_level[mapping.level].append(mapping.feature)
return by_level
def attribute(self, name: str):
for mapping in self.attributes:
if mapping.attribute.name.lower() == name.lower():
return mapping.attribute
def feature(self, name: str):
for mapping in self.features:
if mapping.feature.name.lower() == name.lower():
return mapping.feature
return None
def attributes_at_level(self, level: int):
return list(itertools.chain(*[attrs for lvl, attrs in self.attributes_by_level.items() if lvl <= level]))
def features_at_level(self, level: int):
return list(itertools.chain(*[attrs for lvl, attrs in self.features_by_level.items() if lvl <= level]))
+62 -1
View File
@@ -1,9 +1,10 @@
from collections import defaultdict
from typing import Any, Union
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy import ForeignKey, UniqueConstraint, String
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
@@ -71,6 +72,7 @@ class Modifier(BaseObject):
relative_attribute: Mapped[str] = mapped_column(nullable=True, default=None)
new_value: Mapped[str] = mapped_column(nullable=True, default=None)
description: Mapped[str] = mapped_column(default="")
condition_id: Mapped[int] = mapped_column(ForeignKey("condition.id"), init=False, nullable=True, default=None)
class ModifierMixin:
@@ -277,3 +279,62 @@ class ModifierMixin:
modifiable_class=col.info.get("modifiable_class", None),
)
raise AttributeError(f"No such attribute on {self.__class__.__name__} object: {attr_name}.")
class Condition(BaseObject):
__tablename__ = "condition"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(collation="NOCASE"), nullable=False, unique=True)
description: Mapped[str] = mapped_column(default="")
_modifiers = relationship("Modifier", uselist=True, cascade="all,delete,delete-orphan")
_parent_condition_id: Mapped[int] = mapped_column(ForeignKey("condition.id"), nullable=True, default=None)
conditions = relationship("Condition", lazy="immediate", uselist=True)
@property
def modifiers(self):
"""
Return all modifiers for the current instance as a dict keyed on target attribute name.
"""
all_modifiers = defaultdict(list)
for modifier in self._modifiers:
all_modifiers[modifier.target].append(modifier)
for condition in self.conditions:
print(condition.modifiers)
all_modifiers.update(**condition.modifiers)
return all_modifiers
def add_modifier(self, modifier):
if modifier in self._modifiers:
return False
self._modifiers.append(modifier)
return True
def remove_modifier(self, modifier):
if modifier not in self._modifiers:
return False
self._modifiers = [m for m in self._modifiers if m is not modifier]
return True
def add_condition(self, condition):
if condition in self.conditions:
return False
if self._parent_condition_id and self._parent_condition_id == condition.id:
return False
self.conditions.append(condition)
return True
def remove_condition(self, condition):
if condition not in self.conditions:
return False
self.conditions = [c for c in self.conditions if c != condition]
return True
def __str___(self):
return self.name
def __repr__(self):
mods = ''
if self._modifiers:
mods = "\n" + "\n".join([f" - {mod}" for mod in self._modifiers])
return f"{self.name}{mods}"