diff --git a/ttfrog/assets/static/js/character_sheet.js b/ttfrog/assets/static/js/character_sheet.js index 15bdfb4..41d3b32 100644 --- a/ttfrog/assets/static/js/character_sheet.js +++ b/ttfrog/assets/static/js/character_sheet.js @@ -1,3 +1,20 @@ +function getTraitModifiersForStat(stat) { + var mods = {}; + for (const prop in TRAITS) { + var props = []; + for (const desc in TRAITS[prop]) { + trait = TRAITS[prop][desc] + if (trait.type == "stat" && trait.target == stat) { + props.push(trait); + } + } + if (props) { + mods[prop] = props; + } + } + return mods; +} + function proficiency() { return parseInt(document.getElementById('proficiency_bonus').innerHTML); } @@ -12,6 +29,19 @@ function setStatBonus(stat) { document.getElementById(stat + '_bonus').innerHTML = bonus; } +function applyStatModifiers(stat) { + var score = parseInt(document.getElementById(stat).value); + var modsForStat = getTraitModifiersForStat(stat); + for (desc in modsForStat) { + for (idx in modsForStat[desc]) { + var value = modsForStat[desc][idx].value; + console.log(`Ancestry Trait "${desc}" grants ${value} to ${stat}`); + score += parseInt(value); + } + } + document.getElementById(stat).value = score; +} + function setProficiencyBonus() { var score = document.getElementById('level').value; var bonus = Math.ceil(1 + (0.25 * score)); @@ -25,7 +55,9 @@ function setSpellSaveDC() { (function () { const stats = ['str', 'dex', 'con', 'int', 'wis', 'cha']; + stats.forEach(applyStatModifiers); stats.forEach(setStatBonus); setProficiencyBonus(); setSpellSaveDC(); + })(); diff --git a/ttfrog/assets/templates/base.html b/ttfrog/assets/templates/base.html index 2e25d02..c267eab 100644 --- a/ttfrog/assets/templates/base.html +++ b/ttfrog/assets/templates/base.html @@ -18,6 +18,7 @@ {% block content %}{% endblock %} {% block debug %}{% endblock %} + {% block script %}{% endblock %} {% for resource in c.resources %} {% if resource['type'] == 'script' %} diff --git a/ttfrog/assets/templates/character_sheet.html b/ttfrog/assets/templates/character_sheet.html index baece9f..15e4990 100644 --- a/ttfrog/assets/templates/character_sheet.html +++ b/ttfrog/assets/templates/character_sheet.html @@ -11,7 +11,7 @@ @@ -73,3 +73,25 @@ {{ c }} {% endblock %} + + +{% block script %} + +{% endblock %} diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py index fb38c6f..ccc1536 100644 --- a/ttfrog/db/bootstrap.py +++ b/ttfrog/db/bootstrap.py @@ -9,6 +9,7 @@ from sqlalchemy.exc import IntegrityError data = { 'CharacterClass': [ { + 'id': 1, 'name': 'fighter', 'hit_dice': '1d10', 'hit_dice_stat': 'CON', @@ -17,6 +18,7 @@ data = { 'skills': ['Acrobatics', 'Animal Handling', 'Athletics', 'History', 'Insight', 'Intimidation', 'Perception', 'Survival'], }, { + 'id': 2, 'name': 'rogue', 'hit_dice': '1d8', 'hit_dice_stat': 'DEX', @@ -25,6 +27,7 @@ data = { 'skills': ['Acrobatics', 'Athletics', 'Deception', 'Insight', 'Intimidation', 'Investigation', 'Perception', 'Performance', 'Persuasion', 'Sleight of Hand', 'Stealth'], }, ], + 'Skill': [ {'name': 'Acrobatics'}, {'name': 'Animal Handling'}, @@ -41,25 +44,24 @@ data = { {'name': 'Stealth'}, {'name': 'Survival'}, ], + 'Ancestry': [ - {'name': 'human', 'creature_type': 'humanoid'}, - {'name': 'dragonborn', 'creature_type': 'humanoid'}, - {'name': 'tiefling', 'creature_type': 'humanoid'}, + {'id': 1, 'name': 'human', 'creature_type': 'humanoid'}, + {'id': 2, 'name': 'dragonborn', 'creature_type': 'humanoid'}, + {'id': 3, 'name': 'tiefling', 'creature_type': 'humanoid'}, ], + 'Character': [ { 'id': 1, 'name': 'Sabetha', - 'ancestry': 'tiefling', + 'ancestry': 'human', 'character_class': ['fighter', 'rogue'], 'level': 1, 'armor_class': 10, 'max_hit_points': 14, 'hit_points': 14, 'temp_hit_points': 0, - 'passive_perception': 10, - 'passive_insight': 10, - 'passive_investigation': 10, 'speed': '30 ft.', 'str': 16, 'dex': 12, @@ -71,7 +73,36 @@ data = { 'saving_throws': ['STR', 'CON'], 'skills': ['Acrobatics', 'Animal Handling'], }, - ] + ], + + 'ClassAttribute': [ + { + 'character_class_id': 1, + 'name': 'Fighting Style', + 'value': 'Archery', + 'level': 1, + }, + ], + + 'AncestryTrait': [ + { + 'id': 1, + 'ancestry_id': 1, + 'description': '+1 to All Ability Scores', + 'level': 1, + }, + ], + + 'Modifier': [ + {'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'}, + {'source_table_name': 'class_attribute', 'source_table_id': 1, 'value': '+2', 'type': 'weapon ', 'target': 'ranged'}, + ], + } diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py index 8dad933..c82a0f3 100644 --- a/ttfrog/db/schema.py +++ b/ttfrog/db/schema.py @@ -6,6 +6,7 @@ from sqlalchemy import String from sqlalchemy import ForeignKey from sqlalchemy import Enum from sqlalchemy import Text +from sqlalchemy import UniqueConstraint from ttfrog.db.base import Bases, BaseObject, IterableMixin from ttfrog.db.base import multivalue_string_factory @@ -54,6 +55,18 @@ class Ancestry(*Bases): return str(self.name) +class AncestryTrait(BaseObject, IterableMixin): + __tablename__ = "ancestry_trait" + id = Column(Integer, primary_key=True, autoincrement=True) + ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False) + name = Column(String, nullable="False") + description = Column(Text) + level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) + + def __repr__(self): + return str(self.name) + + class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin): __tablename__ = "character_class" id = Column(Integer, primary_key=True, autoincrement=True) @@ -66,29 +79,52 @@ class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin): return str(self.name) +class ClassAttribute(BaseObject, IterableMixin): + __tablename__ = "class_attribute" + id = Column(Integer, primary_key=True, autoincrement=True) + character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False) + name = Column(String, nullable="False") + value = Column(String, nullable="False") + description = Column(Text) + level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) + + def __repr__(self): + return str(self.name) + + class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin): __tablename__ = "character" id = Column(Integer, primary_key=True, autoincrement=True) ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False) - name = Column(String(255), nullable=False) - level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}) - armor_class = Column(Integer, nullable=False, info={'min': 1, 'max': 99}) - hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999}) - max_hit_points = Column(Integer, nullable=False, info={'min': 0, 'max': 999}) - temp_hit_points = Column(Integer, nullable=False, info={'min': 0}) - passive_perception = Column(Integer, nullable=False) - passive_insight = Column(Integer, nullable=False) - passive_investigation = Column(Integer, nullable=False) + name = Column(String, default='New Character', nullable=False) + level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20}) + 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}) + temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0}) speed = Column(String, nullable=False, default="30 ft.") - str = Column(Integer, info={'min': 0, 'max': 30}) - dex = Column(Integer, info={'min': 0, 'max': 30}) - con = Column(Integer, info={'min': 0, 'max': 30}) - int = Column(Integer, info={'min': 0, 'max': 30}) - wis = Column(Integer, info={'min': 0, 'max': 30}) - cha = Column(Integer, info={'min': 0, 'max': 30}) + 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}) proficiencies = Column(String) +class Modifier(BaseObject, IterableMixin): + __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) + + class TransactionLog(BaseObject, IterableMixin): __tablename__ = "transaction_log" id = Column(Integer, primary_key=True, autoincrement=True) diff --git a/ttfrog/webserver/controllers/base.py b/ttfrog/webserver/controllers/base.py index 18c45b2..d142e6b 100644 --- a/ttfrog/webserver/controllers/base.py +++ b/ttfrog/webserver/controllers/base.py @@ -6,7 +6,6 @@ from collections import defaultdict from pyramid.httpexceptions import HTTPFound from pyramid.interfaces import IRoutesMapper -from ttfrog.attribute_map import AttributeMap from ttfrog.db.manager import db from ttfrog.db import transaction_log @@ -69,6 +68,8 @@ class BaseController: self._form = self.model_form(self.request.POST, obj=self.record) else: self._form = self.model_form(obj=self.record) + if not self.record.id: + self._form.process() return self._form @property @@ -82,18 +83,16 @@ class BaseController: self.attrs['all_records'] = db.query(self.model).all() def template_context(self, **kwargs) -> dict: - return AttributeMap.from_dict({ - 'c': dict( - config=self.config, - request=self.request, - form=self.form, - record=self.record, - routes=get_all_routes(self.request), - resources=self.resources, - **self.attrs, - **kwargs, - ) - }) + return dict( + config=self.config, + request=self.request, + form=self.form, + record=self.record, + routes=get_all_routes(self.request), + resources=self.resources, + **self.attrs, + **kwargs, + ) def save(self): if not self.form.save.data: diff --git a/ttfrog/webserver/controllers/character_sheet.py b/ttfrog/webserver/controllers/character_sheet.py index 02bcffd..79b65db 100644 --- a/ttfrog/webserver/controllers/character_sheet.py +++ b/ttfrog/webserver/controllers/character_sheet.py @@ -1,6 +1,9 @@ from ttfrog.webserver.controllers.base import BaseController from ttfrog.webserver.forms import DeferredSelectField, DeferredSelectMultipleField -from ttfrog.db.schema import Character, Ancestry, CharacterClass, STATS +from ttfrog.db.schema import Character, Ancestry, CharacterClass, AncestryTrait, Modifier, STATS +from ttfrog.db.manager import db +from ttfrog.attribute_map import AttributeMap + from wtforms_alchemy import ModelForm from wtforms.fields import SubmitField, SelectMultipleField from wtforms.widgets import Select @@ -35,3 +38,14 @@ class CharacterSheet(BaseController): return super().resources + [ {'type': 'script', 'uri': 'js/character_sheet.js'}, ] + + def template_context(self, **kwargs) -> dict: + ctx = super().template_context(**kwargs) + if self.record.ancestry: + ancestry = db.query(Ancestry).filter_by(name=self.record.ancestry).one() + ctx['traits'] = {} + for trait in db.query(AncestryTrait).filter_by(ancestry_id=ancestry.id).all(): + ctx['traits'][trait.description] = db.query(Modifier).filter_by(source_table_name=trait.__tablename__, source_table_id=trait.id).all() + else: + ctx['traits'] = {}; + return ctx diff --git a/ttfrog/webserver/views.py b/ttfrog/webserver/views.py index d2addf1..b77ea1b 100644 --- a/ttfrog/webserver/views.py +++ b/ttfrog/webserver/views.py @@ -2,6 +2,10 @@ from pyramid.response import Response from pyramid.view import view_config from ttfrog.db.manager import db from ttfrog.db.schema import Ancestry +from ttfrog.attribute_map import AttributeMap + +def response_from(controller): + return controller.response() or AttributeMap.from_dict({'c': controller.template_context()}) @view_config(route_name='index') @@ -12,5 +16,4 @@ def index(request): @view_config(route_name='sheet', renderer='character_sheet.html') def sheet(request): - controller = request.context - return controller.response() or controller.template_context() + return response_from(request.context)