-
{% for stat in ['str', 'dex', 'con', 'int', 'wis', 'cha'] %}
-
-
{{ c.form[stat].label }}
- {{ c.form[stat] }}
-
-
+
+
{{ c.form[stat].label }}
+ {{ field(stat, DISABLED) }}
+
+
{% endfor %}
-
-
AC
- {{ c.form.armor_class }}
+
+
HP
+ {{ field('hit_points', DISABLED) }} / {{ field('max_hit_points', DISABLED) }}
+
+ TEMP {{ field('temp_hit_points', DISABLED) }}
-
- - Initiative: 3
- - Proficiency Bonus:
- - Spell Save DC:
- - Saving Throws: {{ c.record.saving_throws |join(', ') }}
- - Skills: {{ c.record.skills |join(', ') }}
- {% for field in c.form %}
- {% if field.name in ['proficiencies', 'speed', 'passive_perception', 'passive_insight', 'passive_investigation'] %}
- - {{ field.label }}: {{ field }} {{ field.errors|join(',') }}
- {% endif %}
- {% endfor %}
-
+
+
Skills
+
+ {% for skill in c.record.skills %}
+ {{ skill }} | 3 |
+ {% endfor %}
+
+
+
+
Saving Throws
+ {% for save in c.record.saving_throws %}
+ {{ save }} 3
+ {% endfor %}
+
+
+
+
+
Armor
+ {{ field('armor_class', DISABLED) }}
+
Class
+
+
+
Initiative
+
3
+
Bonus
+
+
+
Speed
+ {{ field('speed', DISABLED) }}
+
+
+
+
+ Actions |
+ To Hit |
+ Range |
+ Targets |
+ Damage |
+
+
+ Attack |
+ Dagger |
+ +7 |
+ 5 |
+ 1 |
+ 1d4+3 slashing |
+
+
+ Attack |
+ Sabetha's Fans |
+ +7 |
+ 5 |
+ 1 |
+ 2d6 slashing |
+
+
+ Spell |
+ Eldritch Blast |
+ +5 |
+ 120 |
+ 1 |
+ 1d10 force |
+
+
+ Bonus Actions |
+ To Hit |
+ Range |
+ Targets |
+ Damage |
+
+
+
+
+ Attack (1 per Action), Cast a Spell, Dash, Disengage, Dodge, Grapple,
Help, Hide, Improvise, Ready, Search, Shove, or Use an Object
+
+
+
+
+
+
+
- {{ c.form.save }} {{ c.form.delete }}
-
{{ c.form.csrf_token }}
@@ -70,6 +157,8 @@
Debug
+{{ DISABLED }}
+
{{ c }}
{% endblock %}
diff --git a/ttfrog/db/base.py b/ttfrog/db/base.py
index ac66fab..55ad7b9 100644
--- a/ttfrog/db/base.py
+++ b/ttfrog/db/base.py
@@ -1,8 +1,8 @@
+import logging
import nanoid
from nanoid_dictionary import human_alphabet
from sqlalchemy import Column
from sqlalchemy import String
-from sqlalchemy import String
from pyramid_sqlalchemy import BaseObject
from slugify import slugify
@@ -31,9 +31,23 @@ class IterableMixin:
for attr in self.__mapper__.columns.keys():
if attr in values:
yield attr, values[attr]
+ for relname in self.__mapper__.relationships.keys():
+ relvals = []
+ for rel in self.__getattribute__(relname):
+ try:
+ relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
+ except TypeError:
+ relvals.append(rel)
+ yield relname, relvals
- def __repr__(self):
- return f"{self.__class__.__name__}: {str(dict(self))}"
+ def __json__(self, request):
+ serialized = dict()
+ for (key, value) in self:
+ try:
+ serialized[key] = getattr(self.value, '__json__')(request)
+ except AttributeError:
+ serialized[key] = value
+ return serialized
def multivalue_string_factory(name, column=Column(String), separator=';'):
diff --git a/ttfrog/db/bootstrap.py b/ttfrog/db/bootstrap.py
index ccc1536..171471e 100644
--- a/ttfrog/db/bootstrap.py
+++ b/ttfrog/db/bootstrap.py
@@ -62,7 +62,7 @@ data = {
'max_hit_points': 14,
'hit_points': 14,
'temp_hit_points': 0,
- 'speed': '30 ft.',
+ 'speed': 30,
'str': 16,
'dex': 12,
'con': 18,
@@ -88,18 +88,32 @@ data = {
{
'id': 1,
'ancestry_id': 1,
- 'description': '+1 to All Ability Scores',
+ 'name': '+1 to All Ability Scores',
+ 'level': 1,
+ },
+ {
+ 'id': 2,
+ 'ancestry_id': 2,
+ 'name': 'Breath Weapon',
'level': 1,
},
],
'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'},
],
diff --git a/ttfrog/db/schema.py b/ttfrog/db/schema.py
index c82a0f3..20cc847 100644
--- a/ttfrog/db/schema.py
+++ b/ttfrog/db/schema.py
@@ -7,6 +7,7 @@ from sqlalchemy import ForeignKey
from sqlalchemy import Enum
from sqlalchemy import Text
from sqlalchemy import UniqueConstraint
+from sqlalchemy.orm import relationship
from ttfrog.db.base import Bases, BaseObject, IterableMixin
from ttfrog.db.base import multivalue_string_factory
@@ -17,9 +18,18 @@ STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
+
+class EnumField(enum.Enum):
+ """
+ A serializable enum.
+ """
+ def __json__(self, request):
+ return self.value
+
+
# enums for db schemas
-StatsEnum = enum.Enum("StatsEnum", ((k, k) for k in STATS))
-CreatureTypesEnum = enum.Enum("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
+StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
+CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
SavingThrowsMixin = multivalue_string_factory('saving_throws')
@@ -50,6 +60,7 @@ class Ancestry(*Bases):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
creature_type = Column(Enum(CreatureTypesEnum))
+ traits = relationship("AncestryTrait")
def __repr__(self):
return str(self.name)
@@ -59,13 +70,10 @@ 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")
+ 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"
@@ -83,8 +91,8 @@ 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")
+ name = Column(String, nullable=False)
+ value = Column(String, nullable=False)
description = Column(Text)
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
@@ -95,14 +103,14 @@ class ClassAttribute(BaseObject, IterableMixin):
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
__tablename__ = "character"
id = Column(Integer, primary_key=True, autoincrement=True)
- ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False)
+ ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False, default='human')
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.")
+ temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0, 'max': 999})
+ speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
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})
diff --git a/ttfrog/webserver/controllers/__init__.py b/ttfrog/webserver/controllers/__init__.py
index 4ae38d0..b5cc66d 100644
--- a/ttfrog/webserver/controllers/__init__.py
+++ b/ttfrog/webserver/controllers/__init__.py
@@ -1,4 +1,5 @@
from .base import BaseController
from .character_sheet import CharacterSheet
+from .json_data import JsonData
-__all__ = [BaseController, CharacterSheet]
+__all__ = [BaseController, CharacterSheet, JsonData]
diff --git a/ttfrog/webserver/controllers/base.py b/ttfrog/webserver/controllers/base.py
index 0eac46a..03f2a35 100644
--- a/ttfrog/webserver/controllers/base.py
+++ b/ttfrog/webserver/controllers/base.py
@@ -63,6 +63,8 @@ class BaseController:
def form(self):
if not self.model:
return
+ if not self.model_form:
+ return
if not self._form:
if self.request.POST:
self._form = self.model_form(self.request.POST, obj=self.record)
@@ -106,8 +108,9 @@ class BaseController:
db.add(self.record)
logging.debug(f"Added {self.record = }")
location = self.request.current_route_path()
- if self.slug not in location:
+ if self.record.slug not in location:
location = f"{location}/{self.record.uri}"
+ logging.debug(f"Redirecting to {location}")
return HTTPFound(location=location)
def delete(self):
diff --git a/ttfrog/webserver/controllers/character_sheet.py b/ttfrog/webserver/controllers/character_sheet.py
index 79b65db..143d455 100644
--- a/ttfrog/webserver/controllers/character_sheet.py
+++ b/ttfrog/webserver/controllers/character_sheet.py
@@ -17,7 +17,7 @@ class CharacterForm(ModelForm):
save = SubmitField()
delete = SubmitField()
- ancestry = DeferredSelectField('Ancestry', model=Ancestry, validate_choice=True, widget=Select())
+ ancestry = DeferredSelectField('Ancestry', model=Ancestry, default='human', validate_choice=True, widget=Select())
character_class = DeferredSelectMultipleField(
'CharacterClass',
diff --git a/ttfrog/webserver/routes.py b/ttfrog/webserver/routes.py
index d19df81..4b03c8b 100644
--- a/ttfrog/webserver/routes.py
+++ b/ttfrog/webserver/routes.py
@@ -1,3 +1,4 @@
def routes(config):
config.add_route('index', '/')
config.add_route('sheet', '/c{uri:.*}', factory='ttfrog.webserver.controllers.CharacterSheet')
+ config.add_route('data', '/_/{table_name}{uri:.*}', factory='ttfrog.webserver.controllers.JsonData')
diff --git a/ttfrog/webserver/views.py b/ttfrog/webserver/views.py
index b77ea1b..6803aa7 100644
--- a/ttfrog/webserver/views.py
+++ b/ttfrog/webserver/views.py
@@ -17,3 +17,7 @@ def index(request):
@view_config(route_name='sheet', renderer='character_sheet.html')
def sheet(request):
return response_from(request.context)
+
+@view_config(route_name='data', renderer='json')
+def data(request):
+ return response_from(request.context)