modifiable columns subclass int/str

This commit is contained in:
evilchili 2024-04-29 01:09:58 -07:00
parent 3980be5f07
commit 3292b11d89
4 changed files with 91 additions and 91 deletions

View File

@ -26,7 +26,7 @@ def bootstrap():
rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX") rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX")
# characters # characters
sabetha = schema.Character("Sabetha", ancestry=tiefling) sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14)
sabetha.add_class(fighter, level=2) sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3) sabetha.add_class(rogue, level=3)
@ -34,3 +34,5 @@ def bootstrap():
# 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])
print(f"{sabetha.intelligence.bonus = }, {sabetha.size = }")

View File

@ -76,7 +76,7 @@ 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):
self.session.configure(bind=self.engine, autoflush=False) self.session.configure(bind=self.engine)
self.metadata.bind = self.engine self.metadata.bind = self.engine
self.metadata.create_all(self.engine) self.metadata.create_all(self.engine)

View File

@ -4,7 +4,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, SlugMixin from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, SlugMixin
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute 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__ = [ __all__ = [
"Ancestry", "Ancestry",
@ -141,21 +141,36 @@ class CharacterClassAttributeMap(BaseObject):
) )
class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin): class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierMixin):
__tablename__ = "character" __tablename__ = "character"
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(default="New Character", nullable=False) 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}) _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}) _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}) _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}) 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}) _strength: Mapped[int] = mapped_column(
dexterity: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30}) 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}) )
intelligence: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30}) _dexterity: Mapped[int] = mapped_column(
wisdom: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30}) 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}) )
_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}) _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): def traits(self):
return self.ancestry.traits 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 @property
def speed(self): def speed(self):
return self.apply_modifiers("speed", self.ancestry.speed) return self.apply_modifiers("speed", self.ancestry.speed)
@ -233,15 +216,11 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
@property @property
def fly_speed(self): 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 @property
def size(self): def size(self):
return self.apply_modifiers("size", self.ancestry.size) return self._apply_modifiers("size", self.ancestry.size)
@property
def vision(self):
return self.apply_modifiers("vision", self._vision)
@property @property
def vision_in_darkness(self): def vision_in_darkness(self):
@ -295,21 +274,3 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
) )
return True return True
return False 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

View File

@ -7,6 +7,34 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject 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): class ModifierMap(BaseObject):
""" """
Creates a many-to-many between Modifier and any model inheriting from the ModifierMixin. 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 ... ]} {'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(
@ -111,31 +137,42 @@ class ModifierMixin:
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): def _apply_modifiers(self, target, initial, modify_class=None):
if not self._modifiable_attributes: if not modify_class:
raise NotImplementedError( modify_class = globals()["ModifiableInt"] if isinstance(initial, int) else globals()["ModifiableStr"]
f"You must define the '_modifiable_attributes' property on {self.__class__.__name__}."
)
# get the modifiers in order from most to least recent
modifiers = list(reversed(self.modifiers.get(target, []))) modifiers = list(reversed(self.modifiers.get(target, [])))
if initial is None:
return initial
if isinstance(initial, int): if isinstance(initial, int):
absolute = [mod for mod in modifiers if mod.absolute_value is not None] absolute = [mod for mod in modifiers if mod.absolute_value is not None]
if absolute: if absolute:
return absolute[0].absolute_value modified = absolute[0].absolute_value
else:
multiple = [mod for mod in modifiers if mod.multiply_value is not None] multiple = [mod for mod in modifiers if mod.multiply_value is not None]
if multiple: if multiple:
return int(initial * multiple[0].multiply_value + 0.5) modified = 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) 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] new = [mod for mod in modifiers if mod.new_value is not None]
if new: if new:
return new[0].new_value modified = new[0].new_value
return initial else:
modified = 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): def __getattr__(self, attr_name):
prop = self._modifiable_attributes.get(attr_name, None) col = getattr(self.__table__.columns, f"_{attr_name}", None)
if not prop: if col is not None and col.info.get("modify", False):
raise AttributeError(f"Attribute not found: {attr_name}") return self._apply_modifiers(
return self.apply_modifiers(attr_name, getattr(self, prop)) attr_name, getattr(self, col.name), modify_class=col.info.get("modify_class", None)
)
return super().__getattr__(attr_name)