fixing modifier bugs, fixing traits, adding speed attrs
This commit is contained in:
parent
5db6e40eae
commit
1ff0e5ca7d
|
@ -116,7 +116,7 @@ def dump(context: typer.Context):
|
|||
"""
|
||||
from ttfrog.db.manager import db
|
||||
|
||||
db.init()
|
||||
setup(context)
|
||||
print(db.dump(context.args))
|
||||
|
||||
|
||||
|
|
|
@ -1,192 +1,36 @@
|
|||
import logging
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from ttfrog.db import schema
|
||||
from ttfrog.db.manager import db
|
||||
|
||||
# move this to json or whatever
|
||||
data = {
|
||||
"CharacterClass": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "fighter",
|
||||
"hit_dice": "1d10",
|
||||
"hit_dice_stat": "CON",
|
||||
"proficiencies": "all armor, all shields, simple weapons, martial weapons",
|
||||
"saving_throws": ["STR, CON"],
|
||||
"skills": [
|
||||
"Acrobatics",
|
||||
"Animal Handling",
|
||||
"Athletics",
|
||||
"History",
|
||||
"Insight",
|
||||
"Intimidation",
|
||||
"Perception",
|
||||
"Survival",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "rogue",
|
||||
"hit_dice": "1d8",
|
||||
"hit_dice_stat": "DEX",
|
||||
"proficiencies": "simple weapons, hand crossbows, longswords, rapiers, shortswords",
|
||||
"saving_throws": ["DEX", "INT"],
|
||||
"skills": [
|
||||
"Acrobatics",
|
||||
"Athletics",
|
||||
"Deception",
|
||||
"Insight",
|
||||
"Intimidation",
|
||||
"Investigation",
|
||||
"Perception",
|
||||
"Performance",
|
||||
"Persuasion",
|
||||
"Sleight of Hand",
|
||||
"Stealth",
|
||||
],
|
||||
},
|
||||
],
|
||||
"Skill": [
|
||||
{"name": "Acrobatics"},
|
||||
{"name": "Animal Handling"},
|
||||
{"name": "Athletics"},
|
||||
{"name": "Deception"},
|
||||
{"name": "History"},
|
||||
{"name": "Insight"},
|
||||
{"name": "Intimidation"},
|
||||
{"name": "Investigation"},
|
||||
{"name": "Perception"},
|
||||
{"name": "Performance"},
|
||||
{"name": "Persuasion"},
|
||||
{"name": "Sleight of Hand"},
|
||||
{"name": "Stealth"},
|
||||
{"name": "Survival"},
|
||||
],
|
||||
"Ancestry": [
|
||||
{"id": 1, "name": "human", "creature_type": "humanoid"},
|
||||
{"id": 2, "name": "dragonborn", "creature_type": "humanoid"},
|
||||
{"id": 3, "name": "tiefling", "creature_type": "humanoid"},
|
||||
{"id": 4, "name": "elf", "creature_type": "humanoid"},
|
||||
],
|
||||
"AncestryTrait": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "+1 to All Ability Scores",
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Breath Weapon",
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Darkvision",
|
||||
},
|
||||
],
|
||||
"AncestryTraitMap": [
|
||||
{"ancestry_id": 1, "ancestry_trait_id": 1, "level": 1}, # human +1 to scores
|
||||
{"ancestry_id": 2, "ancestry_trait_id": 2, "level": 1}, # dragonborn breath weapon
|
||||
{"ancestry_id": 3, "ancestry_trait_id": 3, "level": 1}, # tiefling darkvision
|
||||
{"ancestry_id": 2, "ancestry_trait_id": 2, "level": 1}, # elf darkvision
|
||||
],
|
||||
"CharacterClassMap": [
|
||||
{
|
||||
"character_id": 1,
|
||||
"character_class_id": 1,
|
||||
"level": 2,
|
||||
},
|
||||
{
|
||||
"character_id": 1,
|
||||
"character_class_id": 2,
|
||||
"level": 3,
|
||||
},
|
||||
],
|
||||
"Character": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Sabetha",
|
||||
"ancestry_id": 1,
|
||||
"armor_class": 10,
|
||||
"max_hit_points": 14,
|
||||
"hit_points": 14,
|
||||
"temp_hit_points": 0,
|
||||
"speed": 30,
|
||||
"str": 16,
|
||||
"dex": 12,
|
||||
"con": 18,
|
||||
"int": 11,
|
||||
"wis": 12,
|
||||
"cha": 8,
|
||||
"proficiencies": "all armor, all shields, simple weapons, martial weapons",
|
||||
"saving_throws": ["STR", "CON"],
|
||||
"skills": ["Acrobatics", "Animal Handling"],
|
||||
},
|
||||
],
|
||||
"ClassAttribute": [
|
||||
{"id": 1, "name": "Fighting Style"},
|
||||
{"id": 2, "name": "Another Attribute"},
|
||||
],
|
||||
"ClassAttributeOption": [
|
||||
{"id": 1, "attribute_id": 1, "name": "Archery"},
|
||||
{"id": 2, "attribute_id": 1, "name": "Battlemaster"},
|
||||
{"id": 3, "attribute_id": 2, "name": "Another Option 1"},
|
||||
{"id": 4, "attribute_id": 2, "name": "Another Option 2"},
|
||||
],
|
||||
"ClassAttributeMap": [
|
||||
{"class_attribute_id": 1, "character_class_id": 1, "level": 2}, # Fighter: Fighting Style
|
||||
{"class_attribute_id": 2, "character_class_id": 1, "level": 1}, # Fighter: Another Attr
|
||||
],
|
||||
"CharacterClassAttributeMap": [
|
||||
{"character_id": 1, "class_attribute_id": 2, "option_id": 4}, # Sabetha, another option, option 2
|
||||
{"character_id": 1, "class_attribute_id": 1, "option_id": 1}, # Sabetha, fighting style, archery
|
||||
],
|
||||
"Modifier": [
|
||||
# Humans
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "str"},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "dex"},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "con"},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "int"},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "wis"},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 1, "value": "+1", "type": "stat", "target": "cha"},
|
||||
# Dragonborn
|
||||
{
|
||||
"source_table_name": "ancestry_trait",
|
||||
"source_table_id": 2,
|
||||
"value": "60",
|
||||
"type": "attribute ",
|
||||
"target": "Darkvision",
|
||||
},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 2, "value": "+1", "type": "stat", "target": ""},
|
||||
{"source_table_name": "ancestry_trait", "source_table_id": 2, "value": "+1", "type": "stat", "target": ""},
|
||||
# Fighting Style: Archery
|
||||
{
|
||||
"source_table_name": "class_attribute",
|
||||
"source_table_id": 1,
|
||||
"value": "+2",
|
||||
"type": "weapon ",
|
||||
"target": "ranged",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def bootstrap():
|
||||
"""
|
||||
Initialize the database with source data. Idempotent; will skip anything that already exists.
|
||||
"""
|
||||
db.metadata.drop_all(bind=db.engine)
|
||||
db.init()
|
||||
for table, records in data.items():
|
||||
model = getattr(schema, table)
|
||||
with db.transaction():
|
||||
# ancestries
|
||||
human = schema.Ancestry(name="human")
|
||||
tiefling = schema.Ancestry(name="tiefling")
|
||||
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="intelligence", relative_value=1))
|
||||
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="charisma", relative_value=2))
|
||||
darkvision = schema.AncestryTrait(
|
||||
name="Darkvision",
|
||||
description=(
|
||||
"You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it "
|
||||
"were dim light. You can’t discern color in darkness, only shades of gray."
|
||||
),
|
||||
)
|
||||
darkvision.add_modifier(schema.Modifier(name="Darkvision", target="vision_in_darkness", absolute_value=120))
|
||||
tiefling.add_trait(darkvision)
|
||||
|
||||
for rec in records:
|
||||
obj = model(**rec)
|
||||
try:
|
||||
with db.transaction():
|
||||
db.session.add(obj)
|
||||
logging.info(f"Created {table} {obj}")
|
||||
except IntegrityError as e:
|
||||
if "UNIQUE constraint failed" in str(e):
|
||||
logging.info(f"Skipping existing {table} {obj}")
|
||||
continue
|
||||
raise
|
||||
# classes
|
||||
fighter = schema.CharacterClass(name="fighter", hit_dice="1d10", hit_dice_stat="CON")
|
||||
rogue = schema.CharacterClass(name="rogue", hit_dice="1d8", hit_dice_stat="DEX")
|
||||
|
||||
# characters
|
||||
sabetha = schema.Character(name="Sabetha", ancestry=tiefling)
|
||||
sabetha.add_class(fighter, level=2)
|
||||
sabetha.add_class(rogue, level=3)
|
||||
|
||||
bob = schema.Character(name="Bob", ancestry=human)
|
||||
|
||||
# persist all the records we've created
|
||||
db.add_or_update([sabetha, bob])
|
||||
|
|
|
@ -34,7 +34,7 @@ class AncestryTraitMap(BaseObject):
|
|||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"))
|
||||
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"))
|
||||
trait = relationship("AncestryTrait", lazy="immediate")
|
||||
trait = relationship("AncestryTrait", uselist=False, lazy="immediate")
|
||||
level = Column(Integer, nullable=False, info={"min": 1, "max": 20})
|
||||
|
||||
|
||||
|
@ -48,16 +48,31 @@ class Ancestry(BaseObject, ModifierMixin):
|
|||
name = Column(String, index=True, unique=True)
|
||||
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})
|
||||
walk_speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99})
|
||||
_fly_speed = Column(Integer, info={"min": 0, "max": 99})
|
||||
_climb_speed = Column(Integer, info={"min": 0, "max": 99})
|
||||
_swim_speed = Column(Integer, info={"min": 0, "max": 99})
|
||||
_traits = relationship("AncestryTraitMap", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||
|
||||
@property
|
||||
def traits(self):
|
||||
return [mapping.trait for mapping in self._traits]
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
return self.walk_speed
|
||||
|
||||
@property
|
||||
def climb_speed(self):
|
||||
return self._climb_speed or int(self.speed / 2)
|
||||
|
||||
@property
|
||||
def swim_speed(self):
|
||||
return self._swim_speed or int(self.speed / 2)
|
||||
|
||||
def add_trait(self, trait, level=1):
|
||||
if trait not in self.traits:
|
||||
self._traits.append(AncestryTraitMap(ancestry_id=self.id, ancestry_trait_id=trait.id, level=level))
|
||||
if trait not in self._traits:
|
||||
self._traits.append(AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -65,7 +80,7 @@ class Ancestry(BaseObject, ModifierMixin):
|
|||
return self.name
|
||||
|
||||
|
||||
class AncestryTrait(BaseObject):
|
||||
class AncestryTrait(BaseObject, ModifierMixin):
|
||||
"""
|
||||
A trait granted to a character via its Ancestry.
|
||||
"""
|
||||
|
@ -129,24 +144,14 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
|
|||
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})
|
||||
|
||||
_vision = Column(Integer, info={"min": 0})
|
||||
|
||||
proficiencies = Column(String)
|
||||
|
||||
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
||||
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
||||
|
||||
_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)
|
||||
|
||||
|
@ -157,6 +162,8 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
|
|||
def modifiers(self):
|
||||
unified = {}
|
||||
unified.update(**self.ancestry.modifiers)
|
||||
for trait in self.traits:
|
||||
unified.update(**trait.modifiers)
|
||||
unified.update(**super().modifiers)
|
||||
return unified
|
||||
|
||||
|
@ -204,10 +211,30 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
|
|||
def speed(self):
|
||||
return self.apply_modifiers("speed", self.ancestry.speed)
|
||||
|
||||
@property
|
||||
def climb_speed(self):
|
||||
return self.apply_modifiers("climb_speed", self.ancestry.climb_speed)
|
||||
|
||||
@property
|
||||
def swim_speed(self):
|
||||
return self.apply_modifiers("swim_speed", self.ancestry.swim_speed)
|
||||
|
||||
@property
|
||||
def fly_speed(self):
|
||||
return self.apply_modifiers("fly_speed", self.ancestry._fly_speed)
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.apply_modifiers("size", self.ancestry.size)
|
||||
|
||||
@property
|
||||
def vision(self):
|
||||
return self.apply_modifiers("vision", self._vision)
|
||||
|
||||
@property
|
||||
def vision_in_darkness(self):
|
||||
return self.apply_modifiers("vision_in_darkness", self.vision if self.vision is not None else 0)
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return sum(mapping.level for mapping in self.class_map)
|
||||
|
@ -228,7 +255,7 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
|
|||
level_in_class = level_in_class[0]
|
||||
level_in_class.level = level
|
||||
else:
|
||||
self.class_list.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
|
||||
self.class_list.append(CharacterClassMap(character_id=self.id, character_class=newclass, level=level))
|
||||
for lvl in range(1, level + 1):
|
||||
if not newclass.attributes_by_level[lvl]:
|
||||
continue
|
||||
|
@ -252,15 +279,15 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
|
|||
if attribute.name in self.class_attributes:
|
||||
return True
|
||||
self.attribute_list.append(
|
||||
CharacterClassAttributeMap(
|
||||
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
|
||||
)
|
||||
CharacterClassAttributeMap(character_id=self.id, class_attribute=attribute, option=option)
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def apply_modifiers(self, target, initial):
|
||||
modifiers = list(reversed(self.modifiers.get(target, [])))
|
||||
if initial is None:
|
||||
return initial
|
||||
if isinstance(initial, int):
|
||||
absolute = [mod for mod in modifiers if mod.absolute_value is not None]
|
||||
if absolute:
|
||||
|
|
|
@ -15,11 +15,12 @@ class ModifierMap(BaseObject):
|
|||
__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")
|
||||
|
||||
primary_table_name = Column(String, nullable=False)
|
||||
primary_table_id = Column(Integer, nullable=False)
|
||||
|
||||
|
||||
class Modifier(BaseObject):
|
||||
"""
|
||||
|
@ -67,8 +68,14 @@ class ModifierMixin:
|
|||
def modifier_map(cls):
|
||||
return relationship(
|
||||
"ModifierMap",
|
||||
primaryjoin=f"ModifierMap.primary_table_id == foreign({cls.__name__}.id)",
|
||||
primaryjoin=(
|
||||
"and_("
|
||||
f"foreign(ModifierMap.primary_table_name)=='{cls.__tablename__}', "
|
||||
f"foreign(ModifierMap.primary_table_id)=={cls.__name__}.id"
|
||||
")"
|
||||
),
|
||||
cascade="all,delete,delete-orphan",
|
||||
overlaps="modifier_map,modifier_map",
|
||||
single_parent=True,
|
||||
uselist=True,
|
||||
lazy="immediate",
|
||||
|
@ -84,6 +91,7 @@ class ModifierMixin:
|
|||
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(
|
||||
|
|
|
@ -97,7 +97,7 @@ def test_ancestries(db):
|
|||
porc = schema.Ancestry(
|
||||
name="Pygmy Orc",
|
||||
size="Small",
|
||||
speed=25,
|
||||
walk_speed=25,
|
||||
)
|
||||
db.add_or_update(porc)
|
||||
assert porc.name == "Pygmy Orc"
|
||||
|
@ -141,7 +141,8 @@ def test_modifiers(db, classes_factory, ancestries_factory):
|
|||
|
||||
# no modifiers; speed is ancestry speed
|
||||
carl = schema.Character(name="Carl", ancestry=ancestries["elf"])
|
||||
db.add_or_update(carl)
|
||||
marx = schema.Character(name="Marx", ancestry=ancestries["human"])
|
||||
db.add_or_update([carl, marx])
|
||||
assert carl.speed == carl.ancestry.speed == 30
|
||||
|
||||
cold = schema.Modifier(target="speed", relative_value=-10, name="Cold")
|
||||
|
@ -154,6 +155,9 @@ def test_modifiers(db, classes_factory, ancestries_factory):
|
|||
assert carl.add_modifier(cold)
|
||||
assert carl.speed == 20
|
||||
|
||||
# make sure modifiers only apply to carl. Carl is having a bad day.
|
||||
assert marx.speed == 30
|
||||
|
||||
# speed is doubled
|
||||
assert carl.remove_modifier(cold)
|
||||
assert carl.add_modifier(hasted)
|
||||
|
|
Loading…
Reference in New Issue
Block a user