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.orm import relationship
|
||||
from sqlalchemy.orm import relationship, validates
|
||||
|
||||
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin
|
||||
|
||||
|
@ -8,9 +10,11 @@ __all__ = [
|
|||
"Ancestry",
|
||||
"AncestryTrait",
|
||||
"AncestryTraitMap",
|
||||
"AncestryModifier",
|
||||
"CharacterClassMap",
|
||||
"CharacterClassAttributeMap",
|
||||
"Character",
|
||||
"Modifier",
|
||||
]
|
||||
|
||||
|
||||
|
@ -36,6 +40,7 @@ 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):
|
||||
"""
|
||||
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")
|
||||
speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99})
|
||||
_traits = relationship("AncestryTraitMap", lazy="immediate")
|
||||
modifiers = relationship("AncestryModifier", lazy="immediate")
|
||||
|
||||
@property
|
||||
def traits(self):
|
||||
|
@ -59,10 +65,33 @@ 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.
|
||||
|
@ -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):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, default="New Character", nullable=False)
|
||||
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})
|
||||
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})
|
||||
str = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
dex = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
con = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
int = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
wis = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
cha = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
strength = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
dexterity = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
constitution = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
intelligence = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
|
||||
wisdom = 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)
|
||||
|
||||
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",
|
||||
"strength",
|
||||
"dexterity",
|
||||
"constitution",
|
||||
"intelligence",
|
||||
"wisdom",
|
||||
"charisma",
|
||||
"speed",
|
||||
"size",
|
||||
]
|
||||
|
||||
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
||||
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 = 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
|
||||
def classes(self):
|
||||
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
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.ancestry.size
|
||||
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.ancestry.speed
|
||||
return self.apply_modifiers("speed", self.ancestry.speed)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.apply_modifiers("size", self.ancestry.size)
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
|
@ -204,3 +301,30 @@ 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):
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"Skill",
|
||||
"Proficiency",
|
||||
"Modifier",
|
||||
]
|
||||
|
||||
|
||||
|
@ -26,14 +25,3 @@ class Proficiency(BaseObject):
|
|||
|
||||
def __repr__(self):
|
||||
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")
|
||||
db.add_or_update(char)
|
||||
assert char.id == 1
|
||||
assert char.armor_class == 10
|
||||
assert char.name == "Test Character"
|
||||
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
|
||||
|
||||
# switch ancestry to tiefling
|
||||
|
@ -105,6 +112,70 @@ def test_ancestries(db):
|
|||
db.add_or_update(porc)
|
||||
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)
|
||||
db.add_or_update(grognak)
|
||||
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