diff --git a/src/ttfrog/db/bootstrap.py b/src/ttfrog/db/bootstrap.py index 33c10e7..6fbda6d 100644 --- a/src/ttfrog/db/bootstrap.py +++ b/src/ttfrog/db/bootstrap.py @@ -26,7 +26,7 @@ def bootstrap(): rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX") # characters - sabetha = schema.Character("Sabetha", ancestry=tiefling) + sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14) sabetha.add_class(fighter, level=2) sabetha.add_class(rogue, level=3) @@ -34,3 +34,5 @@ def bootstrap(): # persist all the records we've created db.add_or_update([sabetha, bob]) + + print(f"{sabetha.intelligence.bonus = }, {sabetha.size = }") diff --git a/src/ttfrog/db/manager.py b/src/ttfrog/db/manager.py index 1a3e507..83761fd 100644 --- a/src/ttfrog/db/manager.py +++ b/src/ttfrog/db/manager.py @@ -76,7 +76,7 @@ class SQLDatabaseManager: return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10] def init(self): - self.session.configure(bind=self.engine, autoflush=False) + self.session.configure(bind=self.engine) self.metadata.bind = self.engine self.metadata.create_all(self.engine) diff --git a/src/ttfrog/db/schema/character.py b/src/ttfrog/db/schema/character.py index 0910364..7c23e50 100644 --- a/src/ttfrog/db/schema/character.py +++ b/src/ttfrog/db/schema/character.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship 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, Stat __all__ = [ "Ancestry", @@ -141,21 +141,36 @@ class CharacterClassAttributeMap(BaseObject): ) -class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin): +class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierMixin): __tablename__ = "character" + id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) 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}) + _armor_class: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 1, "max": 99, "modify": True}) + _hit_points: Mapped[int] = mapped_column(default=1, nullable=False, info={"min": 0, "max": 999, "modify": True}) + _max_hit_points: Mapped[int] = mapped_column( + default=10, nullable=False, info={"min": 0, "max": 999, "modify": True} + ) 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}) + _strength: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) + _dexterity: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) + _constitution: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) + _intelligence: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) + _wisdom: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) + _charisma: Mapped[int] = mapped_column( + nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat} + ) _vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0}) @@ -187,38 +202,6 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM def traits(self): return self.ancestry.traits - @property - def AC(self): - return self.apply_modifiers("armor_class", self.armor_class) - - @property - def HP(self): - return self.apply_modifiers("max_hit_points", self.max_hit_points) - - @property - def STR(self): - return self.apply_modifiers("strength", self.strength) - - @property - def DEX(self): - return self.apply_modifiers("dexterity", self.dexterity) - - @property - def CON(self): - return self.apply_modifiers("constitution", self.constitution) - - @property - def INT(self): - return self.apply_modifiers("intelligence", self.intelligence) - - @property - def WIS(self): - return self.apply_modifiers("wisdom", self.wisdom) - - @property - def CHA(self): - return self.apply_modifiers("charisma", self.charisma) - @property def speed(self): return self.apply_modifiers("speed", self.ancestry.speed) @@ -233,15 +216,11 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM @property def fly_speed(self): - return self.apply_modifiers("fly_speed", self.ancestry._fly_speed) + return self.apply_modifiers("fly_speed", self.ancestry.fly_speed) @property def size(self): - return self.apply_modifiers("size", self.ancestry.size) - - @property - def vision(self): - return self.apply_modifiers("vision", self._vision) + return self._apply_modifiers("size", self.ancestry.size) @property def vision_in_darkness(self): @@ -295,21 +274,3 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM ) return True return False - - def apply_modifiers(self, target, initial): - 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 diff --git a/src/ttfrog/db/schema/modifiers.py b/src/ttfrog/db/schema/modifiers.py index 0ca62de..f915a8f 100644 --- a/src/ttfrog/db/schema/modifiers.py +++ b/src/ttfrog/db/schema/modifiers.py @@ -7,6 +7,34 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from ttfrog.db.base import BaseObject +class Modifiable: + def __new__(cls, base, modified=None): + cls.base = base + return super().__new__(cls, modified) + + +class ModifiableStr(Modifiable, str): + """ + A string that also has a '.base' property. + """ + + +class ModifiableInt(Modifiable, int): + """ + An integer that also has a '.base' property + """ + + +class Stat(ModifiableInt): + """ + Same as a Score except it also has a bonus for STR, DEX, CON, etc. + """ + + @property + def bonus(self): + return int((self - 10) / 2) + + class ModifierMap(BaseObject): """ Creates a many-to-many between Modifier and any model inheriting from the ModifierMixin. @@ -64,8 +92,6 @@ class ModifierMixin: {'strength': [Modifier(id=1, target='strength', name='STR+1', relative_value=1 ... ]} """ - _modifiable_attributes = dict() - @declared_attr def modifier_map(cls): return relationship( @@ -111,31 +137,42 @@ class ModifierMixin: self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier] 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__}." - ) + def _apply_modifiers(self, target, initial, modify_class=None): + if not modify_class: + modify_class = globals()["ModifiableInt"] if isinstance(initial, int) else globals()["ModifiableStr"] + # get the modifiers in order from most to least recent 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) + modified = absolute[0].absolute_value + else: + multiple = [mod for mod in modifiers if mod.multiply_value is not None] + if multiple: + modified = int(initial * multiple[0].multiply_value + 0.5) + else: + modified = initial + sum(mod.relative_value for mod in modifiers if mod.relative_value is not None) + else: + new = [mod for mod in modifiers if mod.new_value is not None] + if new: + modified = new[0].new_value + else: + modified = initial - new = [mod for mod in modifiers if mod.new_value is not None] - if new: - return new[0].new_value - return initial + return modify_class(base=initial, modified=modified) + + def __setattr__(self, attr_name, value): + col = getattr(self.__table__.columns, f"_{attr_name}", None) + if col is not None and col.info.get("modify", False): + raise AttributeError(f"You cannot set .{attr_name}. Did you mean ._{attr_name}?") + return super().__setattr__(attr_name, value) 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)) + col = getattr(self.__table__.columns, f"_{attr_name}", None) + if col is not None and col.info.get("modify", False): + return self._apply_modifiers( + attr_name, getattr(self, col.name), modify_class=col.info.get("modify_class", None) + ) + return super().__getattr__(attr_name)