diff --git a/AGENTS.md b/AGENTS.md index 626dab9..82f3707 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. \ No newline at end of file +(Update and evolve as practices change) diff --git a/character_template.json b/character_template.json new file mode 100644 index 0000000..647ece7 --- /dev/null +++ b/character_template.json @@ -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": {} +} diff --git a/db.sqlite3 b/db.sqlite3 index e69de29..e5b772a 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/feature_template.json b/feature_template.json new file mode 100644 index 0000000..a1d26d8 --- /dev/null +++ b/feature_template.json @@ -0,0 +1,7 @@ +{ + "feature_name": "", + "feature_description": "", + "feature_data": { + "effects": [] + } +} \ No newline at end of file diff --git a/lisiumsite/__pycache__/__init__.cpython-314.pyc b/lisiumsite/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..bb77469 Binary files /dev/null and b/lisiumsite/__pycache__/__init__.cpython-314.pyc differ diff --git a/lisiumsite/__pycache__/settings.cpython-313.pyc b/lisiumsite/__pycache__/settings.cpython-313.pyc index 6931bad..460a789 100644 Binary files a/lisiumsite/__pycache__/settings.cpython-313.pyc and b/lisiumsite/__pycache__/settings.cpython-313.pyc differ diff --git a/lisiumsite/__pycache__/settings.cpython-314.pyc b/lisiumsite/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000..bb99e14 Binary files /dev/null and b/lisiumsite/__pycache__/settings.cpython-314.pyc differ diff --git a/lisiumsite/__pycache__/urls.cpython-314.pyc b/lisiumsite/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000..0ee577c Binary files /dev/null and b/lisiumsite/__pycache__/urls.cpython-314.pyc differ diff --git a/lisiumsite/__pycache__/wsgi.cpython-314.pyc b/lisiumsite/__pycache__/wsgi.cpython-314.pyc new file mode 100644 index 0000000..66498e3 Binary files /dev/null and b/lisiumsite/__pycache__/wsgi.cpython-314.pyc differ diff --git a/lisiumsite/settings.py b/lisiumsite/settings.py index 9fb89f3..5cb7fc9 100644 --- a/lisiumsite/settings.py +++ b/lisiumsite/settings.py @@ -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 +] diff --git a/main/__pycache__/__init__.cpython-314.pyc b/main/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..1b5a88a Binary files /dev/null and b/main/__pycache__/__init__.cpython-314.pyc differ diff --git a/main/__pycache__/admin.cpython-313.pyc b/main/__pycache__/admin.cpython-313.pyc index 9729497..dd3c31a 100644 Binary files a/main/__pycache__/admin.cpython-313.pyc and b/main/__pycache__/admin.cpython-313.pyc differ diff --git a/main/__pycache__/admin.cpython-314.pyc b/main/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000..6ed249c Binary files /dev/null and b/main/__pycache__/admin.cpython-314.pyc differ diff --git a/main/__pycache__/apps.cpython-314.pyc b/main/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000..e7a4cd8 Binary files /dev/null and b/main/__pycache__/apps.cpython-314.pyc differ diff --git a/main/__pycache__/models.cpython-313.pyc b/main/__pycache__/models.cpython-313.pyc index 63a8b93..1c0cdd8 100644 Binary files a/main/__pycache__/models.cpython-313.pyc and b/main/__pycache__/models.cpython-313.pyc differ diff --git a/main/__pycache__/models.cpython-314.pyc b/main/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000..5c6715c Binary files /dev/null and b/main/__pycache__/models.cpython-314.pyc differ diff --git a/main/__pycache__/serializers.cpython-313.pyc b/main/__pycache__/serializers.cpython-313.pyc new file mode 100644 index 0000000..9510f7d Binary files /dev/null and b/main/__pycache__/serializers.cpython-313.pyc differ diff --git a/main/__pycache__/serializers.cpython-314.pyc b/main/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000..8d8538a Binary files /dev/null and b/main/__pycache__/serializers.cpython-314.pyc differ diff --git a/main/__pycache__/urls.cpython-313.pyc b/main/__pycache__/urls.cpython-313.pyc index 971a9fa..e3d9c64 100644 Binary files a/main/__pycache__/urls.cpython-313.pyc and b/main/__pycache__/urls.cpython-313.pyc differ diff --git a/main/__pycache__/urls.cpython-314.pyc b/main/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000..c394f83 Binary files /dev/null and b/main/__pycache__/urls.cpython-314.pyc differ diff --git a/main/__pycache__/views.cpython-313.pyc b/main/__pycache__/views.cpython-313.pyc index c781ed7..6fa4acc 100644 Binary files a/main/__pycache__/views.cpython-313.pyc and b/main/__pycache__/views.cpython-313.pyc differ diff --git a/main/__pycache__/views.cpython-314.pyc b/main/__pycache__/views.cpython-314.pyc new file mode 100644 index 0000000..58cbd29 Binary files /dev/null and b/main/__pycache__/views.cpython-314.pyc differ diff --git a/main/admin.py b/main/admin.py index 8c38f3f..107cafd 100644 --- a/main/admin.py +++ b/main/admin.py @@ -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') diff --git a/main/migrations/0001_initial.py b/main/migrations/0001_initial.py new file mode 100644 index 0000000..c998229 --- /dev/null +++ b/main/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/main/migrations/0002_pin.py b/main/migrations/0002_pin.py new file mode 100644 index 0000000..0c97808 --- /dev/null +++ b/main/migrations/0002_pin.py @@ -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)')), + ], + ), + ] diff --git a/main/migrations/0003_pin_pin_type.py b/main/migrations/0003_pin_pin_type.py new file mode 100644 index 0000000..93e6f15 --- /dev/null +++ b/main/migrations/0003_pin_pin_type.py @@ -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), + ), + ] diff --git a/main/migrations/0004_remove_character_features_character_features.py b/main/migrations/0004_remove_character_features_character_features.py new file mode 100644 index 0000000..2b28ea0 --- /dev/null +++ b/main/migrations/0004_remove_character_features_character_features.py @@ -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'), + ), + ] diff --git a/main/migrations/0005_remove_character_armor_remove_character_charisma_and_more.py b/main/migrations/0005_remove_character_armor_remove_character_charisma_and_more.py new file mode 100644 index 0000000..b78653d --- /dev/null +++ b/main/migrations/0005_remove_character_armor_remove_character_charisma_and_more.py @@ -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), + ), + ] diff --git a/main/migrations/0006_remove_character_athletics.py b/main/migrations/0006_remove_character_athletics.py new file mode 100644 index 0000000..c23223a --- /dev/null +++ b/main/migrations/0006_remove_character_athletics.py @@ -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', + ), + ] diff --git a/main/migrations/0007_remove_character_acrobatics_and_more.py b/main/migrations/0007_remove_character_acrobatics_and_more.py new file mode 100644 index 0000000..cf88660 --- /dev/null +++ b/main/migrations/0007_remove_character_acrobatics_and_more.py @@ -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', + ), + ] diff --git a/main/migrations/0008_remove_character_hp_max_remove_character_initiative.py b/main/migrations/0008_remove_character_hp_max_remove_character_initiative.py new file mode 100644 index 0000000..94de5d5 --- /dev/null +++ b/main/migrations/0008_remove_character_hp_max_remove_character_initiative.py @@ -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', + ), + ] diff --git a/main/migrations/0009_character_hp_features.py b/main/migrations/0009_character_hp_features.py new file mode 100644 index 0000000..2725650 --- /dev/null +++ b/main/migrations/0009_character_hp_features.py @@ -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), + ), + ] diff --git a/main/migrations/0010_alter_character_hp_features.py b/main/migrations/0010_alter_character_hp_features.py new file mode 100644 index 0000000..673e514 --- /dev/null +++ b/main/migrations/0010_alter_character_hp_features.py @@ -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), + ), + ] diff --git a/main/migrations/0011_feature_feature_requirements.py b/main/migrations/0011_feature_feature_requirements.py new file mode 100644 index 0000000..928a659 --- /dev/null +++ b/main/migrations/0011_feature_feature_requirements.py @@ -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), + ), + ] diff --git a/main/migrations/0012_packagefeature_requirements_override.py b/main/migrations/0012_packagefeature_requirements_override.py new file mode 100644 index 0000000..c1b761e --- /dev/null +++ b/main/migrations/0012_packagefeature_requirements_override.py @@ -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), + ), + ] diff --git a/main/migrations/0013_alter_feature_feature_requirements.py b/main/migrations/0013_alter_feature_feature_requirements.py new file mode 100644 index 0000000..f5f3212 --- /dev/null +++ b/main/migrations/0013_alter_feature_feature_requirements.py @@ -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), + ), + ] diff --git a/main/migrations/0014_alter_character_features_alter_character_packages.py b/main/migrations/0014_alter_character_features_alter_character_packages.py new file mode 100644 index 0000000..920457b --- /dev/null +++ b/main/migrations/0014_alter_character_features_alter_character_packages.py @@ -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'), + ), + ] diff --git a/main/migrations/0015_alter_character_features_alter_character_packages.py b/main/migrations/0015_alter_character_features_alter_character_packages.py new file mode 100644 index 0000000..fb1df3a --- /dev/null +++ b/main/migrations/0015_alter_character_features_alter_character_packages.py @@ -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'), + ), + ] diff --git a/main/migrations/0016_alter_feature_feature_requirements.py b/main/migrations/0016_alter_feature_feature_requirements.py new file mode 100644 index 0000000..7561c71 --- /dev/null +++ b/main/migrations/0016_alter_feature_feature_requirements.py @@ -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), + ), + ] diff --git a/main/migrations/__pycache__/0001_initial.cpython-313.pyc b/main/migrations/__pycache__/0001_initial.cpython-313.pyc new file mode 100644 index 0000000..b26e466 Binary files /dev/null and b/main/migrations/__pycache__/0001_initial.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0001_initial.cpython-314.pyc b/main/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000..c35b894 Binary files /dev/null and b/main/migrations/__pycache__/0001_initial.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0002_character.cpython-313.pyc b/main/migrations/__pycache__/0002_character.cpython-313.pyc new file mode 100644 index 0000000..591079a Binary files /dev/null and b/main/migrations/__pycache__/0002_character.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0002_pin.cpython-313.pyc b/main/migrations/__pycache__/0002_pin.cpython-313.pyc new file mode 100644 index 0000000..8ff9fa1 Binary files /dev/null and b/main/migrations/__pycache__/0002_pin.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0002_pin.cpython-314.pyc b/main/migrations/__pycache__/0002_pin.cpython-314.pyc new file mode 100644 index 0000000..1b905a5 Binary files /dev/null and b/main/migrations/__pycache__/0002_pin.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0003_feature_package_character_packages.cpython-313.pyc b/main/migrations/__pycache__/0003_feature_package_character_packages.cpython-313.pyc new file mode 100644 index 0000000..9e168a7 Binary files /dev/null and b/main/migrations/__pycache__/0003_feature_package_character_packages.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0003_pin_pin_type.cpython-313.pyc b/main/migrations/__pycache__/0003_pin_pin_type.cpython-313.pyc new file mode 100644 index 0000000..55d4e80 Binary files /dev/null and b/main/migrations/__pycache__/0003_pin_pin_type.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0003_pin_pin_type.cpython-314.pyc b/main/migrations/__pycache__/0003_pin_pin_type.cpython-314.pyc new file mode 100644 index 0000000..2b7334c Binary files /dev/null and b/main/migrations/__pycache__/0003_pin_pin_type.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0004_alter_character_background_and_more.cpython-313.pyc b/main/migrations/__pycache__/0004_alter_character_background_and_more.cpython-313.pyc new file mode 100644 index 0000000..1feb260 Binary files /dev/null and b/main/migrations/__pycache__/0004_alter_character_background_and_more.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-313.pyc b/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-313.pyc new file mode 100644 index 0000000..e28ea1b Binary files /dev/null and b/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-314.pyc b/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-314.pyc new file mode 100644 index 0000000..56ad837 Binary files /dev/null and b/main/migrations/__pycache__/0004_remove_character_features_character_features.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0005_packagefeature_alter_package_features.cpython-313.pyc b/main/migrations/__pycache__/0005_packagefeature_alter_package_features.cpython-313.pyc new file mode 100644 index 0000000..dd362e1 Binary files /dev/null and b/main/migrations/__pycache__/0005_packagefeature_alter_package_features.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-313.pyc b/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-313.pyc new file mode 100644 index 0000000..f9162b3 Binary files /dev/null and b/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-314.pyc b/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-314.pyc new file mode 100644 index 0000000..fc6f141 Binary files /dev/null and b/main/migrations/__pycache__/0005_remove_character_armor_remove_character_charisma_and_more.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0006_remove_character_athletics.cpython-313.pyc b/main/migrations/__pycache__/0006_remove_character_athletics.cpython-313.pyc new file mode 100644 index 0000000..d3b5868 Binary files /dev/null and b/main/migrations/__pycache__/0006_remove_character_athletics.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0006_remove_character_athletics.cpython-314.pyc b/main/migrations/__pycache__/0006_remove_character_athletics.cpython-314.pyc new file mode 100644 index 0000000..e73d37b Binary files /dev/null and b/main/migrations/__pycache__/0006_remove_character_athletics.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-313.pyc b/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-313.pyc new file mode 100644 index 0000000..af51717 Binary files /dev/null and b/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-314.pyc b/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-314.pyc new file mode 100644 index 0000000..ca348e9 Binary files /dev/null and b/main/migrations/__pycache__/0007_remove_character_acrobatics_and_more.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-313.pyc b/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-313.pyc new file mode 100644 index 0000000..be31a6b Binary files /dev/null and b/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-314.pyc b/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-314.pyc new file mode 100644 index 0000000..f8a0a99 Binary files /dev/null and b/main/migrations/__pycache__/0008_remove_character_hp_max_remove_character_initiative.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0009_character_hp_features.cpython-313.pyc b/main/migrations/__pycache__/0009_character_hp_features.cpython-313.pyc new file mode 100644 index 0000000..bfd0461 Binary files /dev/null and b/main/migrations/__pycache__/0009_character_hp_features.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0009_character_hp_features.cpython-314.pyc b/main/migrations/__pycache__/0009_character_hp_features.cpython-314.pyc new file mode 100644 index 0000000..a0f3e8f Binary files /dev/null and b/main/migrations/__pycache__/0009_character_hp_features.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-313.pyc b/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-313.pyc new file mode 100644 index 0000000..c4e0cfa Binary files /dev/null and b/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-314.pyc b/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-314.pyc new file mode 100644 index 0000000..971becc Binary files /dev/null and b/main/migrations/__pycache__/0010_alter_character_hp_features.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-313.pyc b/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-313.pyc new file mode 100644 index 0000000..db27538 Binary files /dev/null and b/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-314.pyc b/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-314.pyc new file mode 100644 index 0000000..b975fc0 Binary files /dev/null and b/main/migrations/__pycache__/0011_feature_feature_requirements.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-313.pyc b/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-313.pyc new file mode 100644 index 0000000..5e0c431 Binary files /dev/null and b/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-314.pyc b/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-314.pyc new file mode 100644 index 0000000..3f3e9a5 Binary files /dev/null and b/main/migrations/__pycache__/0012_packagefeature_requirements_override.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-313.pyc b/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-313.pyc new file mode 100644 index 0000000..0bd58e5 Binary files /dev/null and b/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-313.pyc differ diff --git a/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-314.pyc b/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-314.pyc new file mode 100644 index 0000000..f7b49b0 Binary files /dev/null and b/main/migrations/__pycache__/0013_alter_feature_feature_requirements.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0014_alter_character_features_alter_character_packages.cpython-314.pyc b/main/migrations/__pycache__/0014_alter_character_features_alter_character_packages.cpython-314.pyc new file mode 100644 index 0000000..1c3e2b3 Binary files /dev/null and b/main/migrations/__pycache__/0014_alter_character_features_alter_character_packages.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0015_alter_character_features_alter_character_packages.cpython-314.pyc b/main/migrations/__pycache__/0015_alter_character_features_alter_character_packages.cpython-314.pyc new file mode 100644 index 0000000..5481d89 Binary files /dev/null and b/main/migrations/__pycache__/0015_alter_character_features_alter_character_packages.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/0016_alter_feature_feature_requirements.cpython-314.pyc b/main/migrations/__pycache__/0016_alter_feature_feature_requirements.cpython-314.pyc new file mode 100644 index 0000000..d162bc7 Binary files /dev/null and b/main/migrations/__pycache__/0016_alter_feature_feature_requirements.cpython-314.pyc differ diff --git a/main/migrations/__pycache__/__init__.cpython-314.pyc b/main/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..bd19688 Binary files /dev/null and b/main/migrations/__pycache__/__init__.cpython-314.pyc differ diff --git a/main/models.py b/main/models.py index 71a8362..3fa0963 100644 --- a/main/models.py +++ b/main/models.py @@ -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}%)" \ No newline at end of file diff --git a/main/serializers.py b/main/serializers.py new file mode 100644 index 0000000..3c1941e --- /dev/null +++ b/main/serializers.py @@ -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'] diff --git a/main/templates/main/character_sheet.html b/main/templates/main/character_sheet.html new file mode 100644 index 0000000..d07c512 --- /dev/null +++ b/main/templates/main/character_sheet.html @@ -0,0 +1,290 @@ +{% load static %} + + + + D&D 5e Full Character Sheet + + + + + +
+ +
+

