refactor modifiers

This commit is contained in:
evilchili 2024-04-21 21:30:24 -07:00
parent 36f6f831d9
commit 5db6e40eae
6 changed files with 154 additions and 86 deletions

View File

@ -59,8 +59,11 @@ class SQLDatabaseManager:
tm.abort() tm.abort()
raise raise
def add_or_update(self, *args, **kwargs): def add_or_update(self, record, *args, **kwargs):
self.session.add(*args, **kwargs) if not isinstance(record, list):
record = [record]
for rec in record:
self.session.add(rec, *args, **kwargs)
self.session.flush() self.session.flush()
def query(self, *args, **kwargs): def query(self, *args, **kwargs):

View File

@ -1,4 +1,5 @@
from .character import * from .character import *
from .classes import * from .classes import *
from .log import * from .log import *
from .modifiers import *
from .property import * from .property import *

View File

@ -1,16 +1,14 @@
from collections import defaultdict from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy import Column, Enum, ForeignKey, Integer, Float, String, Text, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, validates from sqlalchemy.orm import relationship
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin
__all__ = [ __all__ = [
"Ancestry", "Ancestry",
"AncestryTrait", "AncestryTrait",
"AncestryTraitMap", "AncestryTraitMap",
"AncestryModifier",
"CharacterClassMap", "CharacterClassMap",
"CharacterClassAttributeMap", "CharacterClassAttributeMap",
"Character", "Character",
@ -40,10 +38,9 @@ class AncestryTraitMap(BaseObject):
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}) level = Column(Integer, nullable=False, info={"min": 1, "max": 20})
# XXX: Replace this with a many-to-many on the Modifiers table. Will need for proficiecies too. class Ancestry(BaseObject, ModifierMixin):
class Ancestry(BaseObject):
""" """
A character ancestry ("race"), which has zero or more AncestryTraits. A character ancestry ("race"), which has zero or more AncestryTraits and Modifiers.
""" """
__tablename__ = "ancestry" __tablename__ = "ancestry"
@ -52,8 +49,7 @@ class Ancestry(BaseObject):
creature_type = Column(Enum(CreatureTypesEnum), nullable=False, default="humanoid") creature_type = Column(Enum(CreatureTypesEnum), nullable=False, default="humanoid")
size = Column(Enum(SizesEnum), nullable=False, default="Medium") size = Column(Enum(SizesEnum), nullable=False, default="Medium")
speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99}) speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99})
_traits = relationship("AncestryTraitMap", lazy="immediate") _traits = relationship("AncestryTraitMap", cascade="all,delete,delete-orphan", lazy="immediate")
modifiers = relationship("AncestryModifier", lazy="immediate")
@property @property
def traits(self): def traits(self):
@ -65,33 +61,10 @@ class Ancestry(BaseObject):
return True return True
return False return False
def add_modifier(self, modifier):
if modifier not in self.modifiers:
self.modifiers.append(modifier)
return True
return False
def __repr__(self): def __repr__(self):
return self.name return self.name
class AncestryModifier(BaseObject):
"""
A modifier granted to a character via its Ancestry.
"""
__tablename__ = "ancestry_modifier"
id = Column(Integer, primary_key=True, autoincrement=True)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
name = Column(String, nullable=False)
target = Column(String, nullable=False)
absolute_value = Column(Integer)
relative_value = Column(Integer)
multiply_value = Column(Float)
new_value = Column(String)
description = Column(String, nullable=False)
class AncestryTrait(BaseObject): class AncestryTrait(BaseObject):
""" """
A trait granted to a character via its Ancestry. A trait granted to a character via its Ancestry.
@ -142,20 +115,7 @@ class CharacterClassAttributeMap(BaseObject):
) )
class Modifier(BaseObject): class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "modifier"
id = Column(Integer, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
target = Column(String, nullable=False)
absolute_value = Column(Integer)
relative_value = Column(Integer)
multiply_value = Column(Float)
new_value = Column(String)
description = Column(String, nullable=False)
class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character" __tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, default="New Character", nullable=False) name = Column(String, default="New Character", nullable=False)
@ -174,7 +134,6 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
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)
_modifiers = relationship("Modifier", cascade="all,delete,delete-orphan", lazy="immediate")
_modify_ok = [ _modify_ok = [
"armor_class", "armor_class",
"max_hit_points", "max_hit_points",
@ -196,12 +155,10 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
@property @property
def modifiers(self): def modifiers(self):
all_modifiers = defaultdict(list) unified = {}
for mod in self.ancestry.modifiers: unified.update(**self.ancestry.modifiers)
all_modifiers[mod.target].append(mod) unified.update(**super().modifiers)
for mod in self._modifiers: return unified
all_modifiers[mod.target].append(mod)
return all_modifiers
@property @property
def classes(self): def classes(self):
@ -302,7 +259,6 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
return True return True
return False return False
def apply_modifiers(self, target, initial): def apply_modifiers(self, target, initial):
modifiers = list(reversed(self.modifiers.get(target, []))) modifiers = list(reversed(self.modifiers.get(target, [])))
if isinstance(initial, int): if isinstance(initial, int):
@ -318,13 +274,3 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
if new: if new:
return new[0].new_value return new[0].new_value
return initial return initial
def add_modifier(self, modifier):
if modifier.absolute_value is not None and modifier.relative_value is not None and modifier.multiple_value:
raise AttributeError(f"You must provide only one of absolute, relative, and multiple values {modifier}.")
self._modifiers.append(modifier)
def remove_modifier(self, modifier):
self._modifiers = [mod for mod in self._modifiers if mod != modifier]

View File

@ -0,0 +1,102 @@
from collections import defaultdict
from sqlalchemy import Column, Float, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship
from ttfrog.db.base import BaseObject
class ModifierMap(BaseObject):
"""
Creates a many-to-many between Modifier and any model inheriting from the ModifierMixin.
"""
__tablename__ = "modifier_map"
__table_args__ = (UniqueConstraint("primary_table_name", "primary_table_id", "modifier_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
primary_table_name = Column(String, nullable=False)
primary_table_id = Column(Integer, nullable=False)
modifier_id = Column(Integer, ForeignKey("modifier.id"), nullable=False)
modifier = relationship("Modifier", uselist=False, lazy="immediate")
class Modifier(BaseObject):
"""
Modifiers modify the base value of an existing attribute on another table.
Modifiers are applied by the Character class, but may be associated with any model via the
ModifierMixIn model; refer to the Ancestry class for an example.
"""
__tablename__ = "modifier"
id = Column(Integer, primary_key=True, autoincrement=True)
target = Column(String, nullable=False)
absolute_value = Column(Integer)
relative_value = Column(Integer)
multiply_value = Column(Float)
new_value = Column(String)
name = Column(String, nullable=False)
description = Column(String)
class ModifierMixin:
"""
Add modifiers to an existing class.
Attributes:
modifier_map - get/set a list of Modifier records associated with the parent
modifiers - read-only dict of lists of modifiers keyed on Modifier.target
Methods:
add_modifier - Add a Modifier association to the modifier_map
remove_modifier - Remove a modifier association from the modifier_map
Example:
>>> class Item(BaseObject, ModifierMixin):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
>>> dwarven_belt = Item(name="Dwarven Belt")
>>> dwarven_belt.add_modifier(Modifier(name="STR+1", target="strength", relative_value=1))
>>> dwarven_belt.modifiers
{'strength': [Modifier(id=1, target='strength', name='STR+1', relative_value=1 ... ]}
"""
@declared_attr
def modifier_map(cls):
return relationship(
"ModifierMap",
primaryjoin=f"ModifierMap.primary_table_id == foreign({cls.__name__}.id)",
cascade="all,delete,delete-orphan",
single_parent=True,
uselist=True,
lazy="immediate",
)
@property
def modifiers(self):
all_modifiers = defaultdict(list)
for mapping in self.modifier_map:
all_modifiers[mapping.modifier.target].append(mapping.modifier)
return all_modifiers
def add_modifier(self, modifier):
if modifier.absolute_value is not None and modifier.relative_value is not None and modifier.multiple_value:
raise AttributeError(f"You must provide only one of absolute, relative, and multiple values {modifier}.")
if [mod for mod in self.modifier_map if mod.modifier == modifier]:
return False
self.modifier_map.append(
ModifierMap(
primary_table_name=self.__tablename__,
primary_table_id=self.id,
modifier=modifier,
)
)
return True
def remove_modifier(self, modifier):
if modifier.id not in [mod.modifier_id for mod in self.modifier_map]:
return False
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
return True

16
test/test_load.py Normal file
View File

@ -0,0 +1,16 @@
import pytest
from ttfrog.db import schema
@pytest.mark.skip
def test_many_records(db):
with db.transaction():
for i in range(1, 1000):
obj = schema.Ancestry(name=f"{i}-ancestry")
db.add_or_update(obj)
assert obj.id == i
for i in range(1, 1000):
obj = schema.Character(name=f"{i}-char")
db.add_or_update(obj)

View File

@ -113,16 +113,15 @@ def test_ancestries(db):
assert endurance in porc.traits assert endurance in porc.traits
# add a +1 STR modifier # add a +1 STR modifier
str_plus_one = schema.AncestryModifier( str_plus_one = schema.Modifier(
name="STR+1 (Pygmy Orc)", name="STR+1 (Pygmy Orc)",
target="strength", target="strength",
relative_value=1, relative_value=1,
description="Your Strength score is increased by 1." description="Your Strength score is increased by 1.",
) )
assert porc.add_modifier(str_plus_one) is True assert porc.add_modifier(str_plus_one) is True
assert porc.add_modifier(str_plus_one) is False # test idempotency assert porc.add_modifier(str_plus_one) is False # test idempotency
db.add_or_update(porc) assert str_plus_one in porc.modifiers["strength"]
assert str_plus_one in porc.modifiers
# now create an orc character and assert it gets traits and modifiers # now create an orc character and assert it gets traits and modifiers
grognak = schema.Character(name="Grognak the Mighty", ancestry=porc) grognak = schema.Character(name="Grognak the Mighty", ancestry=porc)
@ -131,7 +130,7 @@ def test_ancestries(db):
# verify the strength bonus is applied # verify the strength bonus is applied
assert grognak.strength == 10 assert grognak.strength == 10
assert str_plus_one in grognak.modifiers['strength'] assert str_plus_one in grognak.modifiers["strength"]
assert grognak.STR == 11 assert grognak.STR == 11
@ -145,37 +144,38 @@ def test_modifiers(db, classes_factory, ancestries_factory):
db.add_or_update(carl) db.add_or_update(carl)
assert carl.speed == carl.ancestry.speed == 30 assert carl.speed == carl.ancestry.speed == 30
cold = schema.Modifier(target="speed", relative_value=-10, name="Cold")
hasted = schema.Modifier(target="speed", multiply_value=2.0, name="Hasted")
slowed = schema.Modifier(target="speed", multiply_value=0.5, name="Slowed")
restrained = schema.Modifier(target="speed", absolute_value=0, name="Restrained")
reduced = schema.Modifier(target="size", new_value="Tiny", name="Reduced")
# reduce speed by 10 # reduce speed by 10
cold = schema.Modifier(target="speed", relative_value=-10, description="Cold") assert carl.add_modifier(cold)
carl.add_modifier(cold)
assert carl.speed == 20 assert carl.speed == 20
# speed is doubled # speed is doubled
carl.remove_modifier(cold) assert carl.remove_modifier(cold)
hasted = schema.Modifier(target="speed", multiply_value=2.0, description="Hasted") assert carl.add_modifier(hasted)
carl.add_modifier(hasted)
assert carl.speed == 60 assert carl.speed == 60
# speed is halved # speed is halved
slowed = schema.Modifier(target="speed", multiply_value=0.5, description="Slowed") assert carl.remove_modifier(hasted)
carl.remove_modifier(hasted) assert carl.add_modifier(slowed)
carl.add_modifier(slowed)
assert carl.speed == 15 assert carl.speed == 15
# speed is 0 # speed is 0
restrained = schema.Modifier(target="speed", absolute_value=0, description="Restrained") assert carl.add_modifier(restrained)
carl.add_modifier(restrained)
assert carl.speed == 0 assert carl.speed == 0
# no longer restrained, but still slowed # no longer restrained, but still slowed
carl.remove_modifier(restrained) assert carl.remove_modifier(restrained)
assert carl.speed == 15 assert carl.speed == 15
# back to normal # back to normal
carl.remove_modifier(slowed) assert carl.remove_modifier(slowed)
assert carl.speed == carl.ancestry.speed assert carl.speed == carl.ancestry.speed
# modifiers can modify string values too # modifiers can modify string values too
carl.add_modifier(schema.Modifier(target="size", new_value="Tiny", description="Reduced")) assert carl.add_modifier(reduced)
assert carl.size == "Tiny" assert carl.size == "Tiny"