modifiable columns subclass int/str
This commit is contained in:
parent
3980be5f07
commit
3292b11d89
|
@ -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 = }")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user