implemented modifiers on char stats

This commit is contained in:
evilchili 2024-04-21 02:17:47 -07:00
parent 5ec27e9344
commit 36f6f831d9
3 changed files with 210 additions and 27 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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"