implemented modifiers on char stats
This commit is contained in:
parent
5ec27e9344
commit
36f6f831d9
|
@ -1,6 +1,8 @@
|
||||||
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
|
from collections import defaultdict
|
||||||
|
|
||||||
|
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
|
from sqlalchemy.orm import relationship, validates
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin
|
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin
|
||||||
|
|
||||||
|
@ -8,9 +10,11 @@ __all__ = [
|
||||||
"Ancestry",
|
"Ancestry",
|
||||||
"AncestryTrait",
|
"AncestryTrait",
|
||||||
"AncestryTraitMap",
|
"AncestryTraitMap",
|
||||||
|
"AncestryModifier",
|
||||||
"CharacterClassMap",
|
"CharacterClassMap",
|
||||||
"CharacterClassAttributeMap",
|
"CharacterClassAttributeMap",
|
||||||
"Character",
|
"Character",
|
||||||
|
"Modifier",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +40,7 @@ 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):
|
class Ancestry(BaseObject):
|
||||||
"""
|
"""
|
||||||
A character ancestry ("race"), which has zero or more AncestryTraits.
|
A character ancestry ("race"), which has zero or more AncestryTraits.
|
||||||
|
@ -48,6 +53,7 @@ class Ancestry(BaseObject):
|
||||||
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", lazy="immediate")
|
||||||
|
modifiers = relationship("AncestryModifier", lazy="immediate")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def traits(self):
|
def traits(self):
|
||||||
|
@ -59,10 +65,33 @@ 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.
|
||||||
|
@ -113,31 +142,67 @@ 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, 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)
|
||||||
armor_class = Column(Integer, default=10, nullable=False, info={"min": 1, "max": 99})
|
armor_class = Column(Integer, default=10, nullable=False, info={"min": 1, "max": 99})
|
||||||
hit_points = Column(Integer, default=1, nullable=False, info={"min": 0, "max": 999})
|
hit_points = Column(Integer, default=1, nullable=False, info={"min": 0, "max": 999})
|
||||||
max_hit_points = Column(Integer, default=1, nullable=False, info={"min": 0, "max": 999})
|
max_hit_points = Column(Integer, default=10, nullable=False, info={"min": 0, "max": 999})
|
||||||
temp_hit_points = Column(Integer, default=0, nullable=False, info={"min": 0, "max": 999})
|
temp_hit_points = Column(Integer, default=0, nullable=False, info={"min": 0, "max": 999})
|
||||||
str = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
strength = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
dex = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
dexterity = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
con = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
constitution = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
int = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
intelligence = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
wis = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
wisdom = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
cha = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
charisma = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||||
proficiencies = Column(String)
|
proficiencies = Column(String)
|
||||||
|
|
||||||
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 = [
|
||||||
|
"armor_class",
|
||||||
|
"max_hit_points",
|
||||||
|
"strength",
|
||||||
|
"dexterity",
|
||||||
|
"constitution",
|
||||||
|
"intelligence",
|
||||||
|
"wisdom",
|
||||||
|
"charisma",
|
||||||
|
"speed",
|
||||||
|
"size",
|
||||||
|
]
|
||||||
|
|
||||||
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
||||||
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
|
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
|
||||||
|
|
||||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
|
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
|
||||||
ancestry = relationship("Ancestry", uselist=False)
|
ancestry = relationship("Ancestry", uselist=False)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def classes(self):
|
def classes(self):
|
||||||
return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map])
|
return dict([(mapping.character_class.name, mapping.character_class) for mapping in self.class_map])
|
||||||
|
@ -147,12 +212,44 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
||||||
return self.ancestry.traits
|
return self.ancestry.traits
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def AC(self):
|
||||||
return self.ancestry.size
|
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.ancestry.speed
|
return self.apply_modifiers("speed", self.ancestry.speed)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
return self.apply_modifiers("size", self.ancestry.size)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def level(self):
|
def level(self):
|
||||||
|
@ -204,3 +301,30 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def apply_modifiers(self, target, initial):
|
||||||
|
modifiers = list(reversed(self.modifiers.get(target, [])))
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from sqlalchemy import Column, Integer, String, Text, UniqueConstraint
|
from sqlalchemy import Column, Integer, String, Text
|
||||||
|
|
||||||
from ttfrog.db.base import BaseObject
|
from ttfrog.db.base import BaseObject
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Skill",
|
"Skill",
|
||||||
"Proficiency",
|
"Proficiency",
|
||||||
"Modifier",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,14 +25,3 @@ class Proficiency(BaseObject):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
|
||||||
class Modifier(BaseObject):
|
|
||||||
__tablename__ = "modifier"
|
|
||||||
__table_args__ = (UniqueConstraint("source_table_name", "source_table_id", "value", "type", "target"),)
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
source_table_name = Column(String, index=True, nullable=False)
|
|
||||||
source_table_id = Column(Integer, index=True, nullable=False)
|
|
||||||
value = Column(String, nullable=False)
|
|
||||||
type = Column(String, nullable=False)
|
|
||||||
target = Column(String, nullable=False)
|
|
||||||
|
|
|
@ -14,9 +14,16 @@ def test_manage_character(db, classes_factory, ancestries_factory):
|
||||||
char = schema.Character(name="Test Character")
|
char = schema.Character(name="Test Character")
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.id == 1
|
assert char.id == 1
|
||||||
assert char.armor_class == 10
|
|
||||||
assert char.name == "Test Character"
|
assert char.name == "Test Character"
|
||||||
assert char.ancestry.name == "human"
|
assert char.ancestry.name == "human"
|
||||||
|
assert char.AC == 10
|
||||||
|
assert char.HP == 10
|
||||||
|
assert char.STR == 10
|
||||||
|
assert char.DEX == 10
|
||||||
|
assert char.CON == 10
|
||||||
|
assert char.INT == 10
|
||||||
|
assert char.WIS == 10
|
||||||
|
assert char.CHA == 10
|
||||||
assert darkvision not in char.traits
|
assert darkvision not in char.traits
|
||||||
|
|
||||||
# switch ancestry to tiefling
|
# switch ancestry to tiefling
|
||||||
|
@ -105,6 +112,70 @@ def test_ancestries(db):
|
||||||
db.add_or_update(porc)
|
db.add_or_update(porc)
|
||||||
assert endurance in porc.traits
|
assert endurance in porc.traits
|
||||||
|
|
||||||
# now create an orc character and assert it gets Relentless Endurance
|
# add a +1 STR modifier
|
||||||
|
str_plus_one = schema.AncestryModifier(
|
||||||
|
name="STR+1 (Pygmy Orc)",
|
||||||
|
target="strength",
|
||||||
|
relative_value=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
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
db.add_or_update(grognak)
|
||||||
assert endurance in grognak.traits
|
assert endurance in grognak.traits
|
||||||
|
|
||||||
|
# verify the strength bonus is applied
|
||||||
|
assert grognak.strength == 10
|
||||||
|
assert str_plus_one in grognak.modifiers['strength']
|
||||||
|
assert grognak.STR == 11
|
||||||
|
|
||||||
|
|
||||||
|
def test_modifiers(db, classes_factory, ancestries_factory):
|
||||||
|
with db.transaction():
|
||||||
|
classes_factory()
|
||||||
|
ancestries = ancestries_factory()
|
||||||
|
|
||||||
|
# no modifiers; speed is ancestry speed
|
||||||
|
carl = schema.Character(name="Carl", ancestry=ancestries["elf"])
|
||||||
|
db.add_or_update(carl)
|
||||||
|
assert carl.speed == carl.ancestry.speed == 30
|
||||||
|
|
||||||
|
# reduce speed by 10
|
||||||
|
cold = schema.Modifier(target="speed", relative_value=-10, description="Cold")
|
||||||
|
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.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.speed == 15
|
||||||
|
|
||||||
|
# speed is 0
|
||||||
|
restrained = schema.Modifier(target="speed", absolute_value=0, description="Restrained")
|
||||||
|
carl.add_modifier(restrained)
|
||||||
|
assert carl.speed == 0
|
||||||
|
|
||||||
|
# no longer restrained, but still slowed
|
||||||
|
carl.remove_modifier(restrained)
|
||||||
|
assert carl.speed == 15
|
||||||
|
|
||||||
|
# back to normal
|
||||||
|
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.size == "Tiny"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user