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")
# 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 = }")

View File

@ -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)

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.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

View File

@ -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)