Started implementing requirements into code...

This commit is contained in:
Mechseroms 2026-05-06 18:35:19 -05:00
parent 82487a417c
commit c1ad629e79
92 changed files with 2831 additions and 224 deletions

View File

@ -1,28 +1,25 @@
# AGENTS.md: Guidelines for Agent Coders (2025)
# AGENTS.md: Agent Coding Guidelines (2025)
## Build/Lint/Test
- Main entry: `python main.py`
- No test suite found; to run a single test (if using pytest):
- `pytest test_file.py::test_func`
- Lint (if installed): `flake8 .` or `pylint main.py`
- Add to `requirements.txt` if you introduce dependencies.
- No shell/env scripts found by default.
## Build, Lint, and Test Commands
- Main entry point: `python main.py`
- Start dev server: `python manage.py runserver`
- Run all tests: `pytest main/tests.py`
- Run a single test: `pytest main/tests.py::test_func`
- Lint code (if installed): `flake8 .` or `pylint main.py`
- Add new dependencies: update `requirements.txt`
## Python Code Style
- Follow [PEP8](https://pep8.org/) (4 spaces, ≤79 chars/line)
- Import order: stdlib, third-party, project/local
- Never use wildcard imports; always explicit
- Naming: snake_case for vars/functions, PascalCase for classes, UPPER_SNAKE_CASE for constants
- Add type annotations and docstrings to all public APIs/classes
- Handle errors with try/except; log or re-raise as needed
- One statement per line; trim trailing whitespace
- Avoid global state. Organize code into functions/classes.
- Use `if __name__ == '__main__'` to guard scripts.
## Python & Django Style Guide
- Follow [PEP8](https://pep8.org/): 4 spaces/indent, ≤79 chars/line
- Import order: stdlib, third-party, then project/local modules
- Use explicit imports; do NOT use wildcard imports
- Naming: snake_case (vars/functions), PascalCase (classes), UPPER_SNAKE_CASE (constants)
- All public classes/APIs require type annotations and docstrings
- Django models/views should have descriptive docstrings
- Handle errors with try/except; log, re-raise, or message as appropriate
- One statement per line, trim trailing whitespace
- Avoid global state; use functions/classes, avoid module-level code
- Guard standalone scripts with `if __name__ == '__main__':`
- No Cursor or Copilot rules present as of 2025
- Reference `.github/instructions/memory.instruction.md` for user customizations
- No Cursor/.Copilot rules found (Dec 2025)
(Update as project conventions evolve)
## Custom Rules
- Ensure that you guide me to code myself and not write code that I should learn to do myself.
(Update and evolve as practices change)

157
character_template.json Normal file
View File

@ -0,0 +1,157 @@
{
"name": "",
"level": 0,
"race": "",
"subrace": "",
"class": "",
"subclass": "",
"background": "",
"alignment": "",
"size": "",
"age": 0,
"gender": "",
"height": "",
"weight": 0,
"deity": "",
"strength_base": 0,
"dexterity_base": 0,
"constitution_base": 0,
"intelligence_base": 0,
"wisdom_base": 0,
"charisma_base": 0,
"armor_base": 0,
"strength": 0,
"dexterity": 0,
"constitution": 0,
"intelligence": 0,
"wisdom": 0,
"charisma": 0,
"armor": 0,
"strength_modifier": 0,
"dexterity_modifier": 0,
"constitution_modifier": 0,
"intelligence_modifier": 0,
"wisdom_modifier": 0,
"charisma_modifier": 0,
"proficiency": 0,
"inspiration": false,
"experience": 0,
"proficiencies_armor": [],
"proficiencies_weapons": [],
"proficiencies_tools": [],
"languages": [],
"strength_save": 0,
"dexterity_save": 0,
"constitution_save": 0,
"intelligence_save": 0,
"wisdom_save": 0,
"charisma_save": 0,
"hp": 0,
"hp_max": 0,
"hp_temp": 0,
"hit_die": "",
"initiative": 0,
"deathsaves_successes": 0,
"deathsaves_failures": 0,
"fire_resistance": false,
"poison_resistance": false,
"psychic_resistance": false,
"cold_resistance": false,
"thunder_resistance": false,
"acid_resistance": false,
"force_resistance": false,
"radiant_resistance": false,
"necrotic_resistance": false,
"bludgeoning_resistance": false,
"piercing_resistance": false,
"slashing_resistance": false,
"immunities": [],
"vulnerabilities": [],
"speed_base": 0,
"speed_type": [],
"darkvision": 0,
"blindsight": 0,
"tremorsense": 0,
"truesight": 0,
"athletics": 0,
"acrobatics": 0,
"sleight_of_hand": 0,
"stealth": 0,
"arcana": 0,
"history": 0,
"investigation": 0,
"nature": 0,
"religion": 0,
"animal_handling": 0,
"insight": 0,
"medicine": 0,
"perception": 0,
"survival": 0,
"deception": 0,
"intimidation": 0,
"performance": 0,
"persuasion": 0,
"athletics_passive": 0,
"acrobatics_passive": 0,
"sleight_of_hand_passive": 0,
"stealth_passive": 0,
"arcana_passive": 0,
"history_passive": 0,
"investigation_passive": 0,
"nature_passive": 0,
"religion_passive": 0,
"animal_handling_passive": 0,
"insight_passive": 0,
"medicine_passive": 0,
"perception_passive": 0,
"survival_passive": 0,
"deception_passive": 0,
"intimidation_passive": 0,
"performance_passive": 0,
"persuasion_passive": 0,
"passive_perception": 0,
"passive_investigation": 0,
"passive_insight": 0,
"spellcasting_ability": "",
"spell_save_dc": 0,
"spell_attack_bonus": 0,
"known_spells": [],
"prepared_spells": [],
"spell_slots": {},
"cantrips": [],
"spellcasting_class": "",
"exhaustion_level": 0,
"conditions_active": [],
"cp": 0,
"sp": 0,
"ep": 0,
"gp": 0,
"pp": 0,
"equipment": [],
"packages": [],
"features": [],
"traits": [],
"personality_traits": [],
"ideals": [],
"bonds": [],
"flaws": [],
"notes": "",
"custom_attributes": {}
}

Binary file not shown.

7
feature_template.json Normal file
View File

@ -0,0 +1,7 @@
{
"feature_name": "",
"feature_description": "",
"feature_data": {
"effects": []
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-c@*)_9zq1%@%nv+q18ji6k^ixf(^!7@**di7_r-s5q33m@&w)q
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['192.168.1.45', '127.0.0.1']
# Application definition
@ -38,6 +38,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
'rest_framework',
]
MIDDLEWARE = [
@ -81,6 +82,9 @@ DATABASES = {
}
# Use custom user model with uuid field
AUTH_USER_MODEL = 'main.CustomUser'
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
@ -115,4 +119,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / "static", # or os.path.join(BASE_DIR, "static") for Django <4
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,42 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser, Character, Feature, Package, PackageFeature, Pin
# Register your models here.
class CustomUserAdmin(UserAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'uuid')
readonly_fields = ('uuid',)
fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('uuid',)}),
)
admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Character)
class PackageFeatureInline(admin.TabularInline):
model = PackageFeature
extra = 1
autocomplete_fields = ['feature']
fields = ['feature', 'priority']
ordering = ['priority']
@admin.register(Package)
class PackageAdmin(admin.ModelAdmin):
inlines = [PackageFeatureInline]
list_display = ['package_name', 'package_type']
search_fields = ['package_name', 'package_type']
@admin.register(Feature)
class FeatureAdmin(admin.ModelAdmin):
list_display = ['feature_name']
search_fields = ['feature_name']
@admin.register(PackageFeature)
class PackageFeatureAdmin(admin.ModelAdmin):
list_display = ['package', 'feature', 'priority']
list_filter = ['package']
search_fields = ['package__package_name', 'feature__feature_name']
@admin.register(Pin)
class PinAdmin(admin.ModelAdmin):
list_display = ('label', 'url', 'x', 'y')
search_fields = ('label', 'url')

View File

