convert to modern MappedAsDataclass models

This commit is contained in:
evilchili 2024-04-28 14:30:47 -07:00
parent 1ff0e5ca7d
commit 3980be5f07
6 changed files with 143 additions and 100 deletions

View File

@ -2,9 +2,9 @@ import enum
import nanoid import nanoid
from nanoid_dictionary import human_alphabet from nanoid_dictionary import human_alphabet
from pyramid_sqlalchemy import BaseObject as _BaseObject
from slugify import slugify from slugify import slugify
from sqlalchemy import Column, String from sqlalchemy import Column, String
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
def genslug(): def genslug():
@ -19,7 +19,7 @@ class SlugMixin:
return "-".join([self.slug, slugify(self.name.title().replace(" ", ""), ok="", only_ascii=True, lower=False)]) return "-".join([self.slug, slugify(self.name.title().replace(" ", ""), ok="", only_ascii=True, lower=False)])
class BaseObject(_BaseObject): class BaseObject(MappedAsDataclass, DeclarativeBase):
""" """
Allows for iterating over Model objects' column names and values Allows for iterating over Model objects' column names and values
""" """

View File

@ -7,30 +7,30 @@ def bootstrap():
db.init() db.init()
with db.transaction(): with db.transaction():
# ancestries # ancestries
human = schema.Ancestry(name="human") human = schema.Ancestry("human")
tiefling = schema.Ancestry(name="tiefling") tiefling = schema.Ancestry("tiefling")
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="intelligence", relative_value=1)) tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="intelligence", relative_value=1))
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="charisma", relative_value=2)) tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="charisma", relative_value=2))
darkvision = schema.AncestryTrait( darkvision = schema.AncestryTrait(
name="Darkvision", "Darkvision",
description=( description=(
"You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it " "You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it "
"were dim light. You cant discern color in darkness, only shades of gray." "were dim light. You cant discern color in darkness, only shades of gray."
), ),
) )
darkvision.add_modifier(schema.Modifier(name="Darkvision", target="vision_in_darkness", absolute_value=120)) darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
tiefling.add_trait(darkvision) tiefling.add_trait(darkvision)
# classes # classes
fighter = schema.CharacterClass(name="fighter", hit_dice="1d10", hit_dice_stat="CON") fighter = schema.CharacterClass("fighter", hit_dice="1d10", hit_dice_stat="CON")
rogue = schema.CharacterClass(name="rogue", hit_dice="1d8", hit_dice_stat="DEX") rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX")
# characters # characters
sabetha = schema.Character(name="Sabetha", ancestry=tiefling) sabetha = schema.Character("Sabetha", ancestry=tiefling)
sabetha.add_class(fighter, level=2) sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3) sabetha.add_class(rogue, level=3)
bob = schema.Character(name="Bob", ancestry=human) bob = schema.Character("Bob", ancestry=human)
# persist all the records we've created # persist all the records we've created
db.add_or_update([sabetha, bob]) db.add_or_update([sabetha, bob])

View File

@ -6,8 +6,7 @@ from contextlib import contextmanager
from functools import cached_property from functools import cached_property
import transaction import transaction
from pyramid_sqlalchemy import Session, init_sqlalchemy from pyramid_sqlalchemy.meta import Session
from pyramid_sqlalchemy import metadata as _metadata
from sqlalchemy import create_engine from sqlalchemy import create_engine
import ttfrog.db.schema import ttfrog.db.schema
@ -43,7 +42,7 @@ class SQLDatabaseManager:
@cached_property @cached_property
def metadata(self): def metadata(self):
return _metadata return ttfrog.db.schema.BaseObject.metadata
@cached_property @cached_property
def tables(self): def tables(self):
@ -77,7 +76,8 @@ class SQLDatabaseManager:
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10] return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
def init(self): def init(self):
init_sqlalchemy(self.engine) self.session.configure(bind=self.engine, autoflush=False)
self.metadata.bind = self.engine
self.metadata.create_all(self.engine) self.metadata.create_all(self.engine)
def dump(self, names: list = []): def dump(self, names: list = []):

View File

