2024-02-23 10:45:38 -08:00
|
|
|
import logging
|
|
|
|
|
2024-02-08 01:14:35 -08:00
|
|
|
from ttfrog.webserver.controllers.base import BaseController
|
2024-02-23 10:45:38 -08:00
|
|
|
from ttfrog.webserver.forms import DeferredSelectField
|
|
|
|
from ttfrog.webserver.forms import NullableDeferredSelectField
|
2024-02-26 01:12:45 -08:00
|
|
|
from ttfrog.db.schema import (
|
|
|
|
Character,
|
|
|
|
Ancestry,
|
|
|
|
CharacterClass,
|
|
|
|
CharacterClassMap,
|
|
|
|
ClassAttributeOption,
|
|
|
|
CharacterClassAttributeMap
|
|
|
|
)
|
|
|
|
|
2024-02-18 19:30:41 -08:00
|
|
|
from ttfrog.db.base import STATS
|
2024-02-23 10:45:38 -08:00
|
|
|
from ttfrog.db.manager import db
|
2024-02-08 23:04:28 -08:00
|
|
|
|
2024-02-04 15:38:54 -08:00
|
|
|
from wtforms_alchemy import ModelForm
|
2024-02-23 10:45:38 -08:00
|
|
|
from wtforms.fields import SubmitField, SelectField, SelectMultipleField, FieldList, FormField, HiddenField
|
|
|
|
from wtforms.widgets import Select, ListWidget
|
|
|
|
from wtforms import ValidationError
|
2024-02-26 01:12:45 -08:00
|
|
|
from wtforms.validators import Optional
|
2024-02-23 10:45:38 -08:00
|
|
|
|
|
|
|
VALID_LEVELS = range(1, 21)
|
|
|
|
|
|
|
|
|
2024-02-26 01:12:45 -08:00
|
|
|
class ClassAttributesForm(ModelForm):
|
|
|
|
id = HiddenField()
|
|
|
|
class_attribute_id = HiddenField()
|
|
|
|
|
|
|
|
option_id = SelectField(
|
|
|
|
widget=Select(),
|
|
|
|
choices=[],
|
|
|
|
validators=[Optional()],
|
|
|
|
coerce=int
|
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(self, formdata=None, obj=None, prefix=None):
|
|
|
|
if obj:
|
|
|
|
obj = db.query(CharacterClassAttributeMap).get(obj)
|
|
|
|
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
|
|
|
|
|
|
|
if obj:
|
|
|
|
options = db.query(ClassAttributeOption).filter_by(attribute_id=obj.class_attribute.id)
|
|
|
|
self.option_id.label = obj.class_attribute.name
|
|
|
|
self.option_id.choices = [(rec.id, rec.name) for rec in options.all()]
|
|
|
|
|
|
|
|
|
2024-02-23 10:45:38 -08:00
|
|
|
class MulticlassForm(ModelForm):
|
2024-02-26 01:12:45 -08:00
|
|
|
|
2024-02-23 10:45:38 -08:00
|
|
|
id = HiddenField()
|
|
|
|
character_class_id = NullableDeferredSelectField(
|
|
|
|
model=CharacterClass,
|
|
|
|
validate_choice=True,
|
|
|
|
widget=Select(),
|
|
|
|
coerce=int
|
|
|
|
)
|
|
|
|
level = SelectField(choices=VALID_LEVELS, default=1, coerce=int, validate_choice=True, widget=Select())
|
|
|
|
|
|
|
|
def __init__(self, formdata=None, obj=None, prefix=None):
|
|
|
|
"""
|
|
|
|
Populate the form field with a CharacterClassMap object by converting the object ID
|
|
|
|
to an instance. This will ensure that the rendered field is populated with the current
|
|
|
|
value of the class_map.
|
|
|
|
"""
|
2024-02-26 01:12:45 -08:00
|
|
|
if obj:
|
|
|
|
obj = db.query(CharacterClassMap).get(obj)
|
2024-02-23 10:45:38 -08:00
|
|
|
super().__init__(formdata=formdata, obj=obj, prefix=prefix)
|
2024-02-04 11:40:30 -08:00
|
|
|
|
|
|
|
|
|
|
|
class CharacterForm(ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Character
|
|
|
|
exclude = ['slug']
|
|
|
|
|
2024-02-04 15:22:54 -08:00
|
|
|
save = SubmitField()
|
|
|
|
delete = SubmitField()
|
2024-02-23 10:45:38 -08:00
|
|
|
ancestry_id = DeferredSelectField('Ancestry', model=Ancestry, default=1, validate_choice=True, widget=Select())
|
|
|
|
classes = FieldList(FormField(MulticlassForm, widget=ListWidget()), min_entries=0)
|
|
|
|
newclass = FormField(MulticlassForm, widget=ListWidget())
|
2024-02-26 01:12:45 -08:00
|
|
|
|
|
|
|
class_attributes = FieldList(FormField(ClassAttributesForm, widget=ListWidget()), min_entries=1)
|
|
|
|
|
2024-02-08 01:14:35 -08:00
|
|
|
saving_throws = SelectMultipleField('Saving Throws', validate_choice=True, choices=STATS)
|
2024-01-28 14:31:50 -08:00
|
|
|
|
|
|
|
|
2024-01-31 22:39:54 -08:00
|
|
|
class CharacterSheet(BaseController):
|
2024-02-04 11:40:30 -08:00
|
|
|
model = CharacterForm.Meta.model
|
|
|
|
model_form = CharacterForm
|
2024-02-08 01:14:35 -08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def resources(self):
|
|
|
|
return super().resources + [
|
|
|
|
{'type': 'script', 'uri': 'js/character_sheet.js'},
|
|
|
|
]
|
2024-02-23 10:45:38 -08:00
|
|
|
|
2024-02-26 01:12:45 -08:00
|
|
|
def validate_callback(self):
|
2024-02-23 10:45:38 -08:00
|
|
|
"""
|
|
|
|
Validate multiclass fields in form data.
|
|
|
|
"""
|
2024-02-26 01:12:45 -08:00
|
|
|
ret = super().validate()
|
|
|
|
if not self.form.data['classes']:
|
|
|
|
return ret
|
|
|
|
|
2024-02-23 10:45:38 -08:00
|
|
|
err = ""
|
|
|
|
total_level = 0
|
|
|
|
for field in self.form.data['classes']:
|
2024-02-26 01:12:45 -08:00
|
|
|
level = field.get('level')
|
2024-02-23 10:45:38 -08:00
|
|
|
total_level += level
|
|
|
|
if level not in VALID_LEVELS:
|
|
|
|
err = f"Multiclass form field {field = } level is outside possible range."
|
|
|
|
break
|
|
|
|
if total_level not in VALID_LEVELS:
|
|
|
|
err = f"Total level for all multiclasses ({total_level}) is outside possible range."
|
|
|
|
if err:
|
|
|
|
logging.error(err)
|
|
|
|
raise ValidationError(err)
|
2024-02-26 01:12:45 -08:00
|
|
|
return ret and True
|
|
|
|
|
|
|
|
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
|
|
|
|
for class_map in self.record.class_map:
|
|
|
|
thisclass = classes_by_id[class_map.character_class_id]
|
|
|
|
|
|
|
|
# assign each class attribute available at the character's current
|
|
|
|
# level to the list of the character's class attributes
|
|
|
|
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:
|
2024-03-23 13:46:56 -07:00
|
|
|
self.record.class_attributes.append({
|
|
|
|
'class_attribute_id': attr_map.class_attribute_id,
|
|
|
|
'option_id': default_option.id,
|
|
|
|
})
|
2024-02-26 01:12:45 -08:00
|
|
|
|
|
|
|
def save_callback(self):
|
|
|
|
self.add_class_attributes()
|
2024-02-23 10:45:38 -08:00
|
|
|
|
|
|
|
def populate(self):
|
|
|
|
"""
|
2024-02-26 01:12:45 -08:00
|
|
|
Delete the association proxies' form data before calling form.populate_obj(),
|
|
|
|
and instead use our own methods for populating the fieldlist.
|
2024-02-23 10:45:38 -08:00
|
|
|
"""
|
2024-02-26 01:12:45 -08:00
|
|
|
|
|
|
|
# multiclass form
|
|
|
|
classes_formdata = self.form.data['classes']
|
|
|
|
classes_formdata.append(self.form.data['newclass'])
|
2024-02-23 10:45:38 -08:00
|
|
|
del self.form.classes
|
|
|
|
del self.form.newclass
|
2024-02-26 01:12:45 -08:00
|
|
|
|
|
|
|
# class attributes
|
|
|
|
attrs_formdata = self.form.data['class_attributes']
|
|
|
|
del self.form.class_attributes
|
|
|
|
|
2024-02-23 10:45:38 -08:00
|
|
|
super().populate()
|
2024-02-26 01:12:45 -08:00
|
|
|
|
|
|
|
self.record.classes = self.populate_association('character_class_id', classes_formdata)
|
|
|
|
self.record.class_attributes = self.populate_association('class_attribute_id', attrs_formdata)
|