@ -0,0 +1,231 @@
# Generated by Django 6.0 on 2025-12-14 02:30
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Feature',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('feature_name', models.CharField(max_length=200)),
('feature_description', models.TextField(blank=True)),
('feature_data', models.JSONField(blank=True, default=dict)),
],
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('package_name', models.CharField(max_length=200)),
('package_description', models.TextField(blank=True)),
('package_type', models.CharField(blank=True, max_length=100)),
('package_doc_md', models.TextField(blank=True)),
],
),
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Character',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('level', models.IntegerField(default=0)),
('alignment', models.CharField(blank=True, max_length=50)),
('size', models.CharField(blank=True, max_length=20)),
('age', models.IntegerField(default=0)),
('gender', models.CharField(blank=True, max_length=30)),
('height', models.CharField(blank=True, max_length=20)),
('weight', models.IntegerField(default=0)),
('deity', models.CharField(blank=True, max_length=100)),
('strength_base', models.IntegerField(default=0)),
('dexterity_base', models.IntegerField(default=0)),
('constitution_base', models.IntegerField(default=0)),
('intelligence_base', models.IntegerField(default=0)),
('wisdom_base', models.IntegerField(default=0)),
('charisma_base', models.IntegerField(default=0)),
('armor_base', models.IntegerField(default=0)),
('strength', models.IntegerField(default=0)),
('dexterity', models.IntegerField(default=0)),
('constitution', models.IntegerField(default=0)),
('intelligence', models.IntegerField(default=0)),
('wisdom', models.IntegerField(default=0)),
('charisma', models.IntegerField(default=0)),
('armor', models.IntegerField(default=0)),
('strength_modifier', models.IntegerField(default=0)),
('dexterity_modifier', models.IntegerField(default=0)),
('constitution_modifier', models.IntegerField(default=0)),
('intelligence_modifier', models.IntegerField(default=0)),
('wisdom_modifier', models.IntegerField(default=0)),
('charisma_modifier', models.IntegerField(default=0)),
('proficiency', models.IntegerField(default=0)),
('inspiration', models.BooleanField(default=False)),
('experience', models.IntegerField(default=0)),
('proficiencies_armor', models.JSONField(blank=True, default=list)),
('proficiencies_weapons', models.JSONField(blank=True, default=list)),
('proficiencies_tools', models.JSONField(blank=True, default=list)),
('languages', models.JSONField(blank=True, default=list)),
('strength_save', models.IntegerField(default=0)),
('dexterity_save', models.IntegerField(default=0)),
('constitution_save', models.IntegerField(default=0)),
('intelligence_save', models.IntegerField(default=0)),
('wisdom_save', models.IntegerField(default=0)),
('charisma_save', models.IntegerField(default=0)),
('hp', models.IntegerField(default=0)),
('hp_max', models.IntegerField(default=0)),
('hp_temp', models.IntegerField(default=0)),
('hit_die', models.CharField(blank=True, max_length=20)),
('initiative', models.IntegerField(default=0)),
('deathsaves_successes', models.IntegerField(default=0)),
('deathsaves_failures', models.IntegerField(default=0)),
('fire_resistance', models.BooleanField(default=False)),
('poison_resistance', models.BooleanField(default=False)),
('psychic_resistance', models.BooleanField(default=False)),
('cold_resistance', models.BooleanField(default=False)),
('thunder_resistance', models.BooleanField(default=False)),
('acid_resistance', models.BooleanField(default=False)),
('force_resistance', models.BooleanField(default=False)),
('radiant_resistance', models.BooleanField(default=False)),
('necrotic_resistance', models.BooleanField(default=False)),
('bludgeoning_resistance', models.BooleanField(default=False)),
('piercing_resistance', models.BooleanField(default=False)),
('slashing_resistance', models.BooleanField(default=False)),
('immunities', models.JSONField(blank=True, default=list)),
('vulnerabilities', models.JSONField(blank=True, default=list)),
('speed_base', models.IntegerField(default=0)),
('speed_type', models.JSONField(blank=True, default=list)),
('darkvision', models.IntegerField(default=0)),
('blindsight', models.IntegerField(default=0)),
('tremorsense', models.IntegerField(default=0)),
('truesight', models.IntegerField(default=0)),
('athletics', models.IntegerField(default=0)),
('acrobatics', models.IntegerField(default=0)),
('sleight_of_hand', models.IntegerField(default=0)),
('stealth', models.IntegerField(default=0)),
('arcana', models.IntegerField(default=0)),
('history', models.IntegerField(default=0)),
('investigation', models.IntegerField(default=0)),
('nature', models.IntegerField(default=0)),
('religion', models.IntegerField(default=0)),
('animal_handling', models.IntegerField(default=0)),
('insight', models.IntegerField(default=0)),
('medicine', models.IntegerField(default=0)),
('perception', models.IntegerField(default=0)),
('survival', models.IntegerField(default=0)),
('deception', models.IntegerField(default=0)),
('intimidation', models.IntegerField(default=0)),
('performance', models.IntegerField(default=0)),
('persuasion', models.IntegerField(default=0)),
('athletics_passive', models.IntegerField(default=0)),
('acrobatics_passive', models.IntegerField(default=0)),
('sleight_of_hand_passive', models.IntegerField(default=0)),
('stealth_passive', models.IntegerField(default=0)),
('arcana_passive', models.IntegerField(default=0)),
('history_passive', models.IntegerField(default=0)),
('investigation_passive', models.IntegerField(default=0)),
('nature_passive', models.IntegerField(default=0)),
('religion_passive', models.IntegerField(default=0)),
('animal_handling_passive', models.IntegerField(default=0)),
('insight_passive', models.IntegerField(default=0)),
('medicine_passive', models.IntegerField(default=0)),
('perception_passive', models.IntegerField(default=0)),
('survival_passive', models.IntegerField(default=0)),
('deception_passive', models.IntegerField(default=0)),
('intimidation_passive', models.IntegerField(default=0)),
('performance_passive', models.IntegerField(default=0)),
('persuasion_passive', models.IntegerField(default=0)),
('passive_perception', models.IntegerField(default=0)),
('passive_investigation', models.IntegerField(default=0)),
('passive_insight', models.IntegerField(default=0)),
('spellcasting_ability', models.CharField(blank=True, max_length=50)),
('spell_save_dc', models.IntegerField(default=0)),
('spell_attack_bonus', models.IntegerField(default=0)),
('known_spells', models.JSONField(blank=True, default=list)),
('prepared_spells', models.JSONField(blank=True, default=list)),
('spell_slots', models.JSONField(blank=True, default=dict)),
('cantrips', models.JSONField(blank=True, default=list)),
('spellcasting_class', models.CharField(blank=True, max_length=100)),
('exhaustion_level', models.IntegerField(default=0)),
('conditions_active', models.JSONField(blank=True, default=list)),
('cp', models.IntegerField(default=0)),
('sp', models.IntegerField(default=0)),
('ep', models.IntegerField(default=0)),
('gp', models.IntegerField(default=0)),
('pp', models.IntegerField(default=0)),
('equipment', models.JSONField(blank=True, default=list)),
('features', models.JSONField(blank=True, default=list)),
('traits', models.JSONField(blank=True, default=list)),
('personality_traits', models.JSONField(blank=True, default=list)),
('ideals', models.JSONField(blank=True, default=list)),
('bonds', models.JSONField(blank=True, default=list)),
('flaws', models.JSONField(blank=True, default=list)),
('notes', models.TextField(blank=True)),
('custom_attributes', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to=settings.AUTH_USER_MODEL)),
('background', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='background_characters', to='main.package')),
('char_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='class_characters', to='main.package')),
('packages', models.ManyToManyField(blank=True, related_name='characters', to='main.package')),
('race', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='race_characters', to='main.package')),
('subclass', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subclass_characters', to='main.package')),
('subrace', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subrace_characters', to='main.package')),
],
),
migrations.CreateModel(
name='PackageFeature',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.IntegerField(default=0)),
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.feature')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.package')),
],
options={
'ordering': ['priority'],
'unique_together': {('package', 'feature')},
},
),
migrations.AddField(
model_name='package',
name='features',
field=models.ManyToManyField(blank=True, related_name='packages', through='main.PackageFeature', to='main.feature'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 6.0 on 2025-12-14 16:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Pin',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=100)),
('url', models.URLField(max_length=300)),
('x', models.FloatField(help_text='X position as percentage (0-100)')),
('y', models.FloatField(help_text='Y position as percentage (0-100)')),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-14 18:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0002_pin'),
]
operations = [
migrations.AddField(
model_name='pin',
name='pin_type',
field=models.CharField(default='general', max_length=100),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 6.0 on 2025-12-14 21:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_pin_pin_type'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='features',
),
migrations.AddField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.feature'),
),
]

View File

@ -0,0 +1,123 @@
# Generated by Django 6.0 on 2025-12-14 21:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0004_remove_character_features_character_features'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='armor',
),
migrations.RemoveField(
model_name='character',
name='charisma',
),
migrations.RemoveField(
model_name='character',
name='charisma_modifier',
),
migrations.RemoveField(
model_name='character',
name='charisma_save',
),
migrations.RemoveField(
model_name='character',
name='constitution',
),
migrations.RemoveField(
model_name='character',
name='constitution_modifier',
),
migrations.RemoveField(
model_name='character',
name='constitution_save',
),
migrations.RemoveField(
model_name='character',
name='dexterity',
),
migrations.RemoveField(
model_name='character',
name='dexterity_modifier',
),
migrations.RemoveField(
model_name='character',
name='dexterity_save',
),
migrations.RemoveField(
model_name='character',
name='intelligence',
),
migrations.RemoveField(
model_name='character',
name='intelligence_modifier',
),
migrations.RemoveField(
model_name='character',
name='intelligence_save',
),
migrations.RemoveField(
model_name='character',
name='proficiency',
),
migrations.RemoveField(
model_name='character',
name='strength',
),
migrations.RemoveField(
model_name='character',
name='strength_modifier',
),
migrations.RemoveField(
model_name='character',
name='strength_save',
),
migrations.RemoveField(
model_name='character',
name='wisdom',
),
migrations.RemoveField(
model_name='character',
name='wisdom_modifier',
),
migrations.RemoveField(
model_name='character',
name='wisdom_save',
),
migrations.AddField(
model_name='character',
name='charisma_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='constitution_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='dexterity_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='intelligence_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='strength_save_prof',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='character',
name='wisdom_save_prof',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 6.0 on 2025-12-14 21:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0005_remove_character_armor_remove_character_charisma_and_more'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='athletics',
),
]

View File