@ -1,8 +1,9 @@
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy import ForeignKey, Text, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, SlugMixin
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin from ttfrog.db.schema.modifiers import Modifier, ModifierMixin
__all__ = [ __all__ = [
@ -31,11 +32,11 @@ def attr_map_creator(fields):
class AncestryTraitMap(BaseObject): class AncestryTraitMap(BaseObject):
__tablename__ = "trait_map" __tablename__ = "trait_map"
__table_args__ = (UniqueConstraint("ancestry_id", "ancestry_trait_id"),) __table_args__ = (UniqueConstraint("ancestry_id", "ancestry_trait_id"),)
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
ancestry_id = Column(Integer, ForeignKey("ancestry.id")) ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"))
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id")) ancestry_trait_id: Mapped[int] = mapped_column(ForeignKey("ancestry_trait.id"), init=False)
trait = relationship("AncestryTrait", uselist=False, lazy="immediate") trait: Mapped["AncestryTrait"] = relationship(uselist=False, lazy="immediate")
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}) level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20})
class Ancestry(BaseObject, ModifierMixin): class Ancestry(BaseObject, ModifierMixin):
@ -44,15 +45,20 @@ class Ancestry(BaseObject, ModifierMixin):
""" """
__tablename__ = "ancestry" __tablename__ = "ancestry"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True) name: Mapped[str] = mapped_column(unique=True, nullable=False)
creature_type = Column(Enum(CreatureTypesEnum), nullable=False, default="humanoid")
size = Column(Enum(SizesEnum), nullable=False, default="Medium") creature_type: Mapped[str] = mapped_column(nullable=False, default="humanoid")
walk_speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99}) size: Mapped[str] = mapped_column(nullable=False, default="medium")
_fly_speed = Column(Integer, info={"min": 0, "max": 99}) walk_speed: Mapped[int] = mapped_column(nullable=False, default=30, info={"min": 0, "max": 99})
_climb_speed = Column(Integer, info={"min": 0, "max": 99})
_swim_speed = Column(Integer, info={"min": 0, "max": 99}) _fly_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_traits = relationship("AncestryTraitMap", cascade="all,delete,delete-orphan", lazy="immediate") _climb_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_swim_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_traits = relationship(
"AncestryTraitMap", init=False, uselist=True, cascade="all,delete,delete-orphan", lazy="immediate"
)
@property @property
def traits(self): def traits(self):
@ -71,8 +77,12 @@ class Ancestry(BaseObject, ModifierMixin):
return self._swim_speed or int(self.speed / 2) return self._swim_speed or int(self.speed / 2)
def add_trait(self, trait, level=1): def add_trait(self, trait, level=1):
if trait not in self._traits: if not self._traits or trait not in self._traits:
self._traits.append(AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level)) mapping = AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level)
if not self._traits:
self._traits = [mapping]
else:
self._traits.append(mapping)
return True return True
return False return False
@ -86,9 +96,9 @@ class AncestryTrait(BaseObject, ModifierMixin):
""" """
__tablename__ = "ancestry_trait" __tablename__ = "ancestry_trait"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
description = Column(Text) description: Mapped[Text] = mapped_column(Text, default="")
def __repr__(self): def __repr__(self):
return self.name return self.name
@ -97,13 +107,14 @@ class AncestryTrait(BaseObject, ModifierMixin):
class CharacterClassMap(BaseObject): class CharacterClassMap(BaseObject):
__tablename__ = "class_map" __tablename__ = "class_map"
__table_args__ = (UniqueConstraint("character_id", "character_class_id"),) __table_args__ = (UniqueConstraint("character_id", "character_class_id"),)
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id"), nullable=False) character: Mapped["Character"] = relationship(uselist=False, viewonly=True)
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False) character_class: Mapped["CharacterClass"] = relationship(lazy="immediate")
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
character_class = relationship("CharacterClass", lazy="immediate") character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), init=False, nullable=False)
character = relationship("Character", uselist=False, viewonly=True) character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), init=False, nullable=False)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
def __repr__(self): def __repr__(self):
return "{self.character.name}, {self.character_class.name}, level {self.level}" return "{self.character.name}, {self.character_class.name}, level {self.level}"
@ -112,12 +123,12 @@ class CharacterClassMap(BaseObject):
class CharacterClassAttributeMap(BaseObject): class CharacterClassAttributeMap(BaseObject):
__tablename__ = "character_class_attribute_map" __tablename__ = "character_class_attribute_map"
__table_args__ = (UniqueConstraint("character_id", "class_attribute_id"),) __table_args__ = (UniqueConstraint("character_id", "class_attribute_id"),)
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id"), nullable=False) character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=False)
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), nullable=False) class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
option_id = Column(Integer, ForeignKey("class_attribute_option.id"), nullable=False) option_id: Mapped[int] = mapped_column(ForeignKey("class_attribute_option.id"), nullable=False)
class_attribute = relationship("ClassAttribute", lazy="immediate") class_attribute: Mapped["ClassAttribute"] = relationship(lazy="immediate")
option = relationship("ClassAttributeOption", lazy="immediate") option = relationship("ClassAttributeOption", lazy="immediate")
character_class = relationship( character_class = relationship(
@ -132,22 +143,23 @@ class CharacterClassAttributeMap(BaseObject):
class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin): class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character" __tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, default="New Character", nullable=False)
armor_class = Column(Integer, default=10, nullable=False, info={"min": 1, "max": 99})
hit_points = Column(Integer, default=1, nullable=False, info={"min": 0, "max": 999})
max_hit_points = Column(Integer, default=10, nullable=False, info={"min": 0, "max": 999})
temp_hit_points = Column(Integer, default=0, nullable=False, info={"min": 0, "max": 999})
strength = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
dexterity = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
constitution = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
intelligence = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
wisdom = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
charisma = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
_vision = Column(Integer, info={"min": 0}) name: Mapped[str] = mapped_column(default="New Character", nullable=False)
armor_class: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 1, "max": 99})
hit_points: Mapped[int] = mapped_column(default=1, nullable=False, info={"min": 0, "max": 999})
max_hit_points: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 0, "max": 999})
temp_hit_points: Mapped[int] = mapped_column(default=0, nullable=False, info={"min": 0, "max": 999})
strength: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
dexterity: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
constitution: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
intelligence: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
wisdom: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
charisma: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
proficiencies = Column(String) _vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0})
proficiencies: Mapped[str] = mapped_column(nullable=False, default="")
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan") class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
class_list = association_proxy("class_map", "id", creator=class_map_creator) class_list = association_proxy("class_map", "id", creator=class_map_creator)
@ -155,8 +167,8 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan") character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator) attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1") ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry = relationship("Ancestry", uselist=False) ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
@property @property
def modifiers(self): def modifiers(self):
@ -255,7 +267,7 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
level_in_class = level_in_class[0] level_in_class = level_in_class[0]
level_in_class.level = level level_in_class.level = level
else: else:
self.class_list.append(CharacterClassMap(character_id=self.id, character_class=newclass, level=level)) self.class_list.append(CharacterClassMap(character=self, character_class=newclass, level=level))
for lvl in range(1, level + 1): for lvl in range(1, level + 1):
if not newclass.attributes_by_level[lvl]: if not newclass.attributes_by_level[lvl]:
continue continue

