fix schemas
This commit is contained in:
parent
3292b11d89
commit
b574dacfa1
|
@ -34,5 +34,3 @@ def bootstrap():
|
||||||
|
|
||||||
# persist all the records we've created
|
# persist all the records we've created
|
||||||
db.add_or_update([sabetha, bob])
|
db.add_or_update([sabetha, bob])
|
||||||
|
|
||||||
print(f"{sabetha.intelligence.bonus = }, {sabetha.size = }")
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ class CharacterClassMap(BaseObject):
|
||||||
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
|
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{self.character.name}, {self.character_class.name}, level {self.level}"
|
return f"{self.character.name}, {self.character_class.name}, level {self.level}"
|
||||||
|
|
||||||
|
|
||||||
class CharacterClassAttributeMap(BaseObject):
|
class CharacterClassAttributeMap(BaseObject):
|
||||||
|
@ -147,34 +147,37 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierM
|
||||||
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
name: Mapped[str] = mapped_column(default="New Character", nullable=False)
|
name: Mapped[str] = mapped_column(default="New Character", nullable=False)
|
||||||
_armor_class: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 1, "max": 99, "modify": True})
|
hit_points: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 0, "max": 999})
|
||||||
_hit_points: Mapped[int] = mapped_column(default=1, nullable=False, info={"min": 0, "max": 999, "modify": True})
|
|
||||||
_max_hit_points: Mapped[int] = mapped_column(
|
|
||||||
default=10, nullable=False, info={"min": 0, "max": 999, "modify": True}
|
|
||||||
)
|
|
||||||
temp_hit_points: Mapped[int] = mapped_column(default=0, nullable=False, info={"min": 0, "max": 999})
|
temp_hit_points: Mapped[int] = mapped_column(default=0, nullable=False, info={"min": 0, "max": 999})
|
||||||
|
|
||||||
|
_max_hit_points: Mapped[int] = mapped_column(
|
||||||
|
default=10, nullable=False, info={"min": 0, "max": 999, "modifiable": True}
|
||||||
|
)
|
||||||
|
_armor_class: Mapped[int] = mapped_column(
|
||||||
|
default=10, nullable=False, info={"min": 1, "max": 99, "modifiable": True}
|
||||||
|
)
|
||||||
_strength: Mapped[int] = mapped_column(
|
_strength: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
_dexterity: Mapped[int] = mapped_column(
|
_dexterity: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
_constitution: Mapped[int] = mapped_column(
|
_constitution: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
_intelligence: Mapped[int] = mapped_column(
|
_intelligence: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
_wisdom: Mapped[int] = mapped_column(
|
_wisdom: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
_charisma: Mapped[int] = mapped_column(
|
_charisma: Mapped[int] = mapped_column(
|
||||||
nullable=False, default=10, info={"min": 0, "max": 30, "modify": True, "modify_class": Stat}
|
nullable=False, default=10, info={"min": 0, "max": 30, "modifiable_class": Stat}
|
||||||
)
|
)
|
||||||
|
|
||||||
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0})
|
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0, "modifiable": True})
|
||||||
|
|
||||||
proficiencies: Mapped[str] = mapped_column(nullable=False, default="")
|
_proficiencies: Mapped[str] = mapped_column(nullable=False, default="")
|
||||||
|
|
||||||
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)
|
||||||
|
@ -204,19 +207,19 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierM
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed(self):
|
def speed(self):
|
||||||
return self.apply_modifiers("speed", self.ancestry.speed)
|
return self._apply_modifiers("speed", self.ancestry.speed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def climb_speed(self):
|
def climb_speed(self):
|
||||||
return self.apply_modifiers("climb_speed", self.ancestry.climb_speed)
|
return self._apply_modifiers("climb_speed", self.ancestry._climb_speed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swim_speed(self):
|
def swim_speed(self):
|
||||||
return self.apply_modifiers("swim_speed", self.ancestry.swim_speed)
|
return self._apply_modifiers("swim_speed", self.ancestry._swim_speed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fly_speed(self):
|
def fly_speed(self):
|
||||||
return self.apply_modifiers("fly_speed", self.ancestry.fly_speed)
|
return self._apply_modifiers("fly_speed", self.ancestry._fly_speed)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
|
@ -254,13 +257,15 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierM
|
||||||
self.add_class_attribute(attr, attr.options[0])
|
self.add_class_attribute(attr, attr.options[0])
|
||||||
|
|
||||||
def remove_class(self, target):
|
def remove_class(self, target):
|
||||||
self.class_map = [m for m in self.class_map if m.id != target.id]
|
self.class_map = [m for m in self.class_map if m.character_class != target]
|
||||||
for mapping in self.character_class_attribute_map:
|
for mapping in self.character_class_attribute_map:
|
||||||
if mapping.character_class.id == target.id:
|
if mapping.character_class.id == target.id:
|
||||||
self.remove_class_attribute(mapping.class_attribute)
|
self.remove_class_attribute(mapping.class_attribute)
|
||||||
|
|
||||||
def remove_class_attribute(self, attribute):
|
def remove_class_attribute(self, attribute):
|
||||||
self.character_class_attribute_map = [m for m in self.character_class_attribute_map if m.id != attribute.id]
|
self.character_class_attribute_map = [
|
||||||
|
m for m in self.character_class_attribute_map if m.class_attribute.id != attribute.id
|
||||||
|
]
|
||||||
|
|
||||||
def add_class_attribute(self, attribute, option):
|
def add_class_attribute(self, attribute, option):
|
||||||
for thisclass in self.classes.values():
|
for thisclass in self.classes.values():
|
||||||
|
@ -270,7 +275,12 @@ class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin, ModifierM
|
||||||
if attribute.name in self.class_attributes:
|
if attribute.name in self.class_attributes:
|
||||||
return True
|
return True
|
||||||
self.attribute_list.append(
|
self.attribute_list.append(
|
||||||
CharacterClassAttributeMap(character_id=self.id, class_attribute=attribute, option=option)
|
CharacterClassAttributeMap(
|
||||||
|
character_id=self.id,
|
||||||
|
class_attribute_id=attribute.id,
|
||||||
|
option_id=option.id,
|
||||||
|
class_attribute=attribute,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -27,6 +27,17 @@ class ClassAttribute(BaseObject):
|
||||||
name: Mapped[str] = mapped_column(nullable=False)
|
name: Mapped[str] = mapped_column(nullable=False)
|
||||||
options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate")
|
options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||||
|
|
||||||
|
def add_option(self, **kwargs):
|
||||||
|
option = ClassAttributeOption(attribute_id=self.id, **kwargs)
|
||||||
|
if not self.options or option not in self.options:
|
||||||
|
option.attribute_id = self.id
|
||||||
|
if not self.options:
|
||||||
|
self.options = [option]
|
||||||
|
else:
|
||||||
|
self.options.append(option)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.id}: {self.name}"
|
return f"{self.id}: {self.name}"
|
||||||
|
|
||||||
|
@ -35,7 +46,7 @@ class ClassAttributeOption(BaseObject):
|
||||||
__tablename__ = "class_attribute_option"
|
__tablename__ = "class_attribute_option"
|
||||||
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(nullable=False)
|
name: Mapped[str] = mapped_column(nullable=False)
|
||||||
attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
|
attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin):
|
class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin):
|
||||||
|
@ -47,6 +58,16 @@ class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin):
|
||||||
proficiencies: Mapped[str] = mapped_column(default="")
|
proficiencies: Mapped[str] = mapped_column(default="")
|
||||||
attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate")
|
attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate")
|
||||||
|
|
||||||
|
def add_attribute(self, attribute, level=1):
|
||||||
|
if not self.attributes or attribute not in self.attributes:
|
||||||
|
mapping = ClassAttributeMap(character_class_id=self.id, class_attribute_id=attribute.id, level=level)
|
||||||
|
if not self.attributes:
|
||||||
|
self.attributes = [mapping]
|
||||||
|
else:
|
||||||
|
self.attributes.append(mapping)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attributes_by_level(self):
|
def attributes_by_level(self):
|
||||||
by_level = defaultdict(list)
|
by_level = defaultdict(list)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Any, Union
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, UniqueConstraint
|
from sqlalchemy import ForeignKey, UniqueConstraint
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
@ -94,6 +95,9 @@ class ModifierMixin:
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def modifier_map(cls):
|
def modifier_map(cls):
|
||||||
|
"""
|
||||||
|
Create the join between the current model and the ModifierMap table.
|
||||||
|
"""
|
||||||
return relationship(
|
return relationship(
|
||||||
"ModifierMap",
|
"ModifierMap",
|
||||||
primaryjoin=(
|
primaryjoin=(
|
||||||
|
@ -111,12 +115,20 @@ class ModifierMixin:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modifiers(self):
|
def modifiers(self):
|
||||||
|
"""
|
||||||
|
Return all modifiers for the current instance as a dict keyed on target attribute name.
|
||||||
|
"""
|
||||||
all_modifiers = defaultdict(list)
|
all_modifiers = defaultdict(list)
|
||||||
for mapping in self.modifier_map:
|
for mapping in self.modifier_map:
|
||||||
all_modifiers[mapping.modifier.target].append(mapping.modifier)
|
all_modifiers[mapping.modifier.target].append(mapping.modifier)
|
||||||
return all_modifiers
|
return all_modifiers
|
||||||
|
|
||||||
def add_modifier(self, modifier):
|
def add_modifier(self, modifier: Modifier) -> bool:
|
||||||
|
"""
|
||||||
|
Associate a modifier to the current instance if it isn't already.
|
||||||
|
|
||||||
|
Returns True if the modifier was added; False if was already present.
|
||||||
|
"""
|
||||||
if modifier.absolute_value is not None and modifier.relative_value is not None and modifier.multiple_value:
|
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}.")
|
raise AttributeError(f"You must provide only one of absolute, relative, and multiple values {modifier}.")
|
||||||
|
|
||||||
|
@ -131,15 +143,74 @@ class ModifierMixin:
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove_modifier(self, modifier):
|
def remove_modifier(self, modifier: Modifier) -> bool:
|
||||||
|
"""
|
||||||
|
Remove a modifier from the map.
|
||||||
|
|
||||||
|
Returns True if it was removed and False if it wasn't present.
|
||||||
|
"""
|
||||||
if modifier not in self.modifiers[modifier.target]:
|
if modifier not in self.modifiers[modifier.target]:
|
||||||
return False
|
return False
|
||||||
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
|
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _apply_modifiers(self, target, initial, modify_class=None):
|
def _modifiable_column(self, attr_name: str) -> Union[mapped_column, None]:
|
||||||
if not modify_class:
|
"""
|
||||||
modify_class = globals()["ModifiableInt"] if isinstance(initial, int) else globals()["ModifiableStr"]
|
Given an atttribute name, look for a column attribute with the same
|
||||||
|
name but with an underscore prefix. If that column exists, and it
|
||||||
|
has one or more of the expected "modifiable" keys in its info, the
|
||||||
|
column is modifiable.
|
||||||
|
|
||||||
|
Returns the matching column if it was found, or None.
|
||||||
|
"""
|
||||||
|
col = getattr(self.__table__.columns, f"_{attr_name}", None)
|
||||||
|
if col is None:
|
||||||
|
return None
|
||||||
|
for key in col.info.keys():
|
||||||
|
if key.startswith("modifiable"):
|
||||||
|
return col
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_modifiable_base(self, attr_name: str) -> object:
|
||||||
|
"""
|
||||||
|
Resolve a dottted string "foo.bar.baz" as its corresponding nested attribute.
|
||||||
|
|
||||||
|
This is useful for cases where a column definition includes a modifiable_base
|
||||||
|
that is some other attribute. For example:
|
||||||
|
|
||||||
|
foo[int] = mapped_column(default=0, info={"modifiable_base": "ancestry.bar")
|
||||||
|
|
||||||
|
This will create an initial value for self.foo equal to self.ancesetry.bar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_attr(obj, parts):
|
||||||
|
if parts:
|
||||||
|
name, *parts = parts
|
||||||
|
return get_attr(getattr(obj, name), parts)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
return get_attr(self, attr_name.split("."))
|
||||||
|
|
||||||
|
def _apply_modifiers(self, target: str, initial: Any, modifiable_class: type = None) -> Modifiable:
|
||||||
|
"""
|
||||||
|
Apply all the modifiers for a given target and return the modified value.
|
||||||
|
|
||||||
|
This is mostly called from __getattr__() below to handle cases where a
|
||||||
|
column is named self._foo but the modified value is accessible as
|
||||||
|
self.foo. It can also be invoked directly, as, say from a property:
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self):
|
||||||
|
return self._apply_modifiers("speed", self.ancestry.speed)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target - The name of the attribute to modify
|
||||||
|
initial - The initial value for the target
|
||||||
|
modifiable_class - The object type to return; inferred from the
|
||||||
|
target attribute's type if not specified.
|
||||||
|
"""
|
||||||
|
if not modifiable_class:
|
||||||
|
modifiable_class = globals()["ModifiableInt"] if isinstance(initial, int) else globals()["ModifiableStr"]
|
||||||
|
|
||||||
# get the modifiers in order from most to least recent
|
# get the modifiers in order from most to least recent
|
||||||
modifiers = list(reversed(self.modifiers.get(target, [])))
|
modifiers = list(reversed(self.modifiers.get(target, [])))
|
||||||
|
@ -161,18 +232,28 @@ class ModifierMixin:
|
||||||
else:
|
else:
|
||||||
modified = initial
|
modified = initial
|
||||||
|
|
||||||
return modify_class(base=initial, modified=modified)
|
return modifiable_class(base=initial, modified=modified) if modified is not None else None
|
||||||
|
|
||||||
def __setattr__(self, attr_name, value):
|
def __setattr__(self, attr_name, value):
|
||||||
col = getattr(self.__table__.columns, f"_{attr_name}", None)
|
"""
|
||||||
if col is not None and col.info.get("modify", False):
|
Prevent callers from setting the value of a Modifiable directly.
|
||||||
raise AttributeError(f"You cannot set .{attr_name}. Did you mean ._{attr_name}?")
|
"""
|
||||||
|
col = self._modifiable_column(attr_name)
|
||||||
|
if col is not None:
|
||||||
|
raise AttributeError(f"You cannot modify .{attr_name}. Did you mean ._{attr_name}?")
|
||||||
return super().__setattr__(attr_name, value)
|
return super().__setattr__(attr_name, value)
|
||||||
|
|
||||||
def __getattr__(self, attr_name):
|
def __getattr__(self, attr_name):
|
||||||
col = getattr(self.__table__.columns, f"_{attr_name}", None)
|
"""
|
||||||
if col is not None and col.info.get("modify", False):
|
If the instance has an attribute equal to attr_name but prefixed with an
|
||||||
|
underscore, check to see if that attribute is a column, and modifiable.
|
||||||
|
If it is, return a Modifiable instance corresponding to that column's value.
|
||||||
|
"""
|
||||||
|
col = self._modifiable_column(attr_name)
|
||||||
|
if col is not None:
|
||||||
return self._apply_modifiers(
|
return self._apply_modifiers(
|
||||||
attr_name, getattr(self, col.name), modify_class=col.info.get("modify_class", None)
|
attr_name,
|
||||||
|
self._get_modifiable_base(col.info.get("modifiable_base", col.name)),
|
||||||
|
modifiable_class=col.info.get("modifiable_class", None),
|
||||||
)
|
)
|
||||||
return super().__getattr__(attr_name)
|
return super().__getattr__(attr_name)
|
||||||
|
|
|
@ -31,20 +31,43 @@ def db(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def classes_factory(db):
|
def bootstrap(db):
|
||||||
load_fixture(db, "classes")
|
with db.transaction():
|
||||||
|
# ancestries
|
||||||
|
human = schema.Ancestry("human")
|
||||||
|
|
||||||
def factory():
|
tiefling = schema.Ancestry("tiefling")
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
|
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="intelligence", relative_value=1))
|
||||||
|
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="charisma", relative_value=2))
|
||||||
|
|
||||||
return factory
|
# ancestry traits
|
||||||
|
darkvision = schema.AncestryTrait("Darkvision")
|
||||||
|
darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
|
||||||
|
tiefling.add_trait(darkvision)
|
||||||
|
|
||||||
|
dragonborn = schema.Ancestry("dragonborn")
|
||||||
|
dragonborn.add_trait(darkvision)
|
||||||
|
|
||||||
@pytest.fixture
|
db.add_or_update([human, dragonborn, tiefling])
|
||||||
def ancestries_factory(db):
|
|
||||||
load_fixture(db, "ancestry")
|
|
||||||
|
|
||||||
def factory():
|
# classes
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
|
fighting_style = schema.ClassAttribute("Fighting Style")
|
||||||
|
fighting_style.add_option(name="Archery")
|
||||||
|
fighting_style.add_option(name="Defense")
|
||||||
|
db.add_or_update(fighting_style)
|
||||||
|
|
||||||
return factory
|
fighter = schema.CharacterClass("fighter", hit_dice="1d10", hit_dice_stat="CON")
|
||||||
|
fighter.add_attribute(fighting_style, level=2)
|
||||||
|
|
||||||
|
rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX")
|
||||||
|
db.add_or_update([rogue, fighter])
|
||||||
|
|
||||||
|
# characters
|
||||||
|
foo = schema.Character("Foo", ancestry=tiefling, _intelligence=14)
|
||||||
|
foo.add_class(fighter, level=2)
|
||||||
|
foo.add_class(rogue, level=3)
|
||||||
|
|
||||||
|
bar = schema.Character("Bar", ancestry=human)
|
||||||
|
|
||||||
|
# persist all the records we've created
|
||||||
|
db.add_or_update([foo, bar])
|
||||||
|
|
|
@ -3,62 +3,64 @@ import json
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
|
|
||||||
|
|
||||||
def test_manage_character(db, classes_factory, ancestries_factory):
|
def test_manage_character(db, bootstrap):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
# load the fixtures so they are bound to the current session
|
darkvision = db.AncestryTrait.filter_by(name="Darkvision").one()
|
||||||
classes = classes_factory()
|
human = db.Ancestry.filter_by(name="human").one()
|
||||||
ancestries = ancestries_factory()
|
|
||||||
darkvision = db.AncestryTrait.filter_by(name="Darkvision")[0]
|
|
||||||
|
|
||||||
# create a human character (the default)
|
# create a human character (the default)
|
||||||
char = schema.Character(name="Test Character")
|
char = schema.Character(name="Test Character", ancestry=human)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.id == 1
|
assert char.id == 3
|
||||||
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.armor_class == 10
|
||||||
assert char.HP == 10
|
assert char.hit_points == 10
|
||||||
assert char.STR == 10
|
assert char.strength == 10
|
||||||
assert char.DEX == 10
|
assert char.dexterity == 10
|
||||||
assert char.CON == 10
|
assert char.constitution == 10
|
||||||
assert char.INT == 10
|
assert char.intelligence == 10
|
||||||
assert char.WIS == 10
|
assert char.wisdom == 10
|
||||||
assert char.CHA == 10
|
assert char.charisma == 10
|
||||||
assert darkvision not in char.traits
|
assert darkvision not in char.traits
|
||||||
|
|
||||||
# switch ancestry to tiefling
|
# switch ancestry to tiefling
|
||||||
char.ancestry = ancestries["tiefling"]
|
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||||
|
char.ancestry = tiefling
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
char = db.session.get(schema.Character, 1)
|
char = db.session.get(schema.Character, char.id)
|
||||||
|
assert char.ancestry_id == tiefling.id
|
||||||
assert char.ancestry.name == "tiefling"
|
assert char.ancestry.name == "tiefling"
|
||||||
assert darkvision in char.traits
|
assert darkvision in char.traits
|
||||||
|
|
||||||
# switch ancestry to dragonborn and assert darkvision persists
|
# switch ancestry to dragonborn and assert darkvision persists
|
||||||
char.ancestry = ancestries["dragonborn"]
|
char.ancestry = db.Ancestry.filter_by(name="dragonborn").one()
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert darkvision in char.traits
|
assert darkvision in char.traits
|
||||||
|
|
||||||
# switch ancestry to human and assert darkvision is removed
|
# switch ancestry to human and assert darkvision is removed
|
||||||
char.ancestry = ancestries["human"]
|
char.ancestry = human
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert darkvision not in char.traits
|
assert darkvision not in char.traits
|
||||||
|
|
||||||
|
fighter = db.CharacterClass.filter_by(name="fighter").one()
|
||||||
|
rogue = db.CharacterClass.filter_by(name="rogue").one()
|
||||||
|
|
||||||
# assign a class and level
|
# assign a class and level
|
||||||
char.add_class(classes["fighter"], level=1)
|
char.add_class(fighter, level=1)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.levels == {"fighter": 1}
|
assert char.levels == {"fighter": 1}
|
||||||
assert char.level == 1
|
assert char.level == 1
|
||||||
assert char.class_attributes == {}
|
assert char.class_attributes == {}
|
||||||
|
|
||||||
# 'fighting style' is available, but not at this level
|
# 'fighting style' is available, but not at this level
|
||||||
fighter = classes["fighter"]
|
|
||||||
fighting_style = fighter.attributes_by_level[2]["Fighting Style"]
|
fighting_style = fighter.attributes_by_level[2]["Fighting Style"]
|
||||||
assert char.add_class_attribute(fighting_style, fighting_style.options[0]) is False
|
assert char.add_class_attribute(fighting_style, fighting_style.options[0]) is False
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.class_attributes == {}
|
assert char.class_attributes == {}
|
||||||
|
|
||||||
# level up
|
# level up
|
||||||
char.add_class(classes["fighter"], level=2)
|
char.add_class(fighter, level=2)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.levels == {"fighter": 2}
|
assert char.levels == {"fighter": 2}
|
||||||
assert char.level == 2
|
assert char.level == 2
|
||||||
|
@ -69,26 +71,27 @@ def test_manage_character(db, classes_factory, ancestries_factory):
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
|
|
||||||
# classes
|
# classes
|
||||||
char.add_class(classes["rogue"], level=1)
|
char.add_class(rogue, level=1)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.level == 3
|
assert char.level == 3
|
||||||
assert char.levels == {"fighter": 2, "rogue": 1}
|
assert char.levels == {"fighter": 2, "rogue": 1}
|
||||||
|
|
||||||
# remove a class
|
# remove a class
|
||||||
char.remove_class(classes["rogue"])
|
char.remove_class(rogue)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.levels == {"fighter": 2}
|
assert char.levels == {"fighter": 2}
|
||||||
assert char.level == 2
|
assert char.level == 2
|
||||||
|
|
||||||
# remove remaining class by setting level to zero
|
# remove remaining class by setting level to zero
|
||||||
char.add_class(classes["fighter"], level=0)
|
char.add_class(fighter, level=0)
|
||||||
db.add_or_update(char)
|
db.add_or_update(char)
|
||||||
assert char.levels == {}
|
assert char.levels == {}
|
||||||
|
assert char.class_attributes == {}
|
||||||
|
|
||||||
# ensure we're not persisting any orphan records in the map tables
|
# ensure we're not persisting any orphan records in the map tables
|
||||||
dump = json.loads(db.dump())
|
dump = json.loads(db.dump())
|
||||||
assert dump["class_map"] == []
|
assert not [m for m in dump["character_class_attribute_map"] if m["character_id"] == char.id]
|
||||||
assert dump["character_class_attribute_map"] == []
|
assert not [m for m in dump["class_map"] if m["character_id"] == char.id]
|
||||||
|
|
||||||
|
|
||||||
def test_ancestries(db):
|
def test_ancestries(db):
|
||||||
|
@ -129,19 +132,19 @@ def test_ancestries(db):
|
||||||
assert endurance in grognak.traits
|
assert endurance in grognak.traits
|
||||||
|
|
||||||
# verify the strength bonus is applied
|
# verify the strength bonus is applied
|
||||||
assert grognak.strength == 10
|
assert grognak.strength.base == 10
|
||||||
assert str_plus_one in grognak.modifiers["strength"]
|
assert str_plus_one in grognak.modifiers["strength"]
|
||||||
assert grognak.STR == 11
|
assert grognak.strength == 11
|
||||||
|
|
||||||
|
|
||||||
def test_modifiers(db, classes_factory, ancestries_factory):
|
def test_modifiers(db, bootstrap):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
classes_factory()
|
human = db.Ancestry.filter_by(name="human").one()
|
||||||
ancestries = ancestries_factory()
|
tiefling = db.Ancestry.filter_by(name="tiefling").one()
|
||||||
|
|
||||||
# no modifiers; speed is ancestry speed
|
# no modifiers; speed is ancestry speed
|
||||||
carl = schema.Character(name="Carl", ancestry=ancestries["elf"])
|
carl = schema.Character(name="Carl", ancestry=tiefling)
|
||||||
marx = schema.Character(name="Marx", ancestry=ancestries["human"])
|
marx = schema.Character(name="Marx", ancestry=human)
|
||||||
db.add_or_update([carl, marx])
|
db.add_or_update([carl, marx])
|
||||||
assert carl.speed == carl.ancestry.speed == 30
|
assert carl.speed == carl.ancestry.speed == 30
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user