Implement conditions
This commit is contained in:
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user