View File

@ -1,9 +1,9 @@
from collections import defaultdict from collections import defaultdict
from sqlalchemy import Column, Enum, ForeignKey, Integer, String from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, StatsEnum from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin
__all__ = [ __all__ = [
"ClassAttributeMap", "ClassAttributeMap",
@ -15,16 +15,16 @@ __all__ = [
class ClassAttributeMap(BaseObject): class ClassAttributeMap(BaseObject):
__tablename__ = "class_attribute_map" __tablename__ = "class_attribute_map"
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True) class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), primary_key=True)
character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True) character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), primary_key=True)
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1) level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
attribute = relationship("ClassAttribute", uselist=False, viewonly=True, lazy="immediate") attribute = relationship("ClassAttribute", uselist=False, viewonly=True, lazy="immediate")
class ClassAttribute(BaseObject): class ClassAttribute(BaseObject):
__tablename__ = "class_attribute" __tablename__ = "class_attribute"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate") options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate")
def __repr__(self): def __repr__(self):
@ -33,18 +33,18 @@ class ClassAttribute(BaseObject):
class ClassAttributeOption(BaseObject): class ClassAttributeOption(BaseObject):
__tablename__ = "class_attribute_option" __tablename__ = "class_attribute_option"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
attribute_id = Column(Integer, ForeignKey("class_attribute.id"), nullable=False) attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin): class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character_class" __tablename__ = "character_class"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True) name: Mapped[str] = mapped_column(index=True, unique=True)
hit_dice = Column(String, default="1d6") hit_dice: Mapped[str] = mapped_column(default="1d6")
hit_dice_stat = Column(Enum(StatsEnum)) hit_dice_stat: Mapped[str] = mapped_column(default="")
proficiencies = Column(String) proficiencies: Mapped[str] = mapped_column(default="")
attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate") attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate")
@property @property