@ -0,0 +1,165 @@
# Generated by Django 6.0 on 2025-12-14 22:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0006_remove_character_athletics'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='acrobatics',
),
migrations.RemoveField(
model_name='character',
name='acrobatics_passive',
),
migrations.RemoveField(
model_name='character',
name='animal_handling',
),
migrations.RemoveField(
model_name='character',
name='animal_handling_passive',
),
migrations.RemoveField(
model_name='character',
name='arcana',
),
migrations.RemoveField(
model_name='character',
name='arcana_passive',
),
migrations.RemoveField(
model_name='character',
name='athletics_passive',
),
migrations.RemoveField(
model_name='character',
name='deception',
),
migrations.RemoveField(
model_name='character',
name='deception_passive',
),
migrations.RemoveField(
model_name='character',
name='history',
),
migrations.RemoveField(
model_name='character',
name='history_passive',
),
migrations.RemoveField(
model_name='character',
name='insight',
),
migrations.RemoveField(
model_name='character',
name='insight_passive',
),
migrations.RemoveField(
model_name='character',
name='intimidation',
),
migrations.RemoveField(
model_name='character',
name='intimidation_passive',
),
migrations.RemoveField(
model_name='character',
name='investigation',
),
migrations.RemoveField(
model_name='character',
name='investigation_passive',
),
migrations.RemoveField(
model_name='character',
name='medicine',
),
migrations.RemoveField(
model_name='character',
name='medicine_passive',
),
migrations.RemoveField(
model_name='character',
name='nature',
),
migrations.RemoveField(
model_name='character',
name='nature_passive',
),
migrations.RemoveField(
model_name='character',
name='passive_insight',
),
migrations.RemoveField(
model_name='character',
name='passive_investigation',
),
migrations.RemoveField(
model_name='character',
name='passive_perception',
),
migrations.RemoveField(
model_name='character',
name='perception',
),
migrations.RemoveField(
model_name='character',
name='perception_passive',
),
migrations.RemoveField(
model_name='character',
name='performance',
),
migrations.RemoveField(
model_name='character',
name='performance_passive',
),
migrations.RemoveField(
model_name='character',
name='persuasion',
),
migrations.RemoveField(
model_name='character',
name='persuasion_passive',
),
migrations.RemoveField(
model_name='character',
name='religion',
),
migrations.RemoveField(
model_name='character',
name='religion_passive',
),
migrations.RemoveField(
model_name='character',
name='sleight_of_hand',
),
migrations.RemoveField(
model_name='character',
name='sleight_of_hand_passive',
),
migrations.RemoveField(
model_name='character',
name='stealth',
),
migrations.RemoveField(
model_name='character',
name='stealth_passive',
),
migrations.RemoveField(
model_name='character',
name='survival',
),
migrations.RemoveField(
model_name='character',
name='survival_passive',
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 6.0 on 2025-12-14 23:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0007_remove_character_acrobatics_and_more'),
]
operations = [
migrations.RemoveField(
model_name='character',
name='hp_max',
),
migrations.RemoveField(
model_name='character',
name='initiative',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-14 23:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0008_remove_character_hp_max_remove_character_initiative'),
]
operations = [
migrations.AddField(
model_name='character',
name='hp_features',
field=models.JSONField(blank=True, default=dict),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-14 23:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0009_character_hp_features'),
]
operations = [
migrations.AlterField(
model_name='character',
name='hp_features',
field=models.JSONField(blank=True, default=list),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-15 00:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0010_alter_character_hp_features'),
]
operations = [
migrations.AddField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(default=dict),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-15 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0011_feature_feature_requirements'),
]
operations = [
migrations.AddField(
model_name='packagefeature',
name='requirements_override',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-15 01:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0012_packagefeature_requirements_override'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(default=list),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 6.0.5 on 2026-05-06 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0013_alter_feature_feature_requirements'),
]
operations = [
migrations.AlterField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='+', to='main.feature'),
),
migrations.AlterField(
model_name='character',
name='packages',
field=models.ManyToManyField(blank=True, related_name='+', to='main.package'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 6.0.5 on 2026-05-06 22:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0014_alter_character_features_alter_character_packages'),
]
operations = [
migrations.AlterField(
model_name='character',
name='features',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.feature'),
),
migrations.AlterField(
model_name='character',
name='packages',
field=models.ManyToManyField(blank=True, related_name='characters', to='main.package'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-06 23:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0015_alter_character_features_alter_character_packages'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='feature_requirements',
field=models.JSONField(blank=True, default=list),
),
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,632 @@
from django.db import models
import uuid
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
"""
Custom user model that extends AbstractUser, adding a UUID field
for unique identification and linking to other tables.
"""
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
def __str__(self):
return self.username
def check_requirements(character, requirements):
for requirement in requirements:
char_prop = getattr(character, requirement['property'])
#print(char_prop, requirement['value'], requirement['condition'])
if requirement['condition'] == "==":
#print("==")
if requirement['value'] != char_prop:
return False
if requirement['condition'] == "!=":
#print("!=")
if requirement['value'] == char_prop:
return False
if requirement['condition'] == "<=":
#print("<=")
if requirement['value'] < char_prop:
return False
if requirement['condition'] == ">=":
#print(">=")
if requirement['value'] > char_prop:
return False
return True
def get_operations_for_hp_max(character, attr):
ops = []
for feature in character.hp_features:
if check_requirements(character, feature['feature_requirements']):
feature_ops = feature['feature_data'].get('operations', [])
for op in feature_ops:
if op['attr'] == attr:
ops.append(op)
return ops
def get_operations_for_attr(character, attr):
ops = []
for feature in character.features.all():
feature_ops = feature.feature_data.get('operations', [])
if check_requirements(character, feature.feature_requirements):
for op in feature_ops:
if op['attr'] == attr:
ops.append(op)
return ops
def apply_operations(base, operations, character):
"""Apply a list of operations (add, multiply, set, etc) in order."""
value = base
for op in operations:
if op['operation'] == 'add':
if isinstance(op['value'], int):
value += op['value']
if isinstance(op['value'], str):
value += getattr(character, op['value'])
elif op['operation'] == 'subtract':
if isinstance(op['value'], int):
value -= op['value']
if isinstance(op['value'], str):
value -= getattr(character, op['value'])
elif op['operation'] == 'multiply':
if isinstance(op['value'], int):
value *= op['value']
if isinstance(op['value'], str):
value *= getattr(character, op['value'])
elif op['operation'] == 'divide':
if isinstance(op['value'], int):
value /= op['value']
if isinstance(op['value'], str):
value /= getattr(character, op['value'])
elif op['operation'] == 'set':
if isinstance(op['value'], int):
value = op['value']
if isinstance(op['value'], str):
value = getattr(character, op['value'])
return value
# Character model based on character_template.json
class Character(models.Model):
owner = models.ForeignKey('CustomUser', on_delete=models.CASCADE, related_name='characters')
name = models.CharField(max_length=100)
level = models.IntegerField(default=0)
race = models.ForeignKey('Package', related_name='race_characters', null=True, blank=True, on_delete=models.SET_NULL)
subrace = models.ForeignKey('Package', related_name='subrace_characters', null=True, blank=True, on_delete=models.SET_NULL)
char_class = models.ForeignKey('Package', related_name='class_characters', null=True, blank=True, on_delete=models.SET_NULL)
subclass = models.ForeignKey('Package', related_name='subclass_characters', null=True, blank=True, on_delete=models.SET_NULL)
background = models.ForeignKey('Package', related_name='background_characters', null=True, blank=True, on_delete=models.SET_NULL)
alignment = models.CharField(max_length=50, blank=True)
size = models.CharField(max_length=20, blank=True)
age = models.IntegerField(default=0)
gender = models.CharField(max_length=30, blank=True)
height = models.CharField(max_length=20, blank=True)
weight = models.IntegerField(default=0)
deity = models.CharField(max_length=100, blank=True)
# Relationships to packages and features
packages = models.ManyToManyField('Package', blank=True, related_name='characters')
features = models.ManyToManyField('Feature', blank=True, related_name='characters')
# Base and current stats
strength_base = models.IntegerField(default=0)
dexterity_base = models.IntegerField(default=0)
constitution_base = models.IntegerField(default=0)
intelligence_base = models.IntegerField(default=0)
wisdom_base = models.IntegerField(default=0)
charisma_base = models.IntegerField(default=0)
armor_base = models.IntegerField(default=0)
inspiration = models.BooleanField(default=False)
experience = models.IntegerField(default=0)
# Proficiencies and languages
proficiencies_armor = models.JSONField(default=list, blank=True)
proficiencies_weapons = models.JSONField(default=list, blank=True)
proficiencies_tools = models.JSONField(default=list, blank=True)
languages = models.JSONField(default=list, blank=True)
# Saving throws
strength_save_prof = models.BooleanField(default=False)
dexterity_save_prof = models.BooleanField(default=False)
constitution_save_prof = models.BooleanField(default=False)
intelligence_save_prof = models.BooleanField(default=False)
wisdom_save_prof = models.BooleanField(default=False)
charisma_save_prof = models.BooleanField(default=False)
# HP and combat
hp = models.IntegerField(default=0)
#hp_max = models.IntegerField(default=0)
hp_temp = models.IntegerField(default=0)
hit_die = models.CharField(max_length=20, blank=True)
#initiative = models.IntegerField(default=0)
# Death saves
deathsaves_successes = models.IntegerField(default=0)
deathsaves_failures = models.IntegerField(default=0)
# Resistances
fire_resistance = models.BooleanField(default=False)
poison_resistance = models.BooleanField(default=False)
psychic_resistance = models.BooleanField(default=False)
cold_resistance = models.BooleanField(default=False)
thunder_resistance = models.BooleanField(default=False)
acid_resistance = models.BooleanField(default=False)
force_resistance = models.BooleanField(default=False)
radiant_resistance = models.BooleanField(default=False)
necrotic_resistance = models.BooleanField(default=False)
bludgeoning_resistance = models.BooleanField(default=False)
piercing_resistance = models.BooleanField(default=False)
slashing_resistance = models.BooleanField(default=False)
immunities = models.JSONField(default=list, blank=True)
vulnerabilities = models.JSONField(default=list, blank=True)
# Movement & vision
speed_base = models.IntegerField(default=0)
speed_type = models.JSONField(default=list, blank=True)
darkvision = models.IntegerField(default=0)
blindsight = models.IntegerField(default=0)
tremorsense = models.IntegerField(default=0)
truesight = models.IntegerField(default=0)
# Spellcasting
spellcasting_ability = models.CharField(max_length=50, blank=True)
spell_save_dc = models.IntegerField(default=0)
spell_attack_bonus = models.IntegerField(default=0)
known_spells = models.JSONField(default=list, blank=True)
prepared_spells = models.JSONField(default=list, blank=True)
spell_slots = models.JSONField(default=dict, blank=True)
cantrips = models.JSONField(default=list, blank=True)
spellcasting_class = models.CharField(max_length=100, blank=True)
# Exhaustion, conditions, notes, and attributes
exhaustion_level = models.IntegerField(default=0)
conditions_active = models.JSONField(default=list, blank=True)
cp = models.IntegerField(default=0)
sp = models.IntegerField(default=0)
ep = models.IntegerField(default=0)
gp = models.IntegerField(default=0)
pp = models.IntegerField(default=0)
equipment = models.JSONField(default=list, blank=True)
traits = models.JSONField(default=list, blank=True)
personality_traits = models.JSONField(default=list, blank=True)
ideals = models.JSONField(default=list, blank=True)
bonds = models.JSONField(default=list, blank=True)
flaws = models.JSONField(default=list, blank=True)
notes = models.TextField(blank=True)
hp_features = models.JSONField(default=list, blank=True)
custom_attributes = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name} (Level {self.level})"
@classmethod
def from_db(cls, db, field_names, values):
instance = super().from_db(db, field_names, values)
# Here, do your consolidation logic:
instance.feature_operations = instance.build_feature_operations_dict()
return instance
def build_feature_operations_dict(self):
feature_dict = {}
#Build HP
for feature in self.hp_features:
feature_requirements = feature['feature_requirements']
if check_requirements(self, feature_requirements):
for operation in feature['feature_data'].get('operations', []):
if 'requirements' in operation:
if check_requirements(self, operation['requirements']):
feature_dict.setdefault(operation['attr'], []).append(operation)
else:
feature_dict.setdefault(operation['attr'], []).append(operation)
#Build features
for feature in self.features.all():
feature_requirements = feature.feature_requirements
if check_requirements(self, feature_requirements):
for operation in feature.feature_data.get('operations', []):
if 'requirements' in operation:
if check_requirements(self, operation['requirements']):
feature_dict.setdefault(operation['attr'], []).append(operation)
else:
feature_dict.setdefault(operation['attr'], []).append(operation)
print(self.packages.all())
# Build race
if self.race != None:
for feature in self.race.features.all():
feature_requirements = feature.feature_requirements
if check_requirements(self, feature_requirements):
for operation in feature.feature_data.get('operations', []):
if 'requirements' in operation:
if check_requirements(self, operation['requirements']):
feature_dict.setdefault(operation['attr'], []).append(operation)
else:
feature_dict.setdefault(operation['attr'], []).append(operation)
for feature in feature_dict:
print(feature)
return feature_dict
def stat_total(self, attr):
base = getattr(self, f"{attr}_base")
ops = self.feature_operations.get(attr, [])
return apply_operations(base, ops, self)
@property
def proficiency(self):
lvl = getattr(self, 'level')
return 2 + (lvl-1) // 4
@property
def strength(self):
return self.stat_total('strength')
@property
def strength_modifier(self):
return (getattr(self, 'strength')-10) // 2
@property
def strength_save(self):
sav = getattr(self, 'strength_modifier')
if getattr(self, 'strength_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def dexterity(self):
return self.stat_total('dexterity')
@property
def dexterity_modifier(self):
return (getattr(self, 'dexterity')-10) //2
@property
def dexterity_save(self):
sav = getattr(self, 'dexterity_modifier')
if getattr(self, 'dexterity_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def constitution(self):
return self.stat_total('constitution')
@property
def constitution_modifier(self):
return (getattr(self, 'constitution')-10) //2
@property
def constitution_save(self):
sav = getattr(self, 'constitution_modifier')
if getattr(self, 'constitution_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def intelligence(self):
return self.stat_total('intelligence')
@property
def intelligence_modifier(self):
return (getattr(self, 'intelligence')-10) //2
@property
def intelligence_save(self):
sav = getattr(self, 'intelligence_modifier')
if getattr(self, 'intelligence_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def wisdom(self):
return self.stat_total('wisdom')
@property
def wisdom_modifier(self):
return (getattr(self, 'wisdom')-10) //2
@property
def wisdom_save(self):
sav = getattr(self, 'wisdom_modifier')
if getattr(self, 'wisdom_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def charisma(self):
return self.stat_total('charisma')
@property
def charisma_modifier(self):
return (getattr(self, 'charisma')-10) //2
@property
def charisma_save(self):
sav = getattr(self, 'charisma_modifier')
if getattr(self, 'charisma_save_prof'):
return getattr(self, 'proficiency') + sav
return sav
@property
def armor_class(self):
return self.stat_total('armor_class')
@property
def hp_max(self):
con = getattr(self, 'constitution_modifier')
level = getattr(self, 'level')
base = con * level
ops = self.feature_operations.get('hp_max', [])
print(ops, base)
bonus = apply_operations(base, ops, self)
return bonus
@property
def initiative(self):
return 0
@property
def athletics(self):
ops = get_operations_for_attr(self, 'athletics')
base = getattr(self, 'strength_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus + getattr(self, 'proficiency')
@property
def athletics_passive(self):
return getattr(self, 'athletics') + 10
@property
def acrobatics(self):
ops = get_operations_for_attr(self, 'acrobatics')
base = getattr(self, 'dexterity_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def acrobatics_passive(self):
return getattr(self, 'acrobatics') + 10
@property
def sleight_of_hand(self):
ops = get_operations_for_attr(self, 'sleight_of_hand')
base = getattr(self, 'dexterity_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def sleight_of_hand_passive(self):
return getattr(self, 'sleight_of_hand') + 10
@property
def stealth(self):
ops = get_operations_for_attr(self, 'stealth')
base = getattr(self, 'dexterity_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def stealth_passive(self):
return getattr(self, 'stealth') + 10
@property
def arcana(self):
ops = get_operations_for_attr(self, 'arcana')
base = getattr(self, 'intelligence_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def arcana_passive(self):
return getattr(self, 'arcana') + 10
@property
def history(self):
ops = get_operations_for_attr(self, 'history')
base = getattr(self, 'intelligence_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def history_passive(self):
return getattr(self, 'history') + 10
@property
def investigation(self):
ops = get_operations_for_attr(self, 'investigation')
base = getattr(self, 'intelligence_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def investigation_passive(self):
return getattr(self, 'investigation') + 10
@property
def nature(self):
ops = get_operations_for_attr(self, 'nature')
base = getattr(self, 'intelligence_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def nature_passive(self):
return getattr(self, 'nature') + 10
@property
def religion(self):
ops = get_operations_for_attr(self, 'religion')
base = getattr(self, 'intelligence_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def religion_passive(self):
return getattr(self, 'religion') + 10
@property
def animal_handling(self):
ops = get_operations_for_attr(self, 'animal_handling')
base = getattr(self, 'wisdom_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def animal_handling_passive(self):
return getattr(self, 'animal_handling') + 10
@property
def insight(self):
ops = get_operations_for_attr(self, 'insight')
base = getattr(self, 'wisdom_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def insight_passive(self):
return getattr(self, 'insight') + 10
@property
def medicine(self):
ops = get_operations_for_attr(self, 'medicine')
base = getattr(self, 'wisdom_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def medicine_passive(self):
return getattr(self, 'medicine') + 10
@property
def perception(self):
ops = get_operations_for_attr(self, 'perception')
base = getattr(self, 'wisdom_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def perception_passive(self):
return getattr(self, 'perception') + 10
@property
def survival(self):
ops = get_operations_for_attr(self, 'survival')
base = getattr(self, 'wisdom_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def survival_passive(self):
return getattr(self, 'survival') + 10
@property
def deception(self):
ops = get_operations_for_attr(self, 'deception')
base = getattr(self, 'charisma_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def deception_passive(self):
return getattr(self, 'deception') + 10
@property
def intimidation(self):
ops = get_operations_for_attr(self, 'intimidation')
base = getattr(self, 'charisma_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def intimidation_passive(self):
return getattr(self, 'intimidation') + 10
@property
def performance(self):
ops = get_operations_for_attr(self, 'performance')
base = getattr(self, 'charisma_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def performance_passive(self):
return getattr(self, 'performance') + 10
@property
def persuasion(self):
ops = get_operations_for_attr(self, 'persuasion')
base = getattr(self, 'charisma_modifier')
bonus = apply_operations(0, ops, self)
return base + bonus
@property
def persuasion_passive(self):
return getattr(self, 'persuasion') + 10
# Feature model based on feature_template.json
class Feature(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
feature_name = models.CharField(max_length=200)
feature_description = models.TextField(blank=True)
feature_requirements = models.JSONField(default=list, blank=True)
#feature requirements requires 3 keys, {"property", "value", "condition"}
feature_data = models.JSONField(default=dict, blank=True)
#feature_data holds a value of {"operations", "sources"}
# ----> operations is a list of dictionaries that have to have 6 values
# ----> {"attr", "value", "operation", "limits", "operation_requirements", "priority"}
def __str__(self):
return self.feature_name
# Package <-> Feature through model for priorities
class PackageFeature(models.Model):
package = models.ForeignKey('Package', on_delete=models.CASCADE)
feature = models.ForeignKey('Feature', on_delete=models.CASCADE)
priority = models.IntegerField(default=0)
requirements_override = models.JSONField(blank=True, null=True)
class Meta:
unique_together = ('package', 'feature')
ordering = ['priority']
def __str__(self):
return f"{self.package} - {self.feature} (priority {self.priority})"
# Package model based on package_template.json
class Package(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
package_name = models.CharField(max_length=200)
package_description = models.TextField(blank=True)
package_type = models.CharField(max_length=100, blank=True)
package_doc_md = models.TextField(blank=True)
features = models.ManyToManyField('Feature', through='PackageFeature', blank=True, related_name='packages')
def __str__(self):
return self.package_name
# Create your models here.
class Pin(models.Model):
label = models.CharField(max_length=100)
url = models.URLField(max_length=300)
x = models.FloatField(help_text="X position as percentage (0-100)")
y = models.FloatField(help_text="Y position as percentage (0-100)")
pin_type = models.CharField(max_length=100, default="general")
def as_dict(self):
return {
"label": self.label,
"url": self.url,
"x": self.x,
"y": self.y,
"pin_type": self.pin_type,
}
def __str__(self):
return f"{self.label} ({self.x:.2f}%, {self.y:.2f}%)"

7
main/serializers.py Normal file
View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import Pin
class PinSerializer(serializers.ModelSerializer):
class Meta:
model = Pin
fields = ['id', 'label', 'url', 'x', 'y', 'pin_type']

View File

@ -0,0 +1,290 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>D&D 5e Full Character Sheet</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
<style>
body { background: #f4f4f4; }
.sheet {
max-width: 980px;
margin: 2rem auto;
}
.uk-card {
margin-bottom: 1.5rem;
overflow: visible;
}
.uk-table th, .uk-table td { text-align: center; }
.dice { font-family:monospace; }
.uk-label { margin-right: 0.2em; }
.quickfacts label { font-weight: bold; margin-right: 0.25em; }
.info-table-card { overflow-x: auto; }
.uk-table { margin-bottom: 0; }
.uk-table th, .uk-table td { text-align: center; }
.dice { font-family:monospace; }
.uk-label { margin-right: 0.2em; }
.quickfacts label { font-weight: bold; margin-right: 0.25em; }
.info-table-card { overflow-x: auto; }
.uk-table { margin-bottom: 0; }
</style>
</head>
<body>
<div class="sheet">
<!-- Main Header -->
<div class="uk-card uk-card-default uk-card-body">
<h1 class="uk-text-center uk-margin-remove-bottom">
<span uk-icon="users"></span> {{ character.name }}
<span class="uk-text-meta uk-margin-small-left">Level {{ character.level }}</span>
</h1>
<hr class="uk-divider-icon uk-margin-small-top uk-margin-bottom">
<div class="uk-grid-small uk-child-width-1-2@s uk-child-width-1-4@m" uk-grid>
<div><label>Race:</label> <span class="uk-badge uk-margin-small-right">{{ character.race }}</span> {{ character.subrace }}</div>
<div><label>Class:</label> {{ character.char_class.package_name }} {{ character.subclass }}</div>
<div><label>Background:</label> {{ character.background }}</div>
<div><label>Alignment:</label> {{ character.alignment }}</div>
<div><label>Size:</label> {{ character.size }}</div>
<div><label>Age:</label> {{ character.age }}</div>
<div><label>Gender:</label> {{ character.gender }}</div>
<div><label>Height:</label> {{ character.height }}</div>
<div><label>Weight:</label> {{ character.weight }} lbs</div>
<div><label>Deity:</label> {{ character.deity }}</div>
<div><label>XP:</label> {{ character.experience }}</div>
<div>
<label>Inspiration:</label>
{% if character.inspiration %}
<span class="uk-label uk-label-warning" uk-icon="star"> {{ character.inspiration }}</span>
{% else %}
<span class="uk-text-muted"></span>
{% endif %}
</div>
</div>
</div>
<!-- Core Attrs: Full-width Card to prevent table overflows -->
<div class="uk-card uk-card-default uk-card-body info-table-card">
<h3 class="uk-card-title"><span uk-icon="settings"></span> Core Attributes</h3>
<div style="overflow-x:auto;">
<table class="uk-table uk-table-divider uk-table-small uk-table-striped uk-table-hover">
<thead>
<tr><th></th><th>Score</th><th>Modifier</th><th>Save</th></tr>
</thead>
<tbody>
<tr><th>STR</th><td>{{ character.strength }}</td><td>{{ character.strength_modifier }}</td><td>{{ character.strength_save }}</td></tr>
<tr><th>DEX</th><td>{{ character.dexterity }}</td><td>{{ character.dexterity_modifier }}</td><td>{{ character.dexterity_save }}</td></tr>
<tr><th>CON</th><td>{{ character.constitution }}</td><td>{{ character.constitution_modifier }}</td><td>{{ character.constitution_save }}</td></tr>
<tr><th>INT</th><td>{{ character.intelligence }}</td><td>{{ character.intelligence_modifier }}</td><td>{{ character.intelligence_save }}</td></tr>
<tr><th>WIS</th><td>{{ character.wisdom }}</td><td>{{ character.wisdom_modifier }}</td><td>{{ character.wisdom_save }}</td></tr>
<tr><th>CHA</th><td>{{ character.charisma }}</td><td>{{ character.charisma_modifier }}</td><td>{{ character.charisma_save }}</td></tr>
<tr><th>Armor</th><td>{{ character.armor_base }}</td><td>-</td><td>-</td></tr>
</tbody>
</table>
</div>
<div class="uk-margin uk-child-width-1-3@m" uk-grid>
<div><span class="uk-label">Proficiency</span> {{ character.proficiency }}</div>
<div><span class="uk-label">Initiative</span> {{ character.initiative }}</div>
<div><span class="uk-label">Speed</span> {{ character.speed_base }} {% if character.speed_type %}<span class="uk-text-meta">({{ character.speed_type|join:', ' }})</span>{% endif %}</div>
</div>
</div>
<!-- Secondary Info Cards Grouped in a Responsive Grid -->
<div class="uk-grid-match uk-child-width-1-2@s uk-child-width-1-3@m uk-margin-bottom" uk-grid>
<div>
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<h3 class="uk-card-title"><span uk-icon="lifesaver"></span> HP & Status</h3>
<ul class="uk-list uk-list-small">
<li><span class="uk-label uk-label-danger">HP</span> {{ character.hp }} / {{ character.hp_max }} {% if character.hp_temp %}(+{{ character.hp_temp }} temp){% endif %}</li>
<li><span class="uk-label">Hit Die</span> <span class="dice">{{ character.hit_die }}</span></li>
<li><span class="uk-label">Death Saves</span> {{ character.deathsaves_successes }}&#10003;/{{ character.deathsaves_failures }}&#10007;</li>
<li><span class="uk-label">Exhaustion</span> {% if character.exhaustion_level > 0 %}<span class="uk-label uk-label-warning">{{ character.exhaustion_level }}</span>{% else %}<span class="uk-text-muted">0</span>{% endif %}</li>
<li><span class="uk-label">Active Conditions</span> {% for cond in character.conditions_active %}{{ cond }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
</ul>
</div>
</div>
<div>
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<h3 class="uk-card-title"><span uk-icon="heart"></span> Resistances & Immunities</h3>
<ul class="uk-list uk-list-striped uk-list-small">
<li><b>Immunities:</b> {% for i in character.immunities %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
<li><b>Vulnerabilities:</b> {% for v in character.vulnerabilities %}{{ v }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
<li><b>Resistances:</b>
{% if character.fire_resistance %}Fire {% endif %}
{% if character.poison_resistance %}Poison {% endif %}
{% if character.psychic_resistance %}Psychic {% endif %}
{% if character.cold_resistance %}Cold {% endif %}
{% if character.thunder_resistance %}Thunder {% endif %}
{% if character.acid_resistance %}Acid {% endif %}
{% if character.force_resistance %}Force {% endif %}
{% if character.radiant_resistance %}Radiant {% endif %}
{% if character.necrotic_resistance %}Necrotic {% endif %}
{% if character.bludgeoning_resistance %}Bludgeoning {% endif %}
{% if character.piercing_resistance %}Piercing {% endif %}
{% if character.slashing_resistance %}Slashing {% endif %}
{% if not character.fire_resistance and not character.poison_resistance and not character.psychic_resistance and not character.cold_resistance and not character.thunder_resistance and not character.acid_resistance and not character.force_resistance and not character.radiant_resistance and not character.necrotic_resistance and not character.bludgeoning_resistance and not character.piercing_resistance and not character.slashing_resistance %}
<span class="uk-text-muted">None</span>
{% endif %}
</li>
</ul>
</div>
</div>
<div>
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<h3 class="uk-card-title"><span uk-icon="search"></span> Senses</h3>
<ul class="uk-list uk-list-bullet uk-list-small">
<li>Darkvision: <span class="uk-badge">{{ character.darkvision }}</span></li>
<li>Blindsight: <span class="uk-badge">{{ character.blindsight }}</span></li>
<li>Tremorsense: <span class="uk-badge">{{ character.tremorsense }}</span></li>
<li>Truesight: <span class="uk-badge">{{ character.truesight }}</span></li>
</ul>
</div>
</div>
<div>
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<h3 class="uk-card-title"><span uk-icon="future"></span> Proficiencies & Languages</h3>
<ul class="uk-list uk-list-bullet uk-list-small">
<li><span class="uk-label">Armor:</span> {% for i in character.proficiencies_armor %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
<li><span class="uk-label">Weapons:</span> {% for i in character.proficiencies_weapons %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
<li><span class="uk-label">Tools:</span> {% for i in character.proficiencies_tools %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
<li><span class="uk-label">Languages:</span> {% for i in character.languages %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
</ul>
</div>
</div>
</div>
<!-- Main Section Tabs With Switchers -->
<div class="uk-card uk-card-default uk-card-body">
<ul class="uk-tab uk-child-width-expand uk-margin-remove-bottom" uk-tab>
<li class="uk-active"><a href="#">Skills</a></li>
<li><a href="#">Spellcasting</a></li>
<li><a href="#">Equipment & Wealth</a></li>
<li><a href="#">Features, Traits & Backstory</a></li>
</ul>
<ul class="uk-switcher uk-margin">
<li>
<h3><span uk-icon="bolt"></span> Skills</h3>
<div style="overflow-x:auto;">
<table class="uk-table uk-table-small uk-table-hover uk-table-divider">
<tr><th>Skill</th><th>Bonus</th><th>Passive</th></tr>
<tr><td>Athletics</td><td>{{ character.athletics }}</td><td>{{ character.athletics_passive }}</td></tr>
<tr><td>Acrobatics</td><td>{{ character.acrobatics }}</td><td>{{ character.acrobatics_passive }}</td></tr>
<tr><td>Sleight of Hand</td><td>{{ character.sleight_of_hand }}</td><td>{{ character.sleight_of_hand_passive }}</td></tr>
<tr><td>Stealth</td><td>{{ character.stealth }}</td><td>{{ character.stealth_passive }}</td></tr>
<tr><td>Arcana</td><td>{{ character.arcana }}</td><td>{{ character.arcana_passive }}</td></tr>
<tr><td>History</td><td>{{ character.history }}</td><td>{{ character.history_passive }}</td></tr>
<tr><td>Investigation</td><td>{{ character.investigation }}</td><td>{{ character.investigation_passive }}</td></tr>
<tr><td>Nature</td><td>{{ character.nature }}</td><td>{{ character.nature_passive }}</td></tr>
<tr><td>Religion</td><td>{{ character.religion }}</td><td>{{ character.religion_passive }}</td></tr>
<tr><td>Animal Handling</td><td>{{ character.animal_handling }}</td><td>{{ character.animal_handling_passive }}</td></tr>
<tr><td>Insight</td><td>{{ character.insight }}</td><td>{{ character.insight_passive }}</td></tr>
<tr><td>Medicine</td><td>{{ character.medicine }}</td><td>{{ character.medicine_passive }}</td></tr>
<tr><td>Perception</td><td>{{ character.perception }}</td><td>{{ character.perception_passive }}</td></tr>
<tr><td>Survival</td><td>{{ character.survival }}</td><td>{{ character.survival_passive }}</td></tr>
<tr><td>Deception</td><td>{{ character.deception }}</td><td>{{ character.deception_passive }}</td></tr>
<tr><td>Intimidation</td><td>{{ character.intimidation }}</td><td>{{ character.intimidation_passive }}</td></tr>
<tr><td>Performance</td><td>{{ character.performance }}</td><td>{{ character.performance_passive }}</td></tr>
<tr><td>Persuasion</td><td>{{ character.persuasion }}</td><td>{{ character.persuasion_passive }}</td></tr>
</table>
</div>
<div class="uk-child-width-1-3@m uk-margin" uk-grid>
<div><span class="uk-label">Passive Perception</span> {{ character.passive_perception }}</div>
<div><span class="uk-label">Passive Investigation</span> {{ character.passive_investigation }}</div>
<div><span class="uk-label">Passive Insight</span> {{ character.passive_insight }}</div>
</div>
</li>
<li>
<h3><span uk-icon="nut"></span> Spellcasting</h3>
<div class="uk-child-width-1-2@m" uk-grid>
<div>
<span class="uk-label">Spellcasting Class:</span> {{ character.spellcasting_class }}<br>
<span class="uk-label">Ability:</span> {{ character.spellcasting_ability }}<br>
<span class="uk-label">Spell Save DC:</span> {{ character.spell_save_dc }}<br>
<span class="uk-label">Spell Attack Bonus:</span> {{ character.spell_attack_bonus }}
</div>
<div>
<span class="uk-label">Spell Slots:</span><br>
{% for lvl, slots in character.spell_slots.items %}Level {{ lvl }}: {{ slots }}{% if not forloop.last %} | {% endif %}{% empty %}None{% endfor %}<br>
<span class="uk-label">Cantrips:</span> {% for spell in character.cantrips %}{{ spell }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
</div>
</div>
<hr>
<div class="uk-child-width-1-2@m" uk-grid>
<div>
<label>Known Spells:</label>
<ul class="uk-list uk-list-collapse">{% for spell in character.known_spells %}<li>{{ spell }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
</div>
<div>
<label>Prepared Spells:</label>
<ul class="uk-list uk-list-collapse">{% for spell in character.prepared_spells %}<li>{{ spell }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
</div>
</div>
</li>
<li>
<h3><span uk-icon="bag"></span> Equipment & Wealth</h3>
<div class="uk-child-width-1-2@m" uk-grid>
<div>
<span class="uk-label uk-label-warning">CP</span> {{ character.cp }}
<span class="uk-label uk-label-secondary">SP</span> {{ character.sp }}
<span class="uk-label uk-label-success">EP</span> {{ character.ep }}
<span class="uk-label uk-label-danger">GP</span> {{ character.gp }}
<span class="uk-label uk-label-primary">PP</span> {{ character.pp }}
</div>
<div>
<span class="uk-label">Equipment:</span>
<ul class="uk-list uk-list-bullet">{% for eq in character.equipment %}<li>{{ eq }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
</div>
</div>
</li>
<li>
<h3><span uk-icon="info"></span> Features, Traits & Backstory</h3>
<div class="uk-grid uk-child-width-1-2@m" uk-grid>
<div>
<span class="uk-label">Traits:</span>
<ul class="uk-list uk-list-collapse">{% for val in character.traits %}<li>{{ val }}</li>{% endfor %}</ul>
<span class="uk-label">Personality Traits:</span>
<ul class="uk-list uk-list-collapse">{% for val in character.personality_traits %}<li>{{ val }}</li>{% endfor %}</ul>
</div>
<div>
<span class="uk-label">Ideals:</span>
<ul class="uk-list uk-list-collapse">{% for val in character.ideals %}<li>{{ val }}</li>{% endfor %}</ul>
<span class="uk-label">Bonds:</span>
<ul class="uk-list uk-list-collapse">{% for val in character.bonds %}<li>{{ val }}</li>{% endfor %}</ul>
<span class="uk-label">Flaws:</span>
<ul class="uk-list uk-list-collapse">{% for val in character.flaws %}<li>{{ val }}</li>{% endfor %}</ul>
</div>
</div>
<div><span class="uk-label">Notes:</span><br>{{ character.notes }}</div>
<div class="uk-margin-small-top"><span class="uk-label">Custom Attributes:</span> {% for k,v in character.custom_attributes.items %}{{ k }}: {{ v }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
</div>
</li>
</ul>
</div>
<div class="uk-card uk-card-default uk-card-body">
<ul class="uk-tab uk-child-width-expand uk-margin-remove-bottom" uk-tab>
<li class="uk-active"><a href="#">Class Features</a></li>
<li class="uk-active"><a href="#">Background Features</a></li>
<li class="uk-active"><a href="#">Race Features</a></li>
<li class="uk-active"><a href="#">Features</a></li>
</ul>
<ul class="uk-switcher uk-margin">
<li>
<div uk-grid>
<div class="uk-width-1-1">
<ul class="uk-accordion-default" uk-accordion="multiple: true">
{% for val in character.char_class.display_features.all %}
{% if val.priority <= character.level %}
<li>
<a class="uk-accordion-title" href>{{ val.feature.feature_name }} (Level {{val.priority}})</a>
<div class="uk-accordion-content">
<p>{{ val.feature.feature_description|safe }}</p>
</div>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
</li>
</ul>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,230 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>Features</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
</head>
<body>
<div class="uk-container">
<div uk-grid>
<div class="uk-width-1-1">
<h1 class="uk-heading-line uk-text-center"><span>Add a Feature</span></h1>
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="feature_name">Name</label>
<div class="uk-form-controls">
<input class="uk-input" id="feature_name" name="feature_name" type="text" required>
</div>
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="feature_description">Description</label>
<div class="uk-form-controls">
<textarea class="uk-textarea" id="feature_description" name="feature_description" rows="3"></textarea>
</div>
</div>
<div class="uk-width-1-1">
<ul uk-tab>
<li><a href="#">Operations</a></li>
<li><a href="#">Requirements</a></li>
</ul>
<div class="uk-switcher">
<div uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-muted">By adding an operation to your feature you can have the feature modifier and manipulate
a characters properties at runtime.
</p>
</div>
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-primary uk-align-right" onclick="addOperation()"><span uk-icon="icon: plus"></span> Add Operation</button>
</div>
<div id="operations-body" class="uk-width-1-1" uk-grid>
<div class="uk-width-1-1">
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<h3 class="uk-card-title">Small</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<div class="uk-padding uk-padding-remove-top uk-padding-remove-bottom" uk-grid>
<input class="uk-width-1-4 uk-input" name="operation_attr_${idx}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
<select class="uk-width-1-4 uk-select" name="operation_operation_${idx}">
<option value="add" ${op==='add'?'selected':''}>Add</option>
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
<option value="set" ${op==='set'?'selected':''}>Set</option>
</select>
<input class="uk-width-1-4 uk-input" name="operation_value_${idx}" type="text" placeholder="Value or Attribute..." value="${value}">
<button type='button' class='uk-width-1-4 uk-button uk-button-danger uk-button-small' onclick='this.closest(".uk-card").remove()'><span uk-icon="icon: minus"></span></button>
</div>
<div uk-grid>
<div class='uk-margin-small-top' id='operation-limit-list-${idx}'>
</div>
<button type='button' class='uk-button uk-button-secondary uk-button-small' onclick='addLimitField(${idx})'><span uk-icon="icon: plus"></span> Add Limit/Subpart</button>
<div class='uk-margin-small-top' id='operation-req-list-${idx}'></div>
<button type='button' class='uk-button uk-button-primary uk-button-small uk-margin-small-top' onclick='addOperationRequirementField(${idx})'><span uk-icon="icon: plus"></span> Add Requirement</button>
</div>
</div>
</div>
</div>
</div>
<div uk-grid>
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-primary " onclick="addRequirement()"><span uk-icon="icon: plus"></span> Add Requirement</button>
<div id="requirements-list"></div>
</div>
</div>
</div>
</div>
<div class="uk-width-1-1">
<button type="submit" class="uk-button uk-button-primary uk-width-1-1">Submit Feature</button>
</div>
</div>
</div>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script>
// --- DYNAMIC REQUIREMENTS ---
function addRequirement(prop='', cond='', val='') {
const reqList = document.getElementById('requirements-list');
const idx = reqList.children.length;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '')
el.innerHTML = `
<input class="uk-input" name="requirement_property_${idx}" type="text" placeholder="Property" value="${prop}">
<select class="uk-select" name="requirement_condition_${idx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="requirement_value_${idx}" type="text" placeholder="Value" value="${val}">
<button type="button" class="uk-button uk-button-danger uk-button-small" onclick="this.parentElement.remove()"><span uk-icon="icon: minus"></span></button>
`;
reqList.appendChild(el);
}
// --- DYNAMIC OPERATIONS ---
function addOperation(attr='', op='', value='', extra=[]) {
const operationsBody = document.getElementById('operations-body');
const op_index = operationsBody.children.length;
const main_div = document.createElement('div');
main_div.setAttribute('class', 'uk-width-1-1')
//const el = document.createElement('tr');
main_div.innerHTML = `
<div class="uk-card uk-card-small uk-card-default uk-card-body">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_attr_${op_index}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
</div>
<div class="uk-width-1-4">
<select class="uk-select" name="operation_operation_${op_index}">
<option value="add" ${op==='add'?'selected':''}>Add</option>
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
<option value="set" ${op==='set'?'selected':''}>Set</option>
</select>
</div>
<div class="uk-width-1-4">
<input class="uk-input" name="operation_value_${op_index}" type="text" placeholder="Value or Attribute..." value="${value}">
</div>
<div class="uk-width-1-4">
<button type="button" class="uk-button uk-button-danger" onclick="this.closest(".uk-card").remove()"><span uk-icon="icon: trash"></span></button>
</div>
</div>
<div uk-grid>
<div class="uk-width-1-1">
<ul class="uk-accordion-default" uk-accordion>
<li>
<a class="uk-accordion-title" href>Operation Limits</a>
<div class="uk-accordion-content" uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-meta">Add a limit to when this operation is applied to a character.</p>
</div>
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-secondary" onclick="addLimitField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Limit</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-limit-list-${op_index}"></div>
</div>
</li>
<li>
<a class="uk-accordion-title" href>Operation Requirements</a>
<div class="uk-accordion-content">
<div class="uk-width-1-1">
<button type="button" class="uk-button uk-button-small uk-button-secondary" onclick="addOperationRequirementField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Requirement</button>
</div>
<div class="uk-width-1-1 uk-margin-small-top" id="operation-req-list-${op_index}"></div>
</div>
</li>
</ul>
</div>
</div>
</div>`
operationsBody.appendChild(main_div);
// add pre-existing subparts if any (extra)
if (extra && Array.isArray(extra)) {
extra.forEach(kv => addLimitField(op_index, kv.key, kv.value));
}
}
function addLimitField(opIdx, k='', v='', t='') {
const list = document.getElementById('operation-limit-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<label class="uk-label">Limit</label>
<input class="uk-input" name="operation_${opIdx}_limitkey_${subIdx}" type="text" placeholder="Extra Key (e.g., limit, min, max)" value="${k||''}">
<select class="uk-select" name="operation_${opIdx}_limittype_${subIdx}">
<option value="max" ${t==='max'?'selected':''}>max</option>
<option value="min" ${t==='min'?'selected':''}>min</option>
<option value="equals" ${t==='equals'?'selected':''}>equal</option>
</select>
<input class="uk-input" name="operation_${opIdx}_limitval_${subIdx}" type="text" placeholder="Value" value="${v||''}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
// --- DYNAMIC OPERATION REQUIREMENTS ---
function addOperationRequirementField(opIdx, prop='', cond='', val='') {
const list = document.getElementById('operation-req-list-' + opIdx);
const subIdx = (list) ? list.children.length : 0;
const el = document.createElement('div');
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
el.setAttribute('uk-grid', '');
el.innerHTML = `
<input class="uk-input" name="operation_${opIdx}_req_property_${subIdx}" type="text" placeholder="Requirement Property" value="${prop}">
<select class="uk-select" name="operation_${opIdx}_req_condition_${subIdx}">
<option value="==" ${cond=='==' ? 'selected' : ''}>&equals;</option>
<option value=">=" ${cond=='>=' ? 'selected' : ''}>&ge;</option>
<option value="<=" ${cond=='<=' ? 'selected' : ''}>&le;</option>
<option value="!=" ${cond=='!=' ? 'selected' : ''}>&ne;</option>
</select>
<input class="uk-input" name="operation_${opIdx}_req_value_${subIdx}" type="text" placeholder="Value" value="${val}">
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
`;
list.appendChild(el);
}
// Prevent accidental form submit via Enter in addable fields
// (recommended for dynamic forms)
document.addEventListener('keydown', function(e) {
if(e.key==="Enter" && e.target.tagName==="INPUT") {
// do nothing if inside textarea
if (!(e.target.closest('form') && e.target.closest('form').id === 'feature-form')) return;
e.preventDefault(); return false;
}
});
</script>
</body>
</html>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Lisium Django Demo</title>
</head>
<body>
<h1>Welcome to Django</h1>
<p>
This is being rendered from a django template.
</p>
</body>
</html>

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
{% load static %}
{% csrf_token %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pin Map</title>
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
<link rel="stylesheet" href="{% static 'css/map.css' %}">
<style>
/* Quick fix if static is not configured: place CSS here if needed */
</style>
</head>
<body>
<div class="uk-container">
<h2>Interactive Map with Pins</h2>
<div id="map-container" class="custom-map">
<div id="map-viewport">
<img src="{% static 'map.png' %}" id="main-map" alt="Map" draggable="false"/>
<div id="pins"></div>
</div>
</div>
</div>
<div class="uk-container uk-margin-large-top">
<h4 class="uk-heading-divider">All Pins</h4>
<div class="uk-overflow-auto">
<table id="pins-table" class="uk-table uk-table-divider uk-table-hover uk-table-small uk-table-middle">
<thead>
<tr>
<th>#</th>
<th>Label</th>
<th>URL</th>
<th>X (%)</th>
<th>Y (%)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Rows injected by JS -->
</tbody>
</table>
</div>
</div>
<div id="add-pin-modal" uk-modal>
<div class="uk-modal-dialog uk-modal-body uk-border-rounded">
<button class="uk-modal-close-default" type="button" uk-close></button>
<h3 class="uk-modal-title uk-flex uk-flex-middle">
<span uk-icon="icon: location; ratio: 1.2" class="uk-margin-small-right"></span>
Add a Map Pin
</h3>
<form id="add-pin-form" class="uk-form-stacked">
<div class="uk-margin">
<label class="uk-form-label" for="pin-label">Label</label>
<div class="uk-form-controls">
<input class="uk-input uk-border-pill" id="pin-label" type="text" placeholder="e.g. Castle Gates" required>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="pin-url">Link URL</label>
<div class="uk-form-controls">
<input class="uk-input uk-border-pill" id="pin-url" type="text" placeholder="/castle/" required>
</div>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="pin-url">Pin Type</label>
<div class="uk-form-controls">
<input class="uk-input uk-border-pill" id="pin-type" type="text" placeholder="/castle/" required>
</div>
</div>
<input type="hidden" id="pin-x">
<input type="hidden" id="pin-y">
<div class="uk-flex uk-flex-between uk-margin-top">
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
<button class="uk-button uk-button-primary uk-border-pill" type="submit">
<span uk-icon="plus"></span> Add Pin
</button>
</div>
</form>
</div>
</div>
</body>
<script src="{% static 'js/uikit.min.js' %}"></script>
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
<script src="{% static 'js/map.js' %}"></script>
</html>

View File

@ -2,5 +2,8 @@ from django.urls import path
from . import views
urlpatterns = [
path('', views.home, name='Home')
path('', views.home, name='Home'),
path('feature', views.feature_add_view, name="feature_add"),
path('map', views.map_page, name="Map"),
path('api/pins/', views.pin_list, name='pin_list'),
]

View File

@ -1,6 +1,106 @@
from django.shortcuts import render
from django.http import HttpResponse
from django.apps import apps
from .models import Character, PackageFeature, Feature, Pin
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Pin
from .serializers import PinSerializer
import json
# Create your views here.
def home(request):
return render(request, 'main/home.html')
with open('test_character.json', 'r+') as file:
character = json.load(file)
character = Character.objects.get(name="Gerom")
class_feature_links = (
PackageFeature.objects
.filter(package=character.char_class)
.select_related('feature')
.order_by('priority')
)
character.char_class.display_features = class_feature_links
return render(request, 'main/character_sheet.html', {'character': character, })
def feature_add_view(request):
if request.method == 'GET':
return render(request, 'main/feature.html')
elif request.method == 'POST':
# Parse basic fields
name = request.POST.get('feature_name')
description = request.POST.get('feature_description')
# Parse Requirements
requirements = []
req_keys = [k for k in request.POST.keys() if k.startswith('requirement_')]
idxs = set()
for k in req_keys:
try:
idxs.add(int(k.split('_')[-1]))
except:
continue
for idx in sorted(list(idxs)):
prop = request.POST.get(f'requirement_property_{idx}', '').strip()
cond = request.POST.get(f'requirement_condition_{idx}', '').strip()
val = request.POST.get(f'requirement_value_{idx}', '').strip()
if prop and cond and val:
requirements.append({'property': prop, 'condition': cond, 'value': val})
# Parse Operations (with extra subparts)
operations = []
op_keys = [k for k in request.POST.keys() if k.startswith('operation_attr_')]
op_idxs = [int(k.replace('operation_attr_', '')) for k in op_keys]
for op_idx in op_idxs:
attr = request.POST.get(f'operation_attr_{op_idx}', '').strip()
op = request.POST.get(f'operation_operation_{op_idx}', '').strip()
value = request.POST.get(f'operation_value_{op_idx}', '').strip()
# Subparts/limits
subparts = []
sub_idx = 0
while True:
kfield = f'operation_{op_idx}_limitkey_{sub_idx}'
vfield = f'operation_{op_idx}_limitval_{sub_idx}'
if kfield in request.POST and vfield in request.POST:
k = request.POST.get(kfield, '').strip()
v = request.POST.get(vfield, '').strip()
if k and v:
subparts.append({'key': k, 'value': v})
sub_idx += 1
else:
break
operation = {'attr': attr, 'operation': op, 'value': value}
for part in subparts:
operation[part['key']] = part['value']
operations.append(operation)
feat = Feature.objects.create(
feature_name=name,
feature_description=description,
feature_requirements=requirements,
feature_data={'operations': operations}
)
return render(request, 'main/feature_add.html', {
'msg': f'Feature "{feat.feature_name}" added successfully!',
})
def map_page(request):
return render(request, "main/map.html")
@api_view(['GET', 'POST'])
def pin_list(request):
if request.method == 'GET':
pins = Pin.objects.all()
serializer = PinSerializer(pins, many=True)
return Response(serializer.data)
if request.method == 'POST':
serializer = PinSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

7
package_template.json Normal file
View File

@ -0,0 +1,7 @@
{
"package_name": "",
"package_description": "",
"package_type": "",
"package_doc_md": "",
"package_features": []
}

View File

@ -1,3 +1,6 @@
Flask
requests
Markdown
Django
djangorestframework

99
static/css/map.css Normal file
View File

@ -0,0 +1,99 @@
.custom-map {
position: relative;
width: 100%;
height: auto;
overflow: auto;
max-width: 100%;
max-height: 80vh;
border-radius: 14px;
box-shadow: 0 6px 24px rgba(0,0,0,0.15), 0 1.5px 6px #c6c8c9;
background: #f7f9fb;
border: none;
padding: 24px 0 24px 0;
}
.custom-map #map-container.dragging {
cursor: grabbing;
}
.custom-map #map-viewport {
display: block !important;
width: auto !important;
height: auto !important;
min-width: 0 !important;
min-height: 0 !important;
}
.custom-map #main-map {
display: block;
width: auto !important;
height: auto !important;
max-width: none !important;
max-height: none !important;
min-width: 0 !important;
min-height: 0 !important;
}
.custom-map #pins {
position: absolute;
top: 0; left: 0;
pointer-events: none; /* Clicks fall through to map image */
}
.custom-map .pin {
position: absolute;
width: 64px;
height: 64px;
transform: translate(-50%, -100%); /* Center bottom point on location */
pointer-events: auto; /* Pins remain clickable */
z-index: 10;
transition: transform 0.1s;
cursor: pointer;
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.15));
}
.custom-map .pin:hover {
transform: translate(-50%, -120%) scale(1.1);
filter: drop-shadow(0 6px 18px rgba(0,104,207,0.15)) brightness(1.15);
z-index: 18;
}
.custom-map .pin img {
width: 100%;
height: 100%;
display: block;
border-radius: 10px;
user-select: none;
}
.pin-label {
display: block;
min-width: 45px;
max-width: 168px;
margin-top: 7px;
font-size: 1rem;
font-weight: 500;
color: #1452b9;
background: #fff;
border-radius: 24px;
padding: 2px 10px;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.11);
text-align: center;
pointer-events: none; /* so clicks go to the pin anchor */
opacity: 0.90;
position: absolute;
left: 50%;
transform: translate(-50%, 0%);
z-index: 15;
border: 1.5px solid #dde7fa;
}
.pin:hover .pin-label {
opacity: 1;
background: #f3f8ff;
color: #1d4ba0;
}
.pin-highlight {
animation: pinPulse 0.8s ease;
}
@keyframes pinPulse {
0% { box-shadow: 0 0 0 0px rgba(45,156,219,0.52);}
80% { box-shadow: 0 0 0 18px rgba(45,156,219,0);}
100% { box-shadow: 0 0 0 0px rgba(45,156,219,0);}
}

BIN
static/dungeon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

207
static/js/map.js Normal file
View File

@ -0,0 +1,207 @@
function renderPins(pins) {
const pinsDiv = document.getElementById("pins");
pinsDiv.innerHTML = '';
const img = document.getElementById("main-map");
const imgWidth = img.clientWidth;
const imgHeight = img.clientHeight;
pins.forEach(pin => {
console.log(pin)
const el = document.createElement('a');
el.className = "pin";
el.href = pin.url;
el.title = pin.label;
el.target = "_blank";
el.style.left = (imgWidth * (pin.x / 100)) + 'px';
el.style.top = (imgHeight * (pin.y / 100)) + 'px';
el.dataset.pinid = pin.id;
el.dataset.x = pin.x;
el.dataset.y = pin.y;
if (pin.pin_type == "general"){
el.innerHTML = '<img src="/static/pin.svg" alt="general pin" />';
}
if (pin.pin_type == "town"){
el.innerHTML = '<img src="/static/town.png" alt="town pin" />';
}
if (pin.pin_type == "dungeon"){
el.innerHTML = '<img src="/static/dungeon.png" alt="dungeon pin" />';
}
const label = document.createElement('div');
label.className = 'pin-label';
label.innerText = pin.label;
el.appendChild(label);
pinsDiv.appendChild(el);
});
renderPinsTable(pins)
}
function fetchPins() {
fetch("/api/pins/")
.then(r => {
if (!r.ok) throw new Error("Pin fetch failed");
return r.json();
})
.then(renderPins)
.catch(() => {
// For demo/development: Show example
renderPins([
{x:15, y:30, label:'Demo Pin', url:'/'}
]);
});
}
function renderPinsTable(pins) {
const tbody = document.querySelector('#pins-table tbody');
tbody.innerHTML = '';
pins.forEach((pin, idx) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${idx+1}</td>
<td>${escapeHTML(pin.label)}</td>
<td><a href="${pin.url}" target="_blank">${pin.url}</a></td>
<td>${pin.x.toFixed(2)}</td>
<td>${pin.y.toFixed(2)}</td>
<td>
<button class="uk-button uk-button-small uk-button-default" onclick="centerMapOnPin(${pin.x},${pin.y})">Center</button>
<button class="uk-button uk-button-small uk-button-primary" onclick="editPin('${pin.id}')">Edit</button>
<button class="uk-button uk-button-small uk-button-danger" onclick="deletePin('${pin.id}')">Delete</button>
</td>
`;
tbody.appendChild(tr);
});
}
function escapeHTML(str) {
// Prevent breaking the DOM if user enters < > or & etc
return (str||'').replace(/[&<>"'`]/g, s => ({
'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;', '`':'&#96;'
}[s]));
}
function centerMapOnPin(xPercent, yPercent) {
const mapImg = document.getElementById('main-map');
const container = document.getElementById('map-container');
// Calculate pixel pos relative to image
const imgWidth = mapImg.clientWidth;
const imgHeight = mapImg.clientHeight;
const pinX = imgWidth * (xPercent / 100);
const pinY = imgHeight * (yPercent / 100);
// Center in the scrollable container
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
container.scrollLeft = Math.max(0, pinX - containerWidth / 2);
container.scrollTop = Math.max(0, pinY - containerHeight / 2);
// Highlight the right pin
const pinEl = Array.from(document.querySelectorAll('.pin')).find(
el => Math.abs(parseFloat(el.dataset.x) - xPercent) < 0.0001 &&
Math.abs(parseFloat(el.dataset.y) - yPercent) < 0.0001
);
if(pinEl) {
pinEl.classList.add('pin-highlight');
setTimeout(()=>pinEl.classList.remove('pin-highlight'), 1200);
}
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
fetchPins();
document.getElementById('map-container').addEventListener('contextmenu', function(e) {
e.preventDefault();
const mapImg = document.getElementById('main-map');
const rect = mapImg.getBoundingClientRect();
if (!(e.target === mapImg || e.target.id === 'pins')) return;
const x = 100 * (e.clientX - rect.left) / rect.width;
const y = 100 * (e.clientY - rect.top) / rect.height;
document.getElementById('pin-x').value = x;
document.getElementById('pin-y').value = y;
document.getElementById('add-pin-form').reset();
UIkit.modal('#add-pin-modal').show();
});
document.getElementById('add-pin-form').addEventListener('submit', function(e) {
e.preventDefault();
const x = parseFloat(document.getElementById('pin-x').value);
const y = parseFloat(document.getElementById('pin-y').value);
const label = document.getElementById('pin-label').value;
const url = document.getElementById('pin-url').value;
const pin_type = document.getElementById('pin-type').value;
fetch('/api/pins/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({x, y, label, url, pin_type})
}).then(resp => {
if (resp.ok) {
UIkit.modal('#add-pin-modal').hide();
fetchPins();
} else {
resp.json().then(data => alert(JSON.stringify(data)));
}
});
});
function resizePinsDiv() {
var mapImg = document.getElementById('main-map');
var pinsDiv = document.getElementById('pins');
// Set pins div to same px size as image
pinsDiv.style.width = mapImg.width + 'px';
pinsDiv.style.height = mapImg.height + 'px';
pinsDiv.style.position = 'absolute';
pinsDiv.style.top = '0';
pinsDiv.style.left = '0';
}
document.getElementById('main-map').addEventListener('load', resizePinsDiv);
window.addEventListener('resize', resizePinsDiv);
if (document.getElementById('main-map').complete) resizePinsDiv();
// This code handles the dragging of the map
const container = document.getElementById('map-container');
let isDragging = false;
let startX, startY, scrollLeft, scrollTop;
container.addEventListener('mousedown', function(e) {
isDragging = true;
container.classList.add('dragging');
startX = e.pageX - container.offsetLeft;
startY = e.pageY - container.offsetTop;
scrollLeft = container.scrollLeft;
scrollTop = container.scrollTop;
});
container.addEventListener('mouseleave', function() {
isDragging = false;
container.classList.remove('dragging');
});
container.addEventListener('mouseup', function() {
isDragging = false;
container.classList.remove('dragging');
});
container.addEventListener('mousemove', function(e) {
if (!isDragging) return;
e.preventDefault();
const x = e.pageX - container.offsetLeft;
const y = e.pageY - container.offsetTop;
const walkX = x - startX;
const walkY = y - startY;
container.scrollLeft = scrollLeft - walkX;
container.scrollTop = scrollTop - walkY;
});

BIN
static/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 MiB

1
static/pin.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#ff5151" d="M12 11.5A2.5 2.5 0 0 1 9.5 9A2.5 2.5 0 0 1 12 6.5A2.5 2.5 0 0 1 14.5 9a2.5 2.5 0 0 1-2.5 2.5M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7"/></svg>

After

Width:  |  Height:  |  Size: 270 B

BIN
static/town.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

18
test.py
View File

@ -1,18 +0,0 @@
from backend.models.users import UsersModel
UsersModel.create_table()
payload = UsersModel.Payload(
user_username="Mechseroms",
user_email="jadowyne.ulve@outlook.com",
user_hashed_password="test"
)
#UsersModel.insert_tuple(payload=payload.payload_dictionary())
authenticated, user, message = UsersModel.authenticate_login('Mechseroms', 'test')
print(authenticated)
print(user)
print(message)

156
test_character.json Normal file
View File

@ -0,0 +1,156 @@
{
"name": "Elira Sunshadow",
"level": 5,
"race": "Elf",
"subrace": "High Elf",
"class": "Ranger",
"subclass": "Hunter",
"background": "Outlander",
"alignment": "Chaotic Good",
"size": "Medium",
"age": 121,
"gender": "Female",
"height": "5'7\"",
"weight": 130,
"deity": "Sehanine Moonbow",
"strength_base": 10,
"dexterity_base": 16,
"constitution_base": 13,
"intelligence_base": 12,
"wisdom_base": 15,
"charisma_base": 9,
"armor_base": 11,
"strength": 11,
"dexterity": 18,
"constitution": 14,
"intelligence": 13,
"wisdom": 16,
"charisma": 10,
"armor": 15,
"strength_modifier": 0,
"dexterity_modifier": 4,
"constitution_modifier": 2,
"intelligence_modifier": 1,
"wisdom_modifier": 3,
"charisma_modifier": 0,
"proficiency": 3,
"inspiration": true,
"experience": 6500,
"proficiencies_armor": ["Light Armor", "Medium Armor", "Shields"],
"proficiencies_weapons": ["Simple Weapons", "Martial Weapons"],
"proficiencies_tools": ["Flute", "Herbalism Kit"],
"languages": ["Common", "Elvish", "Sylvan"],
"strength_save": 0,
"dexterity_save": 7,
"constitution_save": 2,
"intelligence_save": 1,
"wisdom_save": 6,
"charisma_save": 0,
"hp": 42,
"hp_max": 42,
"hp_temp": 5,
"hit_die": "1d10",
"initiative": 4,
"deathsaves_successes": 0,
"deathsaves_failures": 0,
"fire_resistance": false,
"poison_resistance": true,
"psychic_resistance": false,
"cold_resistance": false,
"thunder_resistance": false,
"acid_resistance": false,
"force_resistance": false,
"radiant_resistance": false,
"necrotic_resistance": false,
"bludgeoning_resistance": false,
"piercing_resistance": true,
"slashing_resistance": false,
"immunities": [],
"vulnerabilities": ["Radiant"],
"speed_base": 35,
"speed_type": ["Walk"],
"darkvision": 60,
"blindsight": 0,
"tremorsense": 0,
"truesight": 0,
"athletics": 2,
"acrobatics": 7,
"sleight_of_hand": 4,
"stealth": 7,
"arcana": 1,
"history": 1,
"investigation": 4,
"nature": 3,
"religion": 1,
"animal_handling": 6,
"insight": 6,
"medicine": 3,
"perception": 6,
"survival": 6,
"deception": 0,
"intimidation": 0,
"performance": 0,
"persuasion": 0,
"athletics_passive": 2,
"acrobatics_passive": 7,
"sleight_of_hand_passive": 4,
"stealth_passive": 7,
"arcana_passive": 1,
"history_passive": 1,
"investigation_passive": 4,
"nature_passive": 3,
"religion_passive": 1,
"animal_handling_passive": 6,
"insight_passive": 6,
"medicine_passive": 3,
"perception_passive": 16,
"survival_passive": 6,
"deception_passive": 0,
"intimidation_passive": 0,
"performance_passive": 0,
"persuasion_passive": 0,
"passive_perception": 16,
"passive_investigation": 14,
"passive_insight": 16,
"spellcasting_ability": "Wisdom",
"spell_save_dc": 14,
"spell_attack_bonus": 6,
"known_spells": ["Hunter's Mark", "Goodberry", "Speak with Animals"],
"prepared_spells": ["Hunter's Mark", "Goodberry"],
"spell_slots": {"1": 4, "2": 2},
"cantrips": ["Produce Flame", "Druidcraft"],
"spellcasting_class": "Ranger",
"exhaustion_level": 0,
"conditions_active": [],
"cp": 43,
"sp": 27,
"ep": 0,
"gp": 89,
"pp": 3,
"equipment": ["Longbow", "Quiver", "Studded Leather Armor", "Explorer's Pack", "Flute"],
"features": ["Favored Enemy", "Natural Explorer"],
"traits": ["Keen Senses", "Fey Ancestry"],
"personality_traits": ["I watch over my friends as if they were a litter of newborn pups."],
"ideals": ["Change: Life is like the seasons, in constant change, and we must change with it."],
"bonds": ["An injury to the unspoiled wilderness of my home is an injury to me."],
"flaws": ["I am slow to trust members of other races."],
"notes": "Carries a carved wooden pendant given by her mentor.",
"custom_attributes": {"Favorite Animal": "Stag", "Secret": "Afraid of deep water"}
}

View File

@ -1,164 +0,0 @@
from flask import Flask, jsonify, request, render_template, session, redirect, url_for
from functools import wraps
import requests
import markdown
app = Flask(__name__)
app.secret_key = "53016ca1157efe86174936227721c701048926d4c31c4531201bcfcff4725277"
backend_config = {
'host': '127.0.0.1',
'port': '5001'
}
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if 'user' not in session or session['user'] == None:
return redirect(url_for('login'))
return func(*args, **kwargs)
return wrapper
@app.route('/')
@login_required
def home():
return 'Hello, World!'
@app.route('/p/<uuid>', methods=['GET'])
@login_required
def package_main(uuid):
defined_types = [
{'value': '5e_class', 'string': '5e Class'},
{'value': '5e_race', 'string': '5e Race'},
{'value': '5e_background', 'string': '5e Background'},
{'value': '5e_subclass', 'string': '5e Subclass'},
]
if uuid == "new":
response = requests.post(url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/add')
if response.status_code == 200 and not response.json()['error']:
return redirect(f'/p/{response.json()['package']['package_uuid']}')
response = requests.get(url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/get/{uuid}')
if response.status_code == 200 and not response.json()['error']:
package = response.json()['package']
return render_template('package.html', package_object=package, defined_types=defined_types)
return redirect('/')
@app.route('/p/<uuid>/save', methods=['POST'])
def package_save(uuid):
if request.method != 'POST':
return jsonify(
error = True,
message = f"Incorrect Method attempted; {request.method}."
)
response = requests.post(
url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/update',
json = request.get_json()
)
if response.status_code == 200 and not response.json()['error']:
return jsonify(
error=False,
message=f"{uuid} updated successfully!"
)
return jsonify(
error=True,
message=f"Error while trying to update package {uuid}"
)
@app.route('/f/<uuid>', methods=['GET'])
@login_required
def feature_main(uuid):
if uuid == "new":
response = requests.post(url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/add')
if response.status_code == 200 and not response.json()['error']:
return redirect(f'/f/{response.json()['feature']['feature_uuid']}')
response = requests.get(url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/get/{uuid}')
if response.status_code == 200 and not response.json()['error']:
feature = response.json()['feature']
return render_template('feature.html', feature=feature)
return redirect('/')
@app.route('/f/<uuid>/save', methods=['POST'])
def feature_save(uuid):
if request.method != 'POST':
return jsonify(
error = True,
message = f"Incorrect Method attempted; {request.method}."
)
response = requests.post(
url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/update',
json = request.get_json()
)
if response.status_code == 200 and not response.json()['error']:
return jsonify(
error=False,
message=f"{uuid} updated successfully!"
)
return jsonify(
error=True,
message=f"Error while trying to update feature {uuid}"
)
@app.route('/login', methods=['GET'])
def login():
return render_template('login.html')
@app.route('/logout', methods=['GET'])
def logout():
if request.method != "GET":
return jsonify(
error = True,
message = f"Incorrect Method attempted; {request.method}."
)
if session['user']:
del session['user']
return redirect('/login')
@app.route('/login/authenticate', methods=['POST'])
def basic_authenticate():
if request.method != 'POST':
return jsonify(
error = True,
message = f"Incorrect Method attempted; {request.method}."
)
response = requests.post(
url = f'http://{backend_config["host"]}:{backend_config["port"]}/users/authenticate',
json = request.get_json()
)
if response.status_code == 200 and not response.json()['error']:
session['user'] = response.json()['user']
return jsonify(
error = False,
message = f"Successfully Logged in."
)
return jsonify(
error = True,
message = f"Username or Password Incorrect!"
)
if __name__ == '__main__':
app.run(debug=True)