+ {{ character.name }} + Level {{ character.level }} +

+
+
+
{{ character.race }} {{ character.subrace }}
+
{{ character.char_class.package_name }} {{ character.subclass }}
+
{{ character.background }}
+
{{ character.alignment }}
+
{{ character.size }}
+
{{ character.age }}
+
{{ character.gender }}
+
{{ character.height }}
+
{{ character.weight }} lbs
+
{{ character.deity }}
+
{{ character.experience }}
+
+ + {% if character.inspiration %} + {{ character.inspiration }} + {% else %} + + {% endif %} +
+
+
+ +
+

Core Attributes

+
+ + + + + + + + + + + + + +
ScoreModifierSave
STR{{ character.strength }}{{ character.strength_modifier }}{{ character.strength_save }}
DEX{{ character.dexterity }}{{ character.dexterity_modifier }}{{ character.dexterity_save }}
CON{{ character.constitution }}{{ character.constitution_modifier }}{{ character.constitution_save }}
INT{{ character.intelligence }}{{ character.intelligence_modifier }}{{ character.intelligence_save }}
WIS{{ character.wisdom }}{{ character.wisdom_modifier }}{{ character.wisdom_save }}
CHA{{ character.charisma }}{{ character.charisma_modifier }}{{ character.charisma_save }}
Armor{{ character.armor_base }}--
+
+
+
Proficiency {{ character.proficiency }}
+
Initiative {{ character.initiative }}
+
Speed {{ character.speed_base }} {% if character.speed_type %}({{ character.speed_type|join:', ' }}){% endif %}
+
+
+ +
+
+
+

