refactor modifiers
This commit is contained in:
parent
36f6f831d9
commit
5db6e40eae
|
@ -59,8 +59,11 @@ class SQLDatabaseManager:
|
|||
tm.abort()
|
||||
raise
|
||||
|
||||
def add_or_update(self, *args, **kwargs):
|
||||
self.session.add(*args, **kwargs)
|
||||
def add_or_update(self, record, *args, **kwargs):
|
||||
if not isinstance(record, list):
|
||||
record = [record]
|
||||
for rec in record:
|
||||
self.session.add(rec, *args, **kwargs)
|
||||
self.session.flush()
|
||||
|
||||
def query(self, *args, **kwargs):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from .character import *
|
||||
from .classes import *
|
||||
from .log import *
|
||||
from .modifiers import *
|
||||
from .property import *
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from sqlalchemy import Column, Enum, ForeignKey, Integer, Float, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
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.schema.modifiers import Modifier, ModifierMixin
|
||||
|
||||
__all__ = [
|
||||
"Ancestry",
|
||||
"AncestryTrait",
|
||||
"AncestryTraitMap",
|
||||
"AncestryModifier",
|
||||
"CharacterClassMap",
|
||||
"CharacterClassAttributeMap",
|
||||
"Character",
|
||||
|
@ -40,10 +38,9 @@ class AncestryTraitMap(BaseObject):
|
|||
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):
|
||||
class Ancestry(BaseObject, ModifierMixin):
|
||||
"""
|
||||
A character ancestry ("race"), which has zero or more AncestryTraits.
|
||||
A character ancestry ("race"), which has zero or more AncestryTraits and Modifiers.
|
||||
"""
|
||||
|
||||
__tablename__ = "ancestry"
|
||||
|
@ -52,8 +49,7 @@ class Ancestry(BaseObject):
|
|||
creature_type = Column(Enum(CreatureTypesEnum), nullable=False, default="humanoid")
|
||||
size = Column(Enum(SizesEnum), nullable=False, default="Medium")
|
||||
speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99})
|
||||
_traits = relationship("AncestryTraitMap", lazy="immediate")
|
||||
modifiers = relationship("AncestryModifier", lazy="immediate")
|
||||
_traits = relationship("AncestryTraitMap", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
|
||||
@property
|
||||
def traits(self):
|
||||
|
@ -65,33 +61,10 @@ class Ancestry(BaseObject):
|
|||
return True
|
||||
return False
|
||||
|
||||
def add_modifier(self, modifier):
|
||||
if modifier not in self.modifiers:
|
||||
self.modifiers.append(modifier)
|
||||
return True
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
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):
|
||||
"""
|
||||
A trait granted to a character via its Ancestry.
|
||||
|
@ -142,20 +115,7 @@ class CharacterClassAttributeMap(BaseObject):
|
|||
)
|
||||
|
||||
|
||||
class Modifier(BaseObject):
|
||||
__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):
|
||||
class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
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_list = association_proxy("class_map", "id", creator=class_map_creator)
|
||||
|
||||
_modifiers = relationship("Modifier", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
_modify_ok = [
|
||||
"armor_class",
|
||||
"max_hit_points",
|
||||
|
@ -196,12 +155,10 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
|||
|
||||
@property
|
||||
def modifiers(self):
|
||||
all_modifiers = defaultdict(list)
|
||||
for mod in self.ancestry.modifiers:
|
||||
all_modifiers[mod.target].append(mod)
|
||||
for mod in self._modifiers:
|
||||
all_modifiers[mod.target].append(mod)
|
||||
return all_modifiers
|
||||
unified = {}
|
||||
unified.update(**self.ancestry.modifiers)
|
||||
unified.update(**super().modifiers)
|
||||
return unified
|
||||
|
||||
@property
|
||||
def classes(self):
|
||||
|
@ -302,7 +259,6 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
|||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_modifiers(self, target, initial):
|
||||
modifiers = list(reversed(self.modifiers.get(target, [])))
|
||||
if isinstance(initial, int):
|
||||
|
@ -318,13 +274,3 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
|||
if new:
|
||||
return new[0].new_value
|
||||
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]
|
||||
|
|
102
src/ttfrog/db/schema/modifiers.py
Normal file
102
src/ttfrog/db/schema/modifiers.py
Normal 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
16
test/test_load.py
Normal 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)
|
|
@ -113,16 +113,15 @@ def test_ancestries(db):
|
|||
assert endurance in porc.traits
|
||||
|
||||
# add a +1 STR modifier
|
||||
str_plus_one = schema.AncestryModifier(
|
||||
str_plus_one = schema.Modifier(
|
||||
name="STR+1 (Pygmy Orc)",
|
||||
target="strength",
|
||||
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 False # test idempotency
|
||||
db.add_or_update(porc)
|
||||
assert str_plus_one in porc.modifiers
|
||||
assert str_plus_one in porc.modifiers["strength"]
|
||||
|
||||
# now create an orc character and assert it gets traits and modifiers
|
||||
grognak = schema.Character(name="Grognak the Mighty", ancestry=porc)
|
||||
|
@ -131,7 +130,7 @@ def test_ancestries(db):
|
|||
|
||||
# verify the strength bonus is applied
|
||||
assert grognak.strength == 10
|
||||
assert str_plus_one in grognak.modifiers['strength']
|
||||
assert str_plus_one in grognak.modifiers["strength"]
|
||||
assert grognak.STR == 11
|
||||
|
||||
|
||||
|
@ -145,37 +144,38 @@ def test_modifiers(db, classes_factory, ancestries_factory):
|
|||
db.add_or_update(carl)
|
||||
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
|
||||
cold = schema.Modifier(target="speed", relative_value=-10, description="Cold")
|
||||
carl.add_modifier(cold)
|
||||
assert carl.add_modifier(cold)
|
||||
assert carl.speed == 20
|
||||
|
||||
# speed is doubled
|
||||
carl.remove_modifier(cold)
|
||||
hasted = schema.Modifier(target="speed", multiply_value=2.0, description="Hasted")
|
||||
carl.add_modifier(hasted)
|
||||
assert carl.remove_modifier(cold)
|
||||
assert carl.add_modifier(hasted)
|
||||
assert carl.speed == 60
|
||||
|
||||
# speed is halved
|
||||
slowed = schema.Modifier(target="speed", multiply_value=0.5, description="Slowed")
|
||||
carl.remove_modifier(hasted)
|
||||
carl.add_modifier(slowed)
|
||||
assert carl.remove_modifier(hasted)
|
||||
assert carl.add_modifier(slowed)
|
||||
assert carl.speed == 15
|
||||
|
||||
# speed is 0
|
||||
restrained = schema.Modifier(target="speed", absolute_value=0, description="Restrained")
|
||||
carl.add_modifier(restrained)
|
||||
assert carl.add_modifier(restrained)
|
||||
assert carl.speed == 0
|
||||
|
||||
# no longer restrained, but still slowed
|
||||
carl.remove_modifier(restrained)
|
||||
assert carl.remove_modifier(restrained)
|
||||
assert carl.speed == 15
|
||||
|
||||
# back to normal
|
||||
carl.remove_modifier(slowed)
|
||||
assert carl.remove_modifier(slowed)
|
||||
assert carl.speed == carl.ancestry.speed
|
||||
|
||||
# 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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user