wip
This commit is contained in:
parent
dbb9461b7a
commit
44cd8fe9c9
|
@ -16,7 +16,7 @@
|
||||||
<div>
|
<div>
|
||||||
{{ field('name') }}
|
{{ field('name') }}
|
||||||
{{ field('ancestry_id') }}
|
{{ field('ancestry_id') }}
|
||||||
{% for obj in c.form['classes'] %}
|
{% for obj in c.form['class_list'] %}
|
||||||
{{ obj(class='multiclass') }}
|
{{ obj(class='multiclass') }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<span class='label'>Add Class:</span> {{ c.form['newclass'](class='multiclass') }}
|
<span class='label'>Add Class:</span> {{ c.form['newclass'](class='multiclass') }}
|
||||||
|
@ -143,8 +143,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
<div class='label'>Attributes</div>
|
<div class='label'>Attributes</div>
|
||||||
{% if c.record.class_attributes %}
|
{% if c.record.attribute_list %}
|
||||||
{{ field('class_attributes') }}
|
{{ field('attribute_list') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class='card'>
|
<div class='card'>
|
||||||
|
@ -173,7 +173,7 @@
|
||||||
{{ field }}: {{ msg }}
|
{{ field }}: {{ msg }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</code>
|
</code>
|
||||||
{{ c.record.class_attributes }}
|
{{ c.record }}
|
||||||
</code>
|
</code>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -67,8 +67,8 @@ class AncestryTrait(BaseObject, IterableMixin):
|
||||||
class CharacterClassMap(BaseObject, IterableMixin):
|
class CharacterClassMap(BaseObject, IterableMixin):
|
||||||
__tablename__ = "class_map"
|
__tablename__ = "class_map"
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
character_id = Column(Integer, ForeignKey("character.id"))
|
character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
|
||||||
character_class_id = Column(Integer, ForeignKey("character_class.id"))
|
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
|
||||||
mapping = UniqueConstraint(character_id, character_class_id)
|
mapping = UniqueConstraint(character_id, character_class_id)
|
||||||
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
|
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ class CharacterClassMap(BaseObject, IterableMixin):
|
||||||
character = relationship("Character", uselist=False, viewonly=True)
|
character = relationship("Character", uselist=False, viewonly=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.character.name}, {self.character_class.name}, level {self.level}"
|
return "{self.character.name}, {self.character_class.name}, level {self.level}"
|
||||||
|
|
||||||
|
|
||||||
class CharacterClassAttributeMap(BaseObject, IterableMixin):
|
class CharacterClassAttributeMap(BaseObject, IterableMixin):
|
||||||
|
@ -118,10 +118,10 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
proficiencies = Column(String)
|
proficiencies = Column(String)
|
||||||
|
|
||||||
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
|
||||||
_classes = association_proxy("class_map", "id", creator=class_map_creator)
|
class_list = association_proxy("class_map", "id", creator=class_map_creator)
|
||||||
|
|
||||||
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
|
||||||
_class_attributes = 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)
|
||||||
|
@ -153,8 +153,13 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
if level_in_class:
|
if level_in_class:
|
||||||
level_in_class = level_in_class[0]
|
level_in_class = level_in_class[0]
|
||||||
level_in_class.level = level
|
level_in_class.level = level
|
||||||
return
|
else:
|
||||||
self._classes.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
|
self.class_list.append(CharacterClassMap(character_id=self.id, character_class_id=newclass.id, level=level))
|
||||||
|
for lvl in range(1, level + 1):
|
||||||
|
if not newclass.attributes_by_level[lvl]:
|
||||||
|
continue
|
||||||
|
for attr_name, attr in newclass.attributes_by_level[lvl].items():
|
||||||
|
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.id != target.id]
|
||||||
|
@ -167,8 +172,9 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
|
|
||||||
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():
|
||||||
|
# this test is failing?
|
||||||
if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}):
|
if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}):
|
||||||
self._class_attributes.append(
|
self.attribute_list.append(
|
||||||
CharacterClassAttributeMap(
|
CharacterClassAttributeMap(
|
||||||
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
|
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
|
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
@ -47,7 +49,7 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attributes_by_level(self):
|
def attributes_by_level(self):
|
||||||
by_level = {}
|
by_level = defaultdict(list)
|
||||||
for mapping in self.attributes:
|
for mapping in self.attributes:
|
||||||
by_level[mapping.level] = {mapping.attribute.name: mapping.attribute}
|
by_level[mapping.level] = {mapping.attribute.name: mapping.attribute}
|
||||||
return by_level
|
return by_level
|
||||||
|
|
|
@ -45,7 +45,8 @@ class ClassAttributesFormField(FormField):
|
||||||
def process(self, *args, **kwargs):
|
def process(self, *args, **kwargs):
|
||||||
super().process(*args, **kwargs)
|
super().process(*args, **kwargs)
|
||||||
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data["id"])
|
self.character_class_map = db.query(CharacterClassAttributeMap).get(self.data["id"])
|
||||||
self.label.text = self.character_class_map.character_class[0].name
|
if self.character_class_map:
|
||||||
|
self.label.text = self.character_class_map.character_class.name
|
||||||
|
|
||||||
|
|
||||||
class ClassAttributesForm(ModelForm):
|
class ClassAttributesForm(ModelForm):
|
||||||
|
@ -56,6 +57,7 @@ class ClassAttributesForm(ModelForm):
|
||||||
|
|
||||||
def __init__(self, formdata=None, obj=None, prefix=None):
|
def __init__(self, formdata=None, obj=None, prefix=None):
|
||||||
if obj:
|
if obj:
|
||||||
|
logging.debug(f"Loading existing attribute {self = } {formdata = } {obj = }")
|
||||||
obj = db.query(CharacterClassAttributeMap).get(obj)
|
obj = db.query(CharacterClassAttributeMap).get(obj)
|
||||||
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
||||||
|
|
||||||
|
@ -77,6 +79,7 @@ class MulticlassForm(ModelForm):
|
||||||
to an instance. This will ensure that the rendered field is populated with the current
|
to an instance. This will ensure that the rendered field is populated with the current
|
||||||
value of the class_map.
|
value of the class_map.
|
||||||
"""
|
"""
|
||||||
|
logging.debug(f"Loading existing class {self = } {formdata = } {obj = }")
|
||||||
if obj:
|
if obj:
|
||||||
obj = db.query(CharacterClassMap).get(obj)
|
obj = db.query(CharacterClassMap).get(obj)
|
||||||
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
||||||
|
@ -90,10 +93,10 @@ class CharacterForm(ModelForm):
|
||||||
save = SubmitField()
|
save = SubmitField()
|
||||||
delete = SubmitField()
|
delete = SubmitField()
|
||||||
ancestry_id = DeferredSelectField("Ancestry", model=Ancestry, default=1, validate_choice=True, widget=Select())
|
ancestry_id = DeferredSelectField("Ancestry", model=Ancestry, default=1, validate_choice=True, widget=Select())
|
||||||
classes = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
|
class_list = FieldList(FormField(MulticlassForm, label=None, widget=ListWidget()), min_entries=0)
|
||||||
newclass = FormField(MulticlassForm, widget=ListWidget())
|
newclass = FormField(MulticlassForm, widget=ListWidget())
|
||||||
|
|
||||||
class_attributes = FieldList(
|
attribute_list = FieldList(
|
||||||
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
|
ClassAttributesFormField(ClassAttributesForm, widget=ClassAttributeWidget()), min_entries=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,12 +118,12 @@ class CharacterSheet(BaseController):
|
||||||
Validate multiclass fields in form data.
|
Validate multiclass fields in form data.
|
||||||
"""
|
"""
|
||||||
ret = super().validate()
|
ret = super().validate()
|
||||||
if not self.form.data["classes"]:
|
if not self.form.data["class_list"]:
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
err = ""
|
err = ""
|
||||||
total_level = 0
|
total_level = 0
|
||||||
for field in self.form.data["classes"]:
|
for field in self.form.data["class_list"]:
|
||||||
level = field.get("level")
|
level = field.get("level")
|
||||||
total_level += level
|
total_level += level
|
||||||
if level not in VALID_LEVELS:
|
if level not in VALID_LEVELS:
|
||||||
|
@ -134,40 +137,16 @@ class CharacterSheet(BaseController):
|
||||||
return ret and True
|
return ret and True
|
||||||
|
|
||||||
def add_class_attributes(self):
|
def add_class_attributes(self):
|
||||||
# prefetch the records for each of the character's classes
|
|
||||||
classes_by_id = {
|
|
||||||
c.id: c
|
|
||||||
for c in db.query(CharacterClass)
|
|
||||||
.filter(CharacterClass.id.in_(c.character_class_id for c in self.record.class_map))
|
|
||||||
.all()
|
|
||||||
}
|
|
||||||
|
|
||||||
assigned = [int(m.class_attribute_id) for m in self.record.character_class_attribute_map]
|
|
||||||
logging.debug(f"{assigned = }")
|
|
||||||
|
|
||||||
# step through the list of class mappings for this character
|
# step through the list of class mappings for this character
|
||||||
for class_map in self.record.class_map:
|
for class_name, class_def in self.record.classes.items():
|
||||||
thisclass = classes_by_id[class_map.character_class_id]
|
logging.error(f"{class_name = }, {class_def = }")
|
||||||
|
for level in range(1, self.record.levels[class_name] + 1):
|
||||||
# assign each class attribute available at the character's current
|
for attr in class_def.attributes_by_level.get(level, None):
|
||||||
# level to the list of the character's class attributes
|
self.record.add_class_attribute(attr, attr.options[0])
|
||||||
for attr_map in [a for a in thisclass.attributes if a.level <= class_map.level]:
|
|
||||||
# when creating a record, assign the first of the available
|
|
||||||
# options to the character's class attribute.
|
|
||||||
default_option = (
|
|
||||||
db.query(ClassAttributeOption).filter_by(attribute_id=attr_map.class_attribute_id).first()
|
|
||||||
)
|
|
||||||
|
|
||||||
if attr_map.class_attribute_id not in assigned:
|
|
||||||
self.record.class_attributes.append(
|
|
||||||
{
|
|
||||||
"class_attribute_id": attr_map.class_attribute_id,
|
|
||||||
"option_id": default_option.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_callback(self):
|
def save_callback(self):
|
||||||
self.add_class_attributes()
|
# self.add_class_attributes()
|
||||||
|
pass
|
||||||
|
|
||||||
def populate(self):
|
def populate(self):
|
||||||
"""
|
"""
|
||||||
|
@ -176,16 +155,16 @@ class CharacterSheet(BaseController):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# multiclass form
|
# multiclass form
|
||||||
classes_formdata = self.form.data["classes"]
|
classes_formdata = self.form.data["class_list"]
|
||||||
classes_formdata.append(self.form.data["newclass"])
|
classes_formdata.append(self.form.data["newclass"])
|
||||||
del self.form.classes
|
del self.form.class_list
|
||||||
del self.form.newclass
|
del self.form.newclass
|
||||||
|
|
||||||
# class attributes
|
# class attributes
|
||||||
attrs_formdata = self.form.data["class_attributes"]
|
attrs_formdata = self.form.data["attribute_list"]
|
||||||
del self.form.class_attributes
|
del self.form.attribute_list
|
||||||
|
|
||||||
super().populate()
|
super().populate()
|
||||||
|
|
||||||
self.record.classes = self.populate_association("character_class_id", classes_formdata)
|
self.record.class_list = self.populate_association("character_class_id", classes_formdata)
|
||||||
self.record.class_attributes = self.populate_association("class_attribute_id", attrs_formdata)
|
self.record.attribute_list = self.populate_association("class_attribute_id", attrs_formdata)
|
||||||
|
|
|
@ -30,12 +30,16 @@ def db(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def classes(db):
|
def classes_factory(db):
|
||||||
load_fixture(db, "classes")
|
load_fixture(db, "classes")
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
|
def factory():
|
||||||
|
return dict((rec.name, rec) for rec in db.session.query(schema.CharacterClass).all())
|
||||||
|
return factory
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ancestries(db):
|
def ancestries_factory(db):
|
||||||
load_fixture(db, "ancestry")
|
load_fixture(db, "ancestry")
|
||||||
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
|
def factory():
|
||||||
|
return dict((rec.name, rec) for rec in db.session.query(schema.Ancestry).all())
|
||||||
|
return factory
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from ttfrog.db import schema
|
from ttfrog.db import schema
|
||||||
|
|
||||||
|
|
||||||
def test_create_character(db, classes, ancestries):
|
def test_create_character(db, classes_factory, ancestries_factory):
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
|
# load the fixtures so they are bound to the current session
|
||||||
|
classes = classes_factory()
|
||||||
|
ancestries = ancestries_factory()
|
||||||
darkvision = db.session.query(schema.AncestryTrait).filter_by(name="Darkvision")[0]
|
darkvision = db.session.query(schema.AncestryTrait).filter_by(name="Darkvision")[0]
|
||||||
|
|
||||||
# create a human character (the default)
|
# create a human character (the default)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user