HP & Status

+
    +
  • HP {{ character.hp }} / {{ character.hp_max }} {% if character.hp_temp %}(+{{ character.hp_temp }} temp){% endif %}
  • +
  • Hit Die {{ character.hit_die }}
  • +
  • Death Saves {{ character.deathsaves_successes }}✓/{{ character.deathsaves_failures }}✗
  • +
  • Exhaustion {% if character.exhaustion_level > 0 %}{{ character.exhaustion_level }}{% else %}0{% endif %}
  • +
  • Active Conditions {% for cond in character.conditions_active %}{{ cond }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
+
+
+
+
+

Resistances & Immunities

+
    +
  • Immunities: {% for i in character.immunities %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
  • Vulnerabilities: {% for v in character.vulnerabilities %}{{ v }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
  • Resistances: + {% 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 %} + None + {% endif %} +
  • +
+
+
+
+
+

Senses

+
    +
  • Darkvision: {{ character.darkvision }}
  • +
  • Blindsight: {{ character.blindsight }}
  • +
  • Tremorsense: {{ character.tremorsense }}
  • +
  • Truesight: {{ character.truesight }}
  • +
+
+
+
+
+

Proficiencies & Languages

+
    +
  • Armor: {% for i in character.proficiencies_armor %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
  • Weapons: {% for i in character.proficiencies_weapons %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
  • Tools: {% for i in character.proficiencies_tools %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
  • Languages: {% for i in character.languages %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
  • +
+
+
+
+ + +
+ + +
+
+ + +
+ + + + diff --git a/main/templates/main/feature.html b/main/templates/main/feature.html new file mode 100644 index 0000000..10c932c --- /dev/null +++ b/main/templates/main/feature.html @@ -0,0 +1,230 @@ +{% load static %} + + + + Features + + + + +
+
+
+

Add a Feature

+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+
+

By adding an operation to your feature you can have the feature modifier and manipulate + a characters properties at runtime. +

+
+
+ +
+
+
+
+

Small

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ + + + +
+
+
+
+ + +
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/main/templates/main/home.html b/main/templates/main/home.html deleted file mode 100644 index 9295f27..0000000 --- a/main/templates/main/home.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - Lisium Django Demo - - -

Welcome to Django

-

- This is being rendered from a django template. -

- - \ No newline at end of file diff --git a/main/templates/main/map.html b/main/templates/main/map.html new file mode 100644 index 0000000..f8eea19 --- /dev/null +++ b/main/templates/main/map.html @@ -0,0 +1,88 @@ + +{% load static %} +{% csrf_token %} + + + + + Pin Map + + + + + + +
+

Interactive Map with Pins

+
+
+ Map +
+
+
+
+
+

All Pins

+
+ + + + + + + + + + + + + + +
#LabelURLX (%)Y (%)Actions
+
+
+ +
+
+ +

+ + Add a Map Pin +

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/main/urls.py b/main/urls.py index d3aba75..6d17c10 100644 --- a/main/urls.py +++ b/main/urls.py @@ -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'), ] \ No newline at end of file diff --git a/main/views.py b/main/views.py index 3854d4e..f03912b 100644 --- a/main/views.py +++ b/main/views.py @@ -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) \ No newline at end of file diff --git a/package_template.json b/package_template.json new file mode 100644 index 0000000..325c460 --- /dev/null +++ b/package_template.json @@ -0,0 +1,7 @@ +{ + "package_name": "", + "package_description": "", + "package_type": "", + "package_doc_md": "", + "package_features": [] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 46c5736..89f38fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ Flask requests Markdown + +Django +djangorestframework diff --git a/static/css/map.css b/static/css/map.css new file mode 100644 index 0000000..368d6dd --- /dev/null +++ b/static/css/map.css @@ -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);} +} \ No newline at end of file diff --git a/static/dungeon.png b/static/dungeon.png new file mode 100644 index 0000000..1a46a7f Binary files /dev/null and b/static/dungeon.png differ diff --git a/static/js/map.js b/static/js/map.js new file mode 100644 index 0000000..8a98418 --- /dev/null +++ b/static/js/map.js @@ -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 = 'general pin'; + } + if (pin.pin_type == "town"){ + el.innerHTML = 'town pin'; + } + if (pin.pin_type == "dungeon"){ + el.innerHTML = '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 = ` + ${idx+1} + ${escapeHTML(pin.label)} + ${pin.url} + ${pin.x.toFixed(2)} + ${pin.y.toFixed(2)} + + + + + + `; + tbody.appendChild(tr); + }); +} +function escapeHTML(str) { + // Prevent breaking the DOM if user enters < > or & etc + return (str||'').replace(/[&<>"'`]/g, s => ({ + '&':'&', '<':'<', '>':'>', '"':'"', "'":''', '`':'`' + }[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; +}); \ No newline at end of file diff --git a/static/map.png b/static/map.png new file mode 100644 index 0000000..09c5468 Binary files /dev/null and b/static/map.png differ diff --git a/static/pin.svg b/static/pin.svg new file mode 100644 index 0000000..2c9e1f7 --- /dev/null +++ b/static/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/town.png b/static/town.png new file mode 100644 index 0000000..de9a630 Binary files /dev/null and b/static/town.png differ diff --git a/test.py b/test.py deleted file mode 100644 index ff96dea..0000000 --- a/test.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/test_character.json b/test_character.json new file mode 100644 index 0000000..d772430 --- /dev/null +++ b/test_character.json @@ -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"} +} diff --git a/webserver.py b/webserver.py deleted file mode 100644 index 77679e5..0000000 --- a/webserver.py +++ /dev/null @@ -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/', 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//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/', 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//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) \ No newline at end of file