View File

@ -1,8 +1,8 @@
from collections import defaultdict from collections import defaultdict
from sqlalchemy import Column, Float, ForeignKey, Integer, String, UniqueConstraint from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject from ttfrog.db.base import BaseObject
@ -14,12 +14,12 @@ class ModifierMap(BaseObject):
__tablename__ = "modifier_map" __tablename__ = "modifier_map"
__table_args__ = (UniqueConstraint("primary_table_name", "primary_table_id", "modifier_id"),) __table_args__ = (UniqueConstraint("primary_table_name", "primary_table_id", "modifier_id"),)
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
modifier_id = Column(Integer, ForeignKey("modifier.id"), nullable=False) modifier_id: Mapped[int] = mapped_column(ForeignKey("modifier.id"), init=False)
modifier = relationship("Modifier", uselist=False, lazy="immediate") modifier: Mapped["Modifier"] = relationship(uselist=False, lazy="immediate")
primary_table_name = Column(String, nullable=False) primary_table_name: Mapped[str] = mapped_column(nullable=False)
primary_table_id = Column(Integer, nullable=False) primary_table_id: Mapped[int] = mapped_column(nullable=False)
class Modifier(BaseObject): class Modifier(BaseObject):
@ -31,14 +31,14 @@ class Modifier(BaseObject):
""" """
__tablename__ = "modifier" __tablename__ = "modifier"
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
target = Column(String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
absolute_value = Column(Integer) target: Mapped[str] = mapped_column(nullable=False)
relative_value = Column(Integer) absolute_value: Mapped[int] = mapped_column(nullable=True, default=None)
multiply_value = Column(Float) relative_value: Mapped[int] = mapped_column(nullable=True, default=None)
new_value = Column(String) multiply_value: Mapped[float] = mapped_column(nullable=True, default=None)
name = Column(String, nullable=False) new_value: Mapped[str] = mapped_column(nullable=True, default=None)
description = Column(String) description: Mapped[str] = mapped_column(default="")
class ModifierMixin: class ModifierMixin:
@ -56,14 +56,16 @@ class ModifierMixin:
Example: Example:
>>> class Item(BaseObject, ModifierMixin): >>> class Item(BaseObject, ModifierMixin):
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(nullable=False)
>>> dwarven_belt = Item(name="Dwarven Belt") >>> dwarven_belt = Item(name="Dwarven Belt")
>>> dwarven_belt.add_modifier(Modifier(name="STR+1", target="strength", relative_value=1)) >>> dwarven_belt.add_modifier(Modifier(name="STR+1", target="strength", relative_value=1))
>>> dwarven_belt.modifiers >>> dwarven_belt.modifiers
{'strength': [Modifier(id=1, target='strength', name='STR+1', relative_value=1 ... ]} {'strength': [Modifier(id=1, target='strength', name='STR+1', relative_value=1 ... ]}
""" """
_modifiable_attributes = dict()
@declared_attr @declared_attr
def modifier_map(cls): def modifier_map(cls):
return relationship( return relationship(
@ -104,7 +106,36 @@ class ModifierMixin:
return True return True
def remove_modifier(self, modifier): def remove_modifier(self, modifier):
if modifier.id not in [mod.modifier_id for mod in self.modifier_map]: if modifier not in self.modifiers[modifier.target]:
return False return False
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier] self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
return True return True
def apply_modifiers(self, target, initial):
if not self._modifiable_attributes:
raise NotImplementedError(
f"You must define the '_modifiable_attributes' property on {self.__class__.__name__}."
)
modifiers = list(reversed(self.modifiers.get(target, [])))
if initial is None:
return initial
if isinstance(initial, int):
absolute = [mod for mod in modifiers if mod.absolute_value is not None]
if absolute:
return absolute[0].absolute_value
multiple = [mod for mod in modifiers if mod.multiply_value is not None]
if multiple:
return int(initial * multiple[0].multiply_value + 0.5)
return initial + sum(mod.relative_value for mod in modifiers if mod.relative_value is not None)
new = [mod for mod in modifiers if mod.new_value is not None]
if new:
return new[0].new_value
return initial
def __getattr__(self, attr_name):
prop = self._modifiable_attributes.get(attr_name, None)
if not prop:
raise AttributeError(f"Attribute not found: {attr_name}")
return self.apply_modifiers(attr_name, getattr(self, prop))