Started implementing requirements into code...
This commit is contained in:
parent
82487a417c
commit
c1ad629e79
47
AGENTS.md
47
AGENTS.md
@ -1,28 +1,25 @@
|
|||||||
# AGENTS.md: Guidelines for Agent Coders (2025)
|
# AGENTS.md: Agent Coding Guidelines (2025)
|
||||||
|
|
||||||
## Build/Lint/Test
|
## Build, Lint, and Test Commands
|
||||||
- Main entry: `python main.py`
|
- Main entry point: `python main.py`
|
||||||
- No test suite found; to run a single test (if using pytest):
|
- Start dev server: `python manage.py runserver`
|
||||||
- `pytest test_file.py::test_func`
|
- Run all tests: `pytest main/tests.py`
|
||||||
- Lint (if installed): `flake8 .` or `pylint main.py`
|
- Run a single test: `pytest main/tests.py::test_func`
|
||||||
- Add to `requirements.txt` if you introduce dependencies.
|
- Lint code (if installed): `flake8 .` or `pylint main.py`
|
||||||
- No shell/env scripts found by default.
|
- Add new dependencies: update `requirements.txt`
|
||||||
|
|
||||||
## Python Code Style
|
## Python & Django Style Guide
|
||||||
- Follow [PEP8](https://pep8.org/) (4 spaces, ≤79 chars/line)
|
- Follow [PEP8](https://pep8.org/): 4 spaces/indent, ≤79 chars/line
|
||||||
- Import order: stdlib, third-party, project/local
|
- Import order: stdlib, third-party, then project/local modules
|
||||||
- Never use wildcard imports; always explicit
|
- Use explicit imports; do NOT use wildcard imports
|
||||||
- Naming: snake_case for vars/functions, PascalCase for classes, UPPER_SNAKE_CASE for constants
|
- Naming: snake_case (vars/functions), PascalCase (classes), UPPER_SNAKE_CASE (constants)
|
||||||
- Add type annotations and docstrings to all public APIs/classes
|
- All public classes/APIs require type annotations and docstrings
|
||||||
- Handle errors with try/except; log or re-raise as needed
|
- Django models/views should have descriptive docstrings
|
||||||
- One statement per line; trim trailing whitespace
|
- Handle errors with try/except; log, re-raise, or message as appropriate
|
||||||
- Avoid global state. Organize code into functions/classes.
|
- One statement per line, trim trailing whitespace
|
||||||
- Use `if __name__ == '__main__'` to guard scripts.
|
- 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 and evolve as practices change)
|
||||||
|
|
||||||
(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.
|
|
||||||
|
|||||||
157
character_template.json
Normal file
157
character_template.json
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"level": 0,
|
||||||
|
"race": "",
|
||||||
|
"subrace": "",
|
||||||
|
"class": "",
|
||||||
|
"subclass": "",
|
||||||
|
"background": "",
|
||||||
|
"alignment": "",
|
||||||
|
"size": "",
|
||||||
|
"age": 0,
|
||||||
|
"gender": "",
|
||||||
|
"height": "",
|
||||||
|
"weight": 0,
|
||||||
|
"deity": "",
|
||||||
|
|
||||||
|
"strength_base": 0,
|
||||||
|
"dexterity_base": 0,
|
||||||
|
"constitution_base": 0,
|
||||||
|
"intelligence_base": 0,
|
||||||
|
"wisdom_base": 0,
|
||||||
|
"charisma_base": 0,
|
||||||
|
"armor_base": 0,
|
||||||
|
|
||||||
|
"strength": 0,
|
||||||
|
"dexterity": 0,
|
||||||
|
"constitution": 0,
|
||||||
|
"intelligence": 0,
|
||||||
|
"wisdom": 0,
|
||||||
|
"charisma": 0,
|
||||||
|
"armor": 0,
|
||||||
|
|
||||||
|
"strength_modifier": 0,
|
||||||
|
"dexterity_modifier": 0,
|
||||||
|
"constitution_modifier": 0,
|
||||||
|
"intelligence_modifier": 0,
|
||||||
|
"wisdom_modifier": 0,
|
||||||
|
"charisma_modifier": 0,
|
||||||
|
|
||||||
|
"proficiency": 0,
|
||||||
|
"inspiration": false,
|
||||||
|
"experience": 0,
|
||||||
|
|
||||||
|
"proficiencies_armor": [],
|
||||||
|
"proficiencies_weapons": [],
|
||||||
|
"proficiencies_tools": [],
|
||||||
|
"languages": [],
|
||||||
|
|
||||||
|
"strength_save": 0,
|
||||||
|
"dexterity_save": 0,
|
||||||
|
"constitution_save": 0,
|
||||||
|
"intelligence_save": 0,
|
||||||
|
"wisdom_save": 0,
|
||||||
|
"charisma_save": 0,
|
||||||
|
|
||||||
|
"hp": 0,
|
||||||
|
"hp_max": 0,
|
||||||
|
"hp_temp": 0,
|
||||||
|
"hit_die": "",
|
||||||
|
"initiative": 0,
|
||||||
|
|
||||||
|
"deathsaves_successes": 0,
|
||||||
|
"deathsaves_failures": 0,
|
||||||
|
|
||||||
|
"fire_resistance": false,
|
||||||
|
"poison_resistance": false,
|
||||||
|
"psychic_resistance": false,
|
||||||
|
"cold_resistance": false,
|
||||||
|
"thunder_resistance": false,
|
||||||
|
"acid_resistance": false,
|
||||||
|
"force_resistance": false,
|
||||||
|
"radiant_resistance": false,
|
||||||
|
"necrotic_resistance": false,
|
||||||
|
"bludgeoning_resistance": false,
|
||||||
|
"piercing_resistance": false,
|
||||||
|
"slashing_resistance": false,
|
||||||
|
"immunities": [],
|
||||||
|
"vulnerabilities": [],
|
||||||
|
|
||||||
|
"speed_base": 0,
|
||||||
|
"speed_type": [],
|
||||||
|
"darkvision": 0,
|
||||||
|
"blindsight": 0,
|
||||||
|
"tremorsense": 0,
|
||||||
|
"truesight": 0,
|
||||||
|
|
||||||
|
"athletics": 0,
|
||||||
|
"acrobatics": 0,
|
||||||
|
"sleight_of_hand": 0,
|
||||||
|
"stealth": 0,
|
||||||
|
"arcana": 0,
|
||||||
|
"history": 0,
|
||||||
|
"investigation": 0,
|
||||||
|
"nature": 0,
|
||||||
|
"religion": 0,
|
||||||
|
"animal_handling": 0,
|
||||||
|
"insight": 0,
|
||||||
|
"medicine": 0,
|
||||||
|
"perception": 0,
|
||||||
|
"survival": 0,
|
||||||
|
"deception": 0,
|
||||||
|
"intimidation": 0,
|
||||||
|
"performance": 0,
|
||||||
|
"persuasion": 0,
|
||||||
|
|
||||||
|
"athletics_passive": 0,
|
||||||
|
"acrobatics_passive": 0,
|
||||||
|
"sleight_of_hand_passive": 0,
|
||||||
|
"stealth_passive": 0,
|
||||||
|
"arcana_passive": 0,
|
||||||
|
"history_passive": 0,
|
||||||
|
"investigation_passive": 0,
|
||||||
|
"nature_passive": 0,
|
||||||
|
"religion_passive": 0,
|
||||||
|
"animal_handling_passive": 0,
|
||||||
|
"insight_passive": 0,
|
||||||
|
"medicine_passive": 0,
|
||||||
|
"perception_passive": 0,
|
||||||
|
"survival_passive": 0,
|
||||||
|
"deception_passive": 0,
|
||||||
|
"intimidation_passive": 0,
|
||||||
|
"performance_passive": 0,
|
||||||
|
"persuasion_passive": 0,
|
||||||
|
"passive_perception": 0,
|
||||||
|
"passive_investigation": 0,
|
||||||
|
"passive_insight": 0,
|
||||||
|
|
||||||
|
"spellcasting_ability": "",
|
||||||
|
"spell_save_dc": 0,
|
||||||
|
"spell_attack_bonus": 0,
|
||||||
|
"known_spells": [],
|
||||||
|
"prepared_spells": [],
|
||||||
|
"spell_slots": {},
|
||||||
|
"cantrips": [],
|
||||||
|
"spellcasting_class": "",
|
||||||
|
|
||||||
|
"exhaustion_level": 0,
|
||||||
|
"conditions_active": [],
|
||||||
|
|
||||||
|
"cp": 0,
|
||||||
|
"sp": 0,
|
||||||
|
"ep": 0,
|
||||||
|
"gp": 0,
|
||||||
|
"pp": 0,
|
||||||
|
"equipment": [],
|
||||||
|
|
||||||
|
"packages": [],
|
||||||
|
"features": [],
|
||||||
|
"traits": [],
|
||||||
|
"personality_traits": [],
|
||||||
|
"ideals": [],
|
||||||
|
"bonds": [],
|
||||||
|
"flaws": [],
|
||||||
|
|
||||||
|
"notes": "",
|
||||||
|
"custom_attributes": {}
|
||||||
|
}
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
7
feature_template.json
Normal file
7
feature_template.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"feature_name": "",
|
||||||
|
"feature_description": "",
|
||||||
|
"feature_data": {
|
||||||
|
"effects": []
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
lisiumsite/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lisiumsite/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
lisiumsite/__pycache__/settings.cpython-314.pyc
Normal file
BIN
lisiumsite/__pycache__/settings.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lisiumsite/__pycache__/urls.cpython-314.pyc
Normal file
BIN
lisiumsite/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lisiumsite/__pycache__/wsgi.cpython-314.pyc
Normal file
BIN
lisiumsite/__pycache__/wsgi.cpython-314.pyc
Normal file
Binary file not shown.
@ -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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = ['192.168.1.45', '127.0.0.1']
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'main',
|
'main',
|
||||||
|
'rest_framework',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -81,6 +82,9 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Use custom user model with uuid field
|
||||||
|
AUTH_USER_MODEL = 'main.CustomUser'
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@ -115,4 +119,7 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
# 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
|
||||||
|
]
|
||||||
|
|||||||
BIN
main/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
main/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
main/__pycache__/admin.cpython-314.pyc
Normal file
BIN
main/__pycache__/admin.cpython-314.pyc
Normal file
Binary file not shown.
BIN
main/__pycache__/apps.cpython-314.pyc
Normal file
BIN
main/__pycache__/apps.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
main/__pycache__/models.cpython-314.pyc
Normal file
BIN
main/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
main/__pycache__/serializers.cpython-313.pyc
Normal file
BIN
main/__pycache__/serializers.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/__pycache__/serializers.cpython-314.pyc
Normal file
BIN
main/__pycache__/serializers.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
main/__pycache__/urls.cpython-314.pyc
Normal file
BIN
main/__pycache__/urls.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
main/__pycache__/views.cpython-314.pyc
Normal file
BIN
main/__pycache__/views.cpython-314.pyc
Normal file
Binary file not shown.
@ -1,3 +1,42 @@
|
|||||||
from django.contrib import admin
|
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')
|
||||||
|
|||||||
231
main/migrations/0001_initial.py
Normal file
231
main/migrations/0001_initial.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 02:30
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Feature',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('feature_name', models.CharField(max_length=200)),
|
||||||
|
('feature_description', models.TextField(blank=True)),
|
||||||
|
('feature_data', models.JSONField(blank=True, default=dict)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Package',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('package_name', models.CharField(max_length=200)),
|
||||||
|
('package_description', models.TextField(blank=True)),
|
||||||
|
('package_type', models.CharField(blank=True, max_length=100)),
|
||||||
|
('package_doc_md', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Character',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('level', models.IntegerField(default=0)),
|
||||||
|
('alignment', models.CharField(blank=True, max_length=50)),
|
||||||
|
('size', models.CharField(blank=True, max_length=20)),
|
||||||
|
('age', models.IntegerField(default=0)),
|
||||||
|
('gender', models.CharField(blank=True, max_length=30)),
|
||||||
|
('height', models.CharField(blank=True, max_length=20)),
|
||||||
|
('weight', models.IntegerField(default=0)),
|
||||||
|
('deity', models.CharField(blank=True, max_length=100)),
|
||||||
|
('strength_base', models.IntegerField(default=0)),
|
||||||
|
('dexterity_base', models.IntegerField(default=0)),
|
||||||
|
('constitution_base', models.IntegerField(default=0)),
|
||||||
|
('intelligence_base', models.IntegerField(default=0)),
|
||||||
|
('wisdom_base', models.IntegerField(default=0)),
|
||||||
|
('charisma_base', models.IntegerField(default=0)),
|
||||||
|
('armor_base', models.IntegerField(default=0)),
|
||||||
|
('strength', models.IntegerField(default=0)),
|
||||||
|
('dexterity', models.IntegerField(default=0)),
|
||||||
|
('constitution', models.IntegerField(default=0)),
|
||||||
|
('intelligence', models.IntegerField(default=0)),
|
||||||
|
('wisdom', models.IntegerField(default=0)),
|
||||||
|
('charisma', models.IntegerField(default=0)),
|
||||||
|
('armor', models.IntegerField(default=0)),
|
||||||
|
('strength_modifier', models.IntegerField(default=0)),
|
||||||
|
('dexterity_modifier', models.IntegerField(default=0)),
|
||||||
|
('constitution_modifier', models.IntegerField(default=0)),
|
||||||
|
('intelligence_modifier', models.IntegerField(default=0)),
|
||||||
|
('wisdom_modifier', models.IntegerField(default=0)),
|
||||||
|
('charisma_modifier', models.IntegerField(default=0)),
|
||||||
|
('proficiency', models.IntegerField(default=0)),
|
||||||
|
('inspiration', models.BooleanField(default=False)),
|
||||||
|
('experience', models.IntegerField(default=0)),
|
||||||
|
('proficiencies_armor', models.JSONField(blank=True, default=list)),
|
||||||
|
('proficiencies_weapons', models.JSONField(blank=True, default=list)),
|
||||||
|
('proficiencies_tools', models.JSONField(blank=True, default=list)),
|
||||||
|
('languages', models.JSONField(blank=True, default=list)),
|
||||||
|
('strength_save', models.IntegerField(default=0)),
|
||||||
|
('dexterity_save', models.IntegerField(default=0)),
|
||||||
|
('constitution_save', models.IntegerField(default=0)),
|
||||||
|
('intelligence_save', models.IntegerField(default=0)),
|
||||||
|
('wisdom_save', models.IntegerField(default=0)),
|
||||||
|
('charisma_save', models.IntegerField(default=0)),
|
||||||
|
('hp', models.IntegerField(default=0)),
|
||||||
|
('hp_max', models.IntegerField(default=0)),
|
||||||
|
('hp_temp', models.IntegerField(default=0)),
|
||||||
|
('hit_die', models.CharField(blank=True, max_length=20)),
|
||||||
|
('initiative', models.IntegerField(default=0)),
|
||||||
|
('deathsaves_successes', models.IntegerField(default=0)),
|
||||||
|
('deathsaves_failures', models.IntegerField(default=0)),
|
||||||
|
('fire_resistance', models.BooleanField(default=False)),
|
||||||
|
('poison_resistance', models.BooleanField(default=False)),
|
||||||
|
('psychic_resistance', models.BooleanField(default=False)),
|
||||||
|
('cold_resistance', models.BooleanField(default=False)),
|
||||||
|
('thunder_resistance', models.BooleanField(default=False)),
|
||||||
|
('acid_resistance', models.BooleanField(default=False)),
|
||||||
|
('force_resistance', models.BooleanField(default=False)),
|
||||||
|
('radiant_resistance', models.BooleanField(default=False)),
|
||||||
|
('necrotic_resistance', models.BooleanField(default=False)),
|
||||||
|
('bludgeoning_resistance', models.BooleanField(default=False)),
|
||||||
|
('piercing_resistance', models.BooleanField(default=False)),
|
||||||
|
('slashing_resistance', models.BooleanField(default=False)),
|
||||||
|
('immunities', models.JSONField(blank=True, default=list)),
|
||||||
|
('vulnerabilities', models.JSONField(blank=True, default=list)),
|
||||||
|
('speed_base', models.IntegerField(default=0)),
|
||||||
|
('speed_type', models.JSONField(blank=True, default=list)),
|
||||||
|
('darkvision', models.IntegerField(default=0)),
|
||||||
|
('blindsight', models.IntegerField(default=0)),
|
||||||
|
('tremorsense', models.IntegerField(default=0)),
|
||||||
|
('truesight', models.IntegerField(default=0)),
|
||||||
|
('athletics', models.IntegerField(default=0)),
|
||||||
|
('acrobatics', models.IntegerField(default=0)),
|
||||||
|
('sleight_of_hand', models.IntegerField(default=0)),
|
||||||
|
('stealth', models.IntegerField(default=0)),
|
||||||
|
('arcana', models.IntegerField(default=0)),
|
||||||
|
('history', models.IntegerField(default=0)),
|
||||||
|
('investigation', models.IntegerField(default=0)),
|
||||||
|
('nature', models.IntegerField(default=0)),
|
||||||
|
('religion', models.IntegerField(default=0)),
|
||||||
|
('animal_handling', models.IntegerField(default=0)),
|
||||||
|
('insight', models.IntegerField(default=0)),
|
||||||
|
('medicine', models.IntegerField(default=0)),
|
||||||
|
('perception', models.IntegerField(default=0)),
|
||||||
|
('survival', models.IntegerField(default=0)),
|
||||||
|
('deception', models.IntegerField(default=0)),
|
||||||
|
('intimidation', models.IntegerField(default=0)),
|
||||||
|
('performance', models.IntegerField(default=0)),
|
||||||
|
('persuasion', models.IntegerField(default=0)),
|
||||||
|
('athletics_passive', models.IntegerField(default=0)),
|
||||||
|
('acrobatics_passive', models.IntegerField(default=0)),
|
||||||
|
('sleight_of_hand_passive', models.IntegerField(default=0)),
|
||||||
|
('stealth_passive', models.IntegerField(default=0)),
|
||||||
|
('arcana_passive', models.IntegerField(default=0)),
|
||||||
|
('history_passive', models.IntegerField(default=0)),
|
||||||
|
('investigation_passive', models.IntegerField(default=0)),
|
||||||
|
('nature_passive', models.IntegerField(default=0)),
|
||||||
|
('religion_passive', models.IntegerField(default=0)),
|
||||||
|
('animal_handling_passive', models.IntegerField(default=0)),
|
||||||
|
('insight_passive', models.IntegerField(default=0)),
|
||||||
|
('medicine_passive', models.IntegerField(default=0)),
|
||||||
|
('perception_passive', models.IntegerField(default=0)),
|
||||||
|
('survival_passive', models.IntegerField(default=0)),
|
||||||
|
('deception_passive', models.IntegerField(default=0)),
|
||||||
|
('intimidation_passive', models.IntegerField(default=0)),
|
||||||
|
('performance_passive', models.IntegerField(default=0)),
|
||||||
|
('persuasion_passive', models.IntegerField(default=0)),
|
||||||
|
('passive_perception', models.IntegerField(default=0)),
|
||||||
|
('passive_investigation', models.IntegerField(default=0)),
|
||||||
|
('passive_insight', models.IntegerField(default=0)),
|
||||||
|
('spellcasting_ability', models.CharField(blank=True, max_length=50)),
|
||||||
|
('spell_save_dc', models.IntegerField(default=0)),
|
||||||
|
('spell_attack_bonus', models.IntegerField(default=0)),
|
||||||
|
('known_spells', models.JSONField(blank=True, default=list)),
|
||||||
|
('prepared_spells', models.JSONField(blank=True, default=list)),
|
||||||
|
('spell_slots', models.JSONField(blank=True, default=dict)),
|
||||||
|
('cantrips', models.JSONField(blank=True, default=list)),
|
||||||
|
('spellcasting_class', models.CharField(blank=True, max_length=100)),
|
||||||
|
('exhaustion_level', models.IntegerField(default=0)),
|
||||||
|
('conditions_active', models.JSONField(blank=True, default=list)),
|
||||||
|
('cp', models.IntegerField(default=0)),
|
||||||
|
('sp', models.IntegerField(default=0)),
|
||||||
|
('ep', models.IntegerField(default=0)),
|
||||||
|
('gp', models.IntegerField(default=0)),
|
||||||
|
('pp', models.IntegerField(default=0)),
|
||||||
|
('equipment', models.JSONField(blank=True, default=list)),
|
||||||
|
('features', models.JSONField(blank=True, default=list)),
|
||||||
|
('traits', models.JSONField(blank=True, default=list)),
|
||||||
|
('personality_traits', models.JSONField(blank=True, default=list)),
|
||||||
|
('ideals', models.JSONField(blank=True, default=list)),
|
||||||
|
('bonds', models.JSONField(blank=True, default=list)),
|
||||||
|
('flaws', models.JSONField(blank=True, default=list)),
|
||||||
|
('notes', models.TextField(blank=True)),
|
||||||
|
('custom_attributes', models.JSONField(blank=True, default=dict)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('background', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='background_characters', to='main.package')),
|
||||||
|
('char_class', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='class_characters', to='main.package')),
|
||||||
|
('packages', models.ManyToManyField(blank=True, related_name='characters', to='main.package')),
|
||||||
|
('race', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='race_characters', to='main.package')),
|
||||||
|
('subclass', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subclass_characters', to='main.package')),
|
||||||
|
('subrace', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subrace_characters', to='main.package')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PackageFeature',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('priority', models.IntegerField(default=0)),
|
||||||
|
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.feature')),
|
||||||
|
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.package')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['priority'],
|
||||||
|
'unique_together': {('package', 'feature')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='features',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='packages', through='main.PackageFeature', to='main.feature'),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
main/migrations/0002_pin.py
Normal file
23
main/migrations/0002_pin.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 16:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Pin',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('label', models.CharField(max_length=100)),
|
||||||
|
('url', models.URLField(max_length=300)),
|
||||||
|
('x', models.FloatField(help_text='X position as percentage (0-100)')),
|
||||||
|
('y', models.FloatField(help_text='Y position as percentage (0-100)')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0003_pin_pin_type.py
Normal file
18
main/migrations/0003_pin_pin_type.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 18:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0002_pin'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='pin',
|
||||||
|
name='pin_type',
|
||||||
|
field=models.CharField(default='general', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
main/migrations/0006_remove_character_athletics.py
Normal file
17
main/migrations/0006_remove_character_athletics.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 21:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0005_remove_character_armor_remove_character_charisma_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='athletics',
|
||||||
|
),
|
||||||
|
]
|
||||||
165
main/migrations/0007_remove_character_acrobatics_and_more.py
Normal file
165
main/migrations/0007_remove_character_acrobatics_and_more.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 22:20
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0006_remove_character_athletics'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='acrobatics',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='acrobatics_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='animal_handling',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='animal_handling_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='arcana',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='arcana_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='athletics_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='deception',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='deception_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='history',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='history_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='insight',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='insight_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='intimidation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='intimidation_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='investigation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='investigation_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='medicine',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='medicine_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='nature',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='nature_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='passive_insight',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='passive_investigation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='passive_perception',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='perception',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='perception_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='performance',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='performance_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='persuasion',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='persuasion_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='religion',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='religion_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='sleight_of_hand',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='sleight_of_hand_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='stealth',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='stealth_passive',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='survival',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='character',
|
||||||
|
name='survival_passive',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0009_character_hp_features.py
Normal file
18
main/migrations/0009_character_hp_features.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 23:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0008_remove_character_hp_max_remove_character_initiative'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='character',
|
||||||
|
name='hp_features',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0010_alter_character_hp_features.py
Normal file
18
main/migrations/0010_alter_character_hp_features.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-14 23:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0009_character_hp_features'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='character',
|
||||||
|
name='hp_features',
|
||||||
|
field=models.JSONField(blank=True, default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0011_feature_feature_requirements.py
Normal file
18
main/migrations/0011_feature_feature_requirements.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-15 00:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0010_alter_character_hp_features'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='feature',
|
||||||
|
name='feature_requirements',
|
||||||
|
field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0012_packagefeature_requirements_override.py
Normal file
18
main/migrations/0012_packagefeature_requirements_override.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-15 00:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0011_feature_feature_requirements'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='packagefeature',
|
||||||
|
name='requirements_override',
|
||||||
|
field=models.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0013_alter_feature_feature_requirements.py
Normal file
18
main/migrations/0013_alter_feature_feature_requirements.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-15 01:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0012_packagefeature_requirements_override'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feature',
|
||||||
|
name='feature_requirements',
|
||||||
|
field=models.JSONField(default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
main/migrations/0016_alter_feature_feature_requirements.py
Normal file
18
main/migrations/0016_alter_feature_feature_requirements.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.5 on 2026-05-06 23:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0015_alter_character_features_alter_character_packages'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feature',
|
||||||
|
name='feature_requirements',
|
||||||
|
field=models.JSONField(blank=True, default=list),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
main/migrations/__pycache__/0001_initial.cpython-313.pyc
Normal file
BIN
main/migrations/__pycache__/0001_initial.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/migrations/__pycache__/0001_initial.cpython-314.pyc
Normal file
BIN
main/migrations/__pycache__/0001_initial.cpython-314.pyc
Normal file
Binary file not shown.
BIN
main/migrations/__pycache__/0002_character.cpython-313.pyc
Normal file
BIN
main/migrations/__pycache__/0002_character.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/migrations/__pycache__/0002_pin.cpython-313.pyc
Normal file
BIN
main/migrations/__pycache__/0002_pin.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/migrations/__pycache__/0002_pin.cpython-314.pyc
Normal file
BIN
main/migrations/__pycache__/0002_pin.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
main/migrations/__pycache__/0003_pin_pin_type.cpython-313.pyc
Normal file
BIN
main/migrations/__pycache__/0003_pin_pin_type.cpython-313.pyc
Normal file
Binary file not shown.
BIN
main/migrations/__pycache__/0003_pin_pin_type.cpython-314.pyc
Normal file
BIN
main/migrations/__pycache__/0003_pin_pin_type.cpython-314.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
main/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
main/migrations/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
629
main/models.py
629
main/models.py
@ -1,3 +1,632 @@
|
|||||||
from django.db import models
|
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.
|
# Create your models here.
|
||||||
|
class Pin(models.Model):
|
||||||
|
label = models.CharField(max_length=100)
|
||||||
|
url = models.URLField(max_length=300)
|
||||||
|
x = models.FloatField(help_text="X position as percentage (0-100)")
|
||||||
|
y = models.FloatField(help_text="Y position as percentage (0-100)")
|
||||||
|
pin_type = models.CharField(max_length=100, default="general")
|
||||||
|
def as_dict(self):
|
||||||
|
return {
|
||||||
|
"label": self.label,
|
||||||
|
"url": self.url,
|
||||||
|
"x": self.x,
|
||||||
|
"y": self.y,
|
||||||
|
"pin_type": self.pin_type,
|
||||||
|
}
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.label} ({self.x:.2f}%, {self.y:.2f}%)"
|
||||||
7
main/serializers.py
Normal file
7
main/serializers.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import Pin
|
||||||
|
|
||||||
|
class PinSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Pin
|
||||||
|
fields = ['id', 'label', 'url', 'x', 'y', 'pin_type']
|
||||||
290
main/templates/main/character_sheet.html
Normal file
290
main/templates/main/character_sheet.html
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>D&D 5e Full Character Sheet</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
|
||||||
|
<style>
|
||||||
|
body { background: #f4f4f4; }
|
||||||
|
.sheet {
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
.uk-card {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.uk-table th, .uk-table td { text-align: center; }
|
||||||
|
.dice { font-family:monospace; }
|
||||||
|
.uk-label { margin-right: 0.2em; }
|
||||||
|
.quickfacts label { font-weight: bold; margin-right: 0.25em; }
|
||||||
|
.info-table-card { overflow-x: auto; }
|
||||||
|
.uk-table { margin-bottom: 0; }
|
||||||
|
.uk-table th, .uk-table td { text-align: center; }
|
||||||
|
.dice { font-family:monospace; }
|
||||||
|
.uk-label { margin-right: 0.2em; }
|
||||||
|
.quickfacts label { font-weight: bold; margin-right: 0.25em; }
|
||||||
|
.info-table-card { overflow-x: auto; }
|
||||||
|
.uk-table { margin-bottom: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sheet">
|
||||||
|
<!-- Main Header -->
|
||||||
|
<div class="uk-card uk-card-default uk-card-body">
|
||||||
|
<h1 class="uk-text-center uk-margin-remove-bottom">
|
||||||
|
<span uk-icon="users"></span> {{ character.name }}
|
||||||
|
<span class="uk-text-meta uk-margin-small-left">Level {{ character.level }}</span>
|
||||||
|
</h1>
|
||||||
|
<hr class="uk-divider-icon uk-margin-small-top uk-margin-bottom">
|
||||||
|
<div class="uk-grid-small uk-child-width-1-2@s uk-child-width-1-4@m" uk-grid>
|
||||||
|
<div><label>Race:</label> <span class="uk-badge uk-margin-small-right">{{ character.race }}</span> {{ character.subrace }}</div>
|
||||||
|
<div><label>Class:</label> {{ character.char_class.package_name }} {{ character.subclass }}</div>
|
||||||
|
<div><label>Background:</label> {{ character.background }}</div>
|
||||||
|
<div><label>Alignment:</label> {{ character.alignment }}</div>
|
||||||
|
<div><label>Size:</label> {{ character.size }}</div>
|
||||||
|
<div><label>Age:</label> {{ character.age }}</div>
|
||||||
|
<div><label>Gender:</label> {{ character.gender }}</div>
|
||||||
|
<div><label>Height:</label> {{ character.height }}</div>
|
||||||
|
<div><label>Weight:</label> {{ character.weight }} lbs</div>
|
||||||
|
<div><label>Deity:</label> {{ character.deity }}</div>
|
||||||
|
<div><label>XP:</label> {{ character.experience }}</div>
|
||||||
|
<div>
|
||||||
|
<label>Inspiration:</label>
|
||||||
|
{% if character.inspiration %}
|
||||||
|
<span class="uk-label uk-label-warning" uk-icon="star"> {{ character.inspiration }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="uk-text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Core Attrs: Full-width Card to prevent table overflows -->
|
||||||
|
<div class="uk-card uk-card-default uk-card-body info-table-card">
|
||||||
|
<h3 class="uk-card-title"><span uk-icon="settings"></span> Core Attributes</h3>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table class="uk-table uk-table-divider uk-table-small uk-table-striped uk-table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr><th></th><th>Score</th><th>Modifier</th><th>Save</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><th>STR</th><td>{{ character.strength }}</td><td>{{ character.strength_modifier }}</td><td>{{ character.strength_save }}</td></tr>
|
||||||
|
<tr><th>DEX</th><td>{{ character.dexterity }}</td><td>{{ character.dexterity_modifier }}</td><td>{{ character.dexterity_save }}</td></tr>
|
||||||
|
<tr><th>CON</th><td>{{ character.constitution }}</td><td>{{ character.constitution_modifier }}</td><td>{{ character.constitution_save }}</td></tr>
|
||||||
|
<tr><th>INT</th><td>{{ character.intelligence }}</td><td>{{ character.intelligence_modifier }}</td><td>{{ character.intelligence_save }}</td></tr>
|
||||||
|
<tr><th>WIS</th><td>{{ character.wisdom }}</td><td>{{ character.wisdom_modifier }}</td><td>{{ character.wisdom_save }}</td></tr>
|
||||||
|
<tr><th>CHA</th><td>{{ character.charisma }}</td><td>{{ character.charisma_modifier }}</td><td>{{ character.charisma_save }}</td></tr>
|
||||||
|
<tr><th>Armor</th><td>{{ character.armor_base }}</td><td>-</td><td>-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin uk-child-width-1-3@m" uk-grid>
|
||||||
|
<div><span class="uk-label">Proficiency</span> {{ character.proficiency }}</div>
|
||||||
|
<div><span class="uk-label">Initiative</span> {{ character.initiative }}</div>
|
||||||
|
<div><span class="uk-label">Speed</span> {{ character.speed_base }} {% if character.speed_type %}<span class="uk-text-meta">({{ character.speed_type|join:', ' }})</span>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Secondary Info Cards Grouped in a Responsive Grid -->
|
||||||
|
<div class="uk-grid-match uk-child-width-1-2@s uk-child-width-1-3@m uk-margin-bottom" uk-grid>
|
||||||
|
<div>
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<h3 class="uk-card-title"><span uk-icon="lifesaver"></span> HP & Status</h3>
|
||||||
|
<ul class="uk-list uk-list-small">
|
||||||
|
<li><span class="uk-label uk-label-danger">HP</span> {{ character.hp }} / {{ character.hp_max }} {% if character.hp_temp %}(+{{ character.hp_temp }} temp){% endif %}</li>
|
||||||
|
<li><span class="uk-label">Hit Die</span> <span class="dice">{{ character.hit_die }}</span></li>
|
||||||
|
<li><span class="uk-label">Death Saves</span> {{ character.deathsaves_successes }}✓/{{ character.deathsaves_failures }}✗</li>
|
||||||
|
<li><span class="uk-label">Exhaustion</span> {% if character.exhaustion_level > 0 %}<span class="uk-label uk-label-warning">{{ character.exhaustion_level }}</span>{% else %}<span class="uk-text-muted">0</span>{% endif %}</li>
|
||||||
|
<li><span class="uk-label">Active Conditions</span> {% for cond in character.conditions_active %}{{ cond }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<h3 class="uk-card-title"><span uk-icon="heart"></span> Resistances & Immunities</h3>
|
||||||
|
<ul class="uk-list uk-list-striped uk-list-small">
|
||||||
|
<li><b>Immunities:</b> {% for i in character.immunities %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
<li><b>Vulnerabilities:</b> {% for v in character.vulnerabilities %}{{ v }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
<li><b>Resistances:</b>
|
||||||
|
{% if character.fire_resistance %}Fire {% endif %}
|
||||||
|
{% if character.poison_resistance %}Poison {% endif %}
|
||||||
|
{% if character.psychic_resistance %}Psychic {% endif %}
|
||||||
|
{% if character.cold_resistance %}Cold {% endif %}
|
||||||
|
{% if character.thunder_resistance %}Thunder {% endif %}
|
||||||
|
{% if character.acid_resistance %}Acid {% endif %}
|
||||||
|
{% if character.force_resistance %}Force {% endif %}
|
||||||
|
{% if character.radiant_resistance %}Radiant {% endif %}
|
||||||
|
{% if character.necrotic_resistance %}Necrotic {% endif %}
|
||||||
|
{% if character.bludgeoning_resistance %}Bludgeoning {% endif %}
|
||||||
|
{% if character.piercing_resistance %}Piercing {% endif %}
|
||||||
|
{% if character.slashing_resistance %}Slashing {% endif %}
|
||||||
|
{% if not character.fire_resistance and not character.poison_resistance and not character.psychic_resistance and not character.cold_resistance and not character.thunder_resistance and not character.acid_resistance and not character.force_resistance and not character.radiant_resistance and not character.necrotic_resistance and not character.bludgeoning_resistance and not character.piercing_resistance and not character.slashing_resistance %}
|
||||||
|
<span class="uk-text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<h3 class="uk-card-title"><span uk-icon="search"></span> Senses</h3>
|
||||||
|
<ul class="uk-list uk-list-bullet uk-list-small">
|
||||||
|
<li>Darkvision: <span class="uk-badge">{{ character.darkvision }}</span></li>
|
||||||
|
<li>Blindsight: <span class="uk-badge">{{ character.blindsight }}</span></li>
|
||||||
|
<li>Tremorsense: <span class="uk-badge">{{ character.tremorsense }}</span></li>
|
||||||
|
<li>Truesight: <span class="uk-badge">{{ character.truesight }}</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<h3 class="uk-card-title"><span uk-icon="future"></span> Proficiencies & Languages</h3>
|
||||||
|
<ul class="uk-list uk-list-bullet uk-list-small">
|
||||||
|
<li><span class="uk-label">Armor:</span> {% for i in character.proficiencies_armor %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
<li><span class="uk-label">Weapons:</span> {% for i in character.proficiencies_weapons %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
<li><span class="uk-label">Tools:</span> {% for i in character.proficiencies_tools %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
<li><span class="uk-label">Languages:</span> {% for i in character.languages %}{{ i }}{% if not forloop.last %}, {% endif %}{% empty %}<span class="uk-text-muted">None</span>{% endfor %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Section Tabs With Switchers -->
|
||||||
|
<div class="uk-card uk-card-default uk-card-body">
|
||||||
|
<ul class="uk-tab uk-child-width-expand uk-margin-remove-bottom" uk-tab>
|
||||||
|
<li class="uk-active"><a href="#">Skills</a></li>
|
||||||
|
<li><a href="#">Spellcasting</a></li>
|
||||||
|
<li><a href="#">Equipment & Wealth</a></li>
|
||||||
|
<li><a href="#">Features, Traits & Backstory</a></li>
|
||||||
|
</ul>
|
||||||
|
<ul class="uk-switcher uk-margin">
|
||||||
|
<li>
|
||||||
|
<h3><span uk-icon="bolt"></span> Skills</h3>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table class="uk-table uk-table-small uk-table-hover uk-table-divider">
|
||||||
|
<tr><th>Skill</th><th>Bonus</th><th>Passive</th></tr>
|
||||||
|
<tr><td>Athletics</td><td>{{ character.athletics }}</td><td>{{ character.athletics_passive }}</td></tr>
|
||||||
|
<tr><td>Acrobatics</td><td>{{ character.acrobatics }}</td><td>{{ character.acrobatics_passive }}</td></tr>
|
||||||
|
<tr><td>Sleight of Hand</td><td>{{ character.sleight_of_hand }}</td><td>{{ character.sleight_of_hand_passive }}</td></tr>
|
||||||
|
<tr><td>Stealth</td><td>{{ character.stealth }}</td><td>{{ character.stealth_passive }}</td></tr>
|
||||||
|
<tr><td>Arcana</td><td>{{ character.arcana }}</td><td>{{ character.arcana_passive }}</td></tr>
|
||||||
|
<tr><td>History</td><td>{{ character.history }}</td><td>{{ character.history_passive }}</td></tr>
|
||||||
|
<tr><td>Investigation</td><td>{{ character.investigation }}</td><td>{{ character.investigation_passive }}</td></tr>
|
||||||
|
<tr><td>Nature</td><td>{{ character.nature }}</td><td>{{ character.nature_passive }}</td></tr>
|
||||||
|
<tr><td>Religion</td><td>{{ character.religion }}</td><td>{{ character.religion_passive }}</td></tr>
|
||||||
|
<tr><td>Animal Handling</td><td>{{ character.animal_handling }}</td><td>{{ character.animal_handling_passive }}</td></tr>
|
||||||
|
<tr><td>Insight</td><td>{{ character.insight }}</td><td>{{ character.insight_passive }}</td></tr>
|
||||||
|
<tr><td>Medicine</td><td>{{ character.medicine }}</td><td>{{ character.medicine_passive }}</td></tr>
|
||||||
|
<tr><td>Perception</td><td>{{ character.perception }}</td><td>{{ character.perception_passive }}</td></tr>
|
||||||
|
<tr><td>Survival</td><td>{{ character.survival }}</td><td>{{ character.survival_passive }}</td></tr>
|
||||||
|
<tr><td>Deception</td><td>{{ character.deception }}</td><td>{{ character.deception_passive }}</td></tr>
|
||||||
|
<tr><td>Intimidation</td><td>{{ character.intimidation }}</td><td>{{ character.intimidation_passive }}</td></tr>
|
||||||
|
<tr><td>Performance</td><td>{{ character.performance }}</td><td>{{ character.performance_passive }}</td></tr>
|
||||||
|
<tr><td>Persuasion</td><td>{{ character.persuasion }}</td><td>{{ character.persuasion_passive }}</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="uk-child-width-1-3@m uk-margin" uk-grid>
|
||||||
|
<div><span class="uk-label">Passive Perception</span> {{ character.passive_perception }}</div>
|
||||||
|
<div><span class="uk-label">Passive Investigation</span> {{ character.passive_investigation }}</div>
|
||||||
|
<div><span class="uk-label">Passive Insight</span> {{ character.passive_insight }}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3><span uk-icon="nut"></span> Spellcasting</h3>
|
||||||
|
<div class="uk-child-width-1-2@m" uk-grid>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label">Spellcasting Class:</span> {{ character.spellcasting_class }}<br>
|
||||||
|
<span class="uk-label">Ability:</span> {{ character.spellcasting_ability }}<br>
|
||||||
|
<span class="uk-label">Spell Save DC:</span> {{ character.spell_save_dc }}<br>
|
||||||
|
<span class="uk-label">Spell Attack Bonus:</span> {{ character.spell_attack_bonus }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label">Spell Slots:</span><br>
|
||||||
|
{% for lvl, slots in character.spell_slots.items %}Level {{ lvl }}: {{ slots }}{% if not forloop.last %} | {% endif %}{% empty %}None{% endfor %}<br>
|
||||||
|
<span class="uk-label">Cantrips:</span> {% for spell in character.cantrips %}{{ spell }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="uk-child-width-1-2@m" uk-grid>
|
||||||
|
<div>
|
||||||
|
<label>Known Spells:</label>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for spell in character.known_spells %}<li>{{ spell }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Prepared Spells:</label>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for spell in character.prepared_spells %}<li>{{ spell }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3><span uk-icon="bag"></span> Equipment & Wealth</h3>
|
||||||
|
<div class="uk-child-width-1-2@m" uk-grid>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label uk-label-warning">CP</span> {{ character.cp }}
|
||||||
|
<span class="uk-label uk-label-secondary">SP</span> {{ character.sp }}
|
||||||
|
<span class="uk-label uk-label-success">EP</span> {{ character.ep }}
|
||||||
|
<span class="uk-label uk-label-danger">GP</span> {{ character.gp }}
|
||||||
|
<span class="uk-label uk-label-primary">PP</span> {{ character.pp }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label">Equipment:</span>
|
||||||
|
<ul class="uk-list uk-list-bullet">{% for eq in character.equipment %}<li>{{ eq }}</li>{% empty %}<li>None</li>{% endfor %}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3><span uk-icon="info"></span> Features, Traits & Backstory</h3>
|
||||||
|
<div class="uk-grid uk-child-width-1-2@m" uk-grid>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label">Traits:</span>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for val in character.traits %}<li>{{ val }}</li>{% endfor %}</ul>
|
||||||
|
<span class="uk-label">Personality Traits:</span>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for val in character.personality_traits %}<li>{{ val }}</li>{% endfor %}</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="uk-label">Ideals:</span>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for val in character.ideals %}<li>{{ val }}</li>{% endfor %}</ul>
|
||||||
|
<span class="uk-label">Bonds:</span>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for val in character.bonds %}<li>{{ val }}</li>{% endfor %}</ul>
|
||||||
|
<span class="uk-label">Flaws:</span>
|
||||||
|
<ul class="uk-list uk-list-collapse">{% for val in character.flaws %}<li>{{ val }}</li>{% endfor %}</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div><span class="uk-label">Notes:</span><br>{{ character.notes }}</div>
|
||||||
|
<div class="uk-margin-small-top"><span class="uk-label">Custom Attributes:</span> {% for k,v in character.custom_attributes.items %}{{ k }}: {{ v }}{% if not forloop.last %}, {% endif %}{% empty %}None{% endfor %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="uk-card uk-card-default uk-card-body">
|
||||||
|
<ul class="uk-tab uk-child-width-expand uk-margin-remove-bottom" uk-tab>
|
||||||
|
<li class="uk-active"><a href="#">Class Features</a></li>
|
||||||
|
<li class="uk-active"><a href="#">Background Features</a></li>
|
||||||
|
<li class="uk-active"><a href="#">Race Features</a></li>
|
||||||
|
<li class="uk-active"><a href="#">Features</a></li>
|
||||||
|
</ul>
|
||||||
|
<ul class="uk-switcher uk-margin">
|
||||||
|
<li>
|
||||||
|
<div uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<ul class="uk-accordion-default" uk-accordion="multiple: true">
|
||||||
|
{% for val in character.char_class.display_features.all %}
|
||||||
|
{% if val.priority <= character.level %}
|
||||||
|
<li>
|
||||||
|
<a class="uk-accordion-title" href>{{ val.feature.feature_name }} (Level {{val.priority}})</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<p>{{ val.feature.feature_description|safe }}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<script src="{% static 'js/uikit.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
230
main/templates/main/feature.html
Normal file
230
main/templates/main/feature.html
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Features</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="uk-container">
|
||||||
|
<div uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<h1 class="uk-heading-line uk-text-center"><span>Add a Feature</span></h1>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<label class="uk-form-label" for="feature_name">Name</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input" id="feature_name" name="feature_name" type="text" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<label class="uk-form-label" for="feature_description">Description</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<textarea class="uk-textarea" id="feature_description" name="feature_description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<ul uk-tab>
|
||||||
|
<li><a href="#">Operations</a></li>
|
||||||
|
<li><a href="#">Requirements</a></li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="uk-switcher">
|
||||||
|
<div uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<p class="uk-text-muted">By adding an operation to your feature you can have the feature modifier and manipulate
|
||||||
|
a characters properties at runtime.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<button type="button" class="uk-button uk-button-primary uk-align-right" onclick="addOperation()"><span uk-icon="icon: plus"></span> Add Operation</button>
|
||||||
|
</div>
|
||||||
|
<div id="operations-body" class="uk-width-1-1" uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<h3 class="uk-card-title">Small</h3>
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
||||||
|
<div class="uk-padding uk-padding-remove-top uk-padding-remove-bottom" uk-grid>
|
||||||
|
<input class="uk-width-1-4 uk-input" name="operation_attr_${idx}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
|
||||||
|
<select class="uk-width-1-4 uk-select" name="operation_operation_${idx}">
|
||||||
|
<option value="add" ${op==='add'?'selected':''}>Add</option>
|
||||||
|
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
|
||||||
|
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
|
||||||
|
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
|
||||||
|
<option value="set" ${op==='set'?'selected':''}>Set</option>
|
||||||
|
</select>
|
||||||
|
<input class="uk-width-1-4 uk-input" name="operation_value_${idx}" type="text" placeholder="Value or Attribute..." value="${value}">
|
||||||
|
<button type='button' class='uk-width-1-4 uk-button uk-button-danger uk-button-small' onclick='this.closest(".uk-card").remove()'><span uk-icon="icon: minus"></span></button>
|
||||||
|
</div>
|
||||||
|
<div uk-grid>
|
||||||
|
<div class='uk-margin-small-top' id='operation-limit-list-${idx}'>
|
||||||
|
</div>
|
||||||
|
<button type='button' class='uk-button uk-button-secondary uk-button-small' onclick='addLimitField(${idx})'><span uk-icon="icon: plus"></span> Add Limit/Subpart</button>
|
||||||
|
|
||||||
|
<div class='uk-margin-small-top' id='operation-req-list-${idx}'></div>
|
||||||
|
<button type='button' class='uk-button uk-button-primary uk-button-small uk-margin-small-top' onclick='addOperationRequirementField(${idx})'><span uk-icon="icon: plus"></span> Add Requirement</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<button type="button" class="uk-button uk-button-primary " onclick="addRequirement()"><span uk-icon="icon: plus"></span> Add Requirement</button>
|
||||||
|
<div id="requirements-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<button type="submit" class="uk-button uk-button-primary uk-width-1-1">Submit Feature</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{% static 'js/uikit.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
// --- DYNAMIC REQUIREMENTS ---
|
||||||
|
function addRequirement(prop='', cond='', val='') {
|
||||||
|
const reqList = document.getElementById('requirements-list');
|
||||||
|
const idx = reqList.children.length;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
|
||||||
|
el.setAttribute('uk-grid', '')
|
||||||
|
el.innerHTML = `
|
||||||
|
<input class="uk-input" name="requirement_property_${idx}" type="text" placeholder="Property" value="${prop}">
|
||||||
|
<select class="uk-select" name="requirement_condition_${idx}">
|
||||||
|
<option value="==" ${cond=='==' ? 'selected' : ''}>=</option>
|
||||||
|
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||||
|
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||||
|
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</option>
|
||||||
|
</select>
|
||||||
|
<input class="uk-input" name="requirement_value_${idx}" type="text" placeholder="Value" value="${val}">
|
||||||
|
<button type="button" class="uk-button uk-button-danger uk-button-small" onclick="this.parentElement.remove()"><span uk-icon="icon: minus"></span></button>
|
||||||
|
`;
|
||||||
|
reqList.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DYNAMIC OPERATIONS ---
|
||||||
|
function addOperation(attr='', op='', value='', extra=[]) {
|
||||||
|
const operationsBody = document.getElementById('operations-body');
|
||||||
|
const op_index = operationsBody.children.length;
|
||||||
|
|
||||||
|
const main_div = document.createElement('div');
|
||||||
|
main_div.setAttribute('class', 'uk-width-1-1')
|
||||||
|
//const el = document.createElement('tr');
|
||||||
|
|
||||||
|
main_div.innerHTML = `
|
||||||
|
<div class="uk-card uk-card-small uk-card-default uk-card-body">
|
||||||
|
<div class="uk-grid-small" uk-grid>
|
||||||
|
<div class="uk-width-1-4">
|
||||||
|
<input class="uk-input" name="operation_attr_${op_index}" type="text" placeholder="Affected Attribute (e.g., strength)" value="${attr}">
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-4">
|
||||||
|
<select class="uk-select" name="operation_operation_${op_index}">
|
||||||
|
<option value="add" ${op==='add'?'selected':''}>Add</option>
|
||||||
|
<option value="subtract" ${op==='subtract'?'selected':''}>Subtract</option>
|
||||||
|
<option value="multiply" ${op==='multiply'?'selected':''}>Multiply</option>
|
||||||
|
<option value="divide" ${op==='divide'?'selected':''}>Divide</option>
|
||||||
|
<option value="set" ${op==='set'?'selected':''}>Set</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-4">
|
||||||
|
<input class="uk-input" name="operation_value_${op_index}" type="text" placeholder="Value or Attribute..." value="${value}">
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-4">
|
||||||
|
<button type="button" class="uk-button uk-button-danger" onclick="this.closest(".uk-card").remove()"><span uk-icon="icon: trash"></span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<ul class="uk-accordion-default" uk-accordion>
|
||||||
|
<li>
|
||||||
|
<a class="uk-accordion-title" href>Operation Limits</a>
|
||||||
|
<div class="uk-accordion-content" uk-grid>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<p class="uk-text-meta">Add a limit to when this operation is applied to a character.</p>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<button type="button" class="uk-button uk-button-secondary" onclick="addLimitField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Limit</button>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1 uk-margin-small-top" id="operation-limit-list-${op_index}"></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="uk-accordion-title" href>Operation Requirements</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<div class="uk-width-1-1">
|
||||||
|
<button type="button" class="uk-button uk-button-small uk-button-secondary" onclick="addOperationRequirementField(${op_index})"><span uk-icon="icon: plus"></span> Add Operation Requirement</button>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-1-1 uk-margin-small-top" id="operation-req-list-${op_index}"></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
operationsBody.appendChild(main_div);
|
||||||
|
// add pre-existing subparts if any (extra)
|
||||||
|
if (extra && Array.isArray(extra)) {
|
||||||
|
extra.forEach(kv => addLimitField(op_index, kv.key, kv.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLimitField(opIdx, k='', v='', t='') {
|
||||||
|
const list = document.getElementById('operation-limit-list-' + opIdx);
|
||||||
|
const subIdx = (list) ? list.children.length : 0;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle';
|
||||||
|
el.setAttribute('uk-grid', '');
|
||||||
|
el.innerHTML = `
|
||||||
|
<label class="uk-label">Limit</label>
|
||||||
|
<input class="uk-input" name="operation_${opIdx}_limitkey_${subIdx}" type="text" placeholder="Extra Key (e.g., limit, min, max)" value="${k||''}">
|
||||||
|
<select class="uk-select" name="operation_${opIdx}_limittype_${subIdx}">
|
||||||
|
<option value="max" ${t==='max'?'selected':''}>max</option>
|
||||||
|
<option value="min" ${t==='min'?'selected':''}>min</option>
|
||||||
|
<option value="equals" ${t==='equals'?'selected':''}>equal</option>
|
||||||
|
</select>
|
||||||
|
<input class="uk-input" name="operation_${opIdx}_limitval_${subIdx}" type="text" placeholder="Value" value="${v||''}">
|
||||||
|
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
|
||||||
|
`;
|
||||||
|
list.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DYNAMIC OPERATION REQUIREMENTS ---
|
||||||
|
function addOperationRequirementField(opIdx, prop='', cond='', val='') {
|
||||||
|
const list = document.getElementById('operation-req-list-' + opIdx);
|
||||||
|
const subIdx = (list) ? list.children.length : 0;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'uk-grid-small uk-child-width-expand@s uk-flex-middle uk-margin-small';
|
||||||
|
el.setAttribute('uk-grid', '');
|
||||||
|
el.innerHTML = `
|
||||||
|
<input class="uk-input" name="operation_${opIdx}_req_property_${subIdx}" type="text" placeholder="Requirement Property" value="${prop}">
|
||||||
|
<select class="uk-select" name="operation_${opIdx}_req_condition_${subIdx}">
|
||||||
|
<option value="==" ${cond=='==' ? 'selected' : ''}>=</option>
|
||||||
|
<option value=">=" ${cond=='>=' ? 'selected' : ''}>≥</option>
|
||||||
|
<option value="<=" ${cond=='<=' ? 'selected' : ''}>≤</option>
|
||||||
|
<option value="!=" ${cond=='!=' ? 'selected' : ''}>≠</option>
|
||||||
|
</select>
|
||||||
|
<input class="uk-input" name="operation_${opIdx}_req_value_${subIdx}" type="text" placeholder="Value" value="${val}">
|
||||||
|
<button type='button' class='uk-button uk-button-danger uk-button-small' onclick='this.parentElement.remove()'><span uk-icon="icon: minus"></span></button>
|
||||||
|
`;
|
||||||
|
list.appendChild(el);
|
||||||
|
}
|
||||||
|
// Prevent accidental form submit via Enter in addable fields
|
||||||
|
// (recommended for dynamic forms)
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if(e.key==="Enter" && e.target.tagName==="INPUT") {
|
||||||
|
// do nothing if inside textarea
|
||||||
|
if (!(e.target.closest('form') && e.target.closest('form').id === 'feature-form')) return;
|
||||||
|
e.preventDefault(); return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Lisium Django Demo</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Welcome to Django</h1>
|
|
||||||
<p>
|
|
||||||
This is being rendered from a django template.
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
88
main/templates/main/map.html
Normal file
88
main/templates/main/map.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% load static %}
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Pin Map</title>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/uikit.min.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/map.css' %}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Quick fix if static is not configured: place CSS here if needed */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="uk-container">
|
||||||
|
<h2>Interactive Map with Pins</h2>
|
||||||
|
<div id="map-container" class="custom-map">
|
||||||
|
<div id="map-viewport">
|
||||||
|
<img src="{% static 'map.png' %}" id="main-map" alt="Map" draggable="false"/>
|
||||||
|
<div id="pins"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-container uk-margin-large-top">
|
||||||
|
<h4 class="uk-heading-divider">All Pins</h4>
|
||||||
|
<div class="uk-overflow-auto">
|
||||||
|
<table id="pins-table" class="uk-table uk-table-divider uk-table-hover uk-table-small uk-table-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>X (%)</th>
|
||||||
|
<th>Y (%)</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Rows injected by JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="add-pin-modal" uk-modal>
|
||||||
|
<div class="uk-modal-dialog uk-modal-body uk-border-rounded">
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<h3 class="uk-modal-title uk-flex uk-flex-middle">
|
||||||
|
<span uk-icon="icon: location; ratio: 1.2" class="uk-margin-small-right"></span>
|
||||||
|
Add a Map Pin
|
||||||
|
</h3>
|
||||||
|
<form id="add-pin-form" class="uk-form-stacked">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="pin-label">Label</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input uk-border-pill" id="pin-label" type="text" placeholder="e.g. Castle Gates" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="pin-url">Link URL</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input uk-border-pill" id="pin-url" type="text" placeholder="/castle/" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="pin-url">Pin Type</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input class="uk-input uk-border-pill" id="pin-type" type="text" placeholder="/castle/" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="pin-x">
|
||||||
|
<input type="hidden" id="pin-y">
|
||||||
|
<div class="uk-flex uk-flex-between uk-margin-top">
|
||||||
|
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
|
||||||
|
<button class="uk-button uk-button-primary uk-border-pill" type="submit">
|
||||||
|
<span uk-icon="plus"></span> Add Pin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script src="{% static 'js/uikit.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/uikit-icons.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/map.js' %}"></script>
|
||||||
|
</html>
|
||||||
@ -2,5 +2,8 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
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'),
|
||||||
]
|
]
|
||||||
102
main/views.py
102
main/views.py
@ -1,6 +1,106 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponse
|
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.
|
# Create your views here.
|
||||||
def home(request):
|
def home(request):
|
||||||
return render(request, 'main/home.html')
|
with open('test_character.json', 'r+') as file:
|
||||||
|
character = json.load(file)
|
||||||
|
|
||||||
|
character = Character.objects.get(name="Gerom")
|
||||||
|
class_feature_links = (
|
||||||
|
PackageFeature.objects
|
||||||
|
.filter(package=character.char_class)
|
||||||
|
.select_related('feature')
|
||||||
|
.order_by('priority')
|
||||||
|
)
|
||||||
|
character.char_class.display_features = class_feature_links
|
||||||
|
return render(request, 'main/character_sheet.html', {'character': character, })
|
||||||
|
|
||||||
|
|
||||||
|
def feature_add_view(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render(request, 'main/feature.html')
|
||||||
|
elif request.method == 'POST':
|
||||||
|
# Parse basic fields
|
||||||
|
name = request.POST.get('feature_name')
|
||||||
|
description = request.POST.get('feature_description')
|
||||||
|
|
||||||
|
# Parse Requirements
|
||||||
|
requirements = []
|
||||||
|
req_keys = [k for k in request.POST.keys() if k.startswith('requirement_')]
|
||||||
|
idxs = set()
|
||||||
|
for k in req_keys:
|
||||||
|
try:
|
||||||
|
idxs.add(int(k.split('_')[-1]))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
for idx in sorted(list(idxs)):
|
||||||
|
prop = request.POST.get(f'requirement_property_{idx}', '').strip()
|
||||||
|
cond = request.POST.get(f'requirement_condition_{idx}', '').strip()
|
||||||
|
val = request.POST.get(f'requirement_value_{idx}', '').strip()
|
||||||
|
if prop and cond and val:
|
||||||
|
requirements.append({'property': prop, 'condition': cond, 'value': val})
|
||||||
|
|
||||||
|
# Parse Operations (with extra subparts)
|
||||||
|
operations = []
|
||||||
|
op_keys = [k for k in request.POST.keys() if k.startswith('operation_attr_')]
|
||||||
|
op_idxs = [int(k.replace('operation_attr_', '')) for k in op_keys]
|
||||||
|
for op_idx in op_idxs:
|
||||||
|
attr = request.POST.get(f'operation_attr_{op_idx}', '').strip()
|
||||||
|
op = request.POST.get(f'operation_operation_{op_idx}', '').strip()
|
||||||
|
value = request.POST.get(f'operation_value_{op_idx}', '').strip()
|
||||||
|
|
||||||
|
# Subparts/limits
|
||||||
|
subparts = []
|
||||||
|
sub_idx = 0
|
||||||
|
while True:
|
||||||
|
kfield = f'operation_{op_idx}_limitkey_{sub_idx}'
|
||||||
|
vfield = f'operation_{op_idx}_limitval_{sub_idx}'
|
||||||
|
if kfield in request.POST and vfield in request.POST:
|
||||||
|
k = request.POST.get(kfield, '').strip()
|
||||||
|
v = request.POST.get(vfield, '').strip()
|
||||||
|
if k and v:
|
||||||
|
subparts.append({'key': k, 'value': v})
|
||||||
|
sub_idx += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
operation = {'attr': attr, 'operation': op, 'value': value}
|
||||||
|
for part in subparts:
|
||||||
|
operation[part['key']] = part['value']
|
||||||
|
operations.append(operation)
|
||||||
|
|
||||||
|
feat = Feature.objects.create(
|
||||||
|
feature_name=name,
|
||||||
|
feature_description=description,
|
||||||
|
feature_requirements=requirements,
|
||||||
|
feature_data={'operations': operations}
|
||||||
|
)
|
||||||
|
return render(request, 'main/feature_add.html', {
|
||||||
|
'msg': f'Feature "{feat.feature_name}" added successfully!',
|
||||||
|
})
|
||||||
|
|
||||||
|
def map_page(request):
|
||||||
|
return render(request, "main/map.html")
|
||||||
|
|
||||||
|
@api_view(['GET', 'POST'])
|
||||||
|
def pin_list(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
pins = Pin.objects.all()
|
||||||
|
serializer = PinSerializer(pins, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
if request.method == 'POST':
|
||||||
|
serializer = PinSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
7
package_template.json
Normal file
7
package_template.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"package_name": "",
|
||||||
|
"package_description": "",
|
||||||
|
"package_type": "",
|
||||||
|
"package_doc_md": "",
|
||||||
|
"package_features": []
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
Flask
|
Flask
|
||||||
requests
|
requests
|
||||||
Markdown
|
Markdown
|
||||||
|
|
||||||
|
Django
|
||||||
|
djangorestframework
|
||||||
|
|||||||
99
static/css/map.css
Normal file
99
static/css/map.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
.custom-map {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 6px 24px rgba(0,0,0,0.15), 0 1.5px 6px #c6c8c9;
|
||||||
|
background: #f7f9fb;
|
||||||
|
border: none;
|
||||||
|
padding: 24px 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-map #map-container.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.custom-map #map-viewport {
|
||||||
|
display: block !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-map #main-map {
|
||||||
|
display: block;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
.custom-map #pins {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0;
|
||||||
|
pointer-events: none; /* Clicks fall through to map image */
|
||||||
|
}
|
||||||
|
.custom-map .pin {
|
||||||
|
position: absolute;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
transform: translate(-50%, -100%); /* Center bottom point on location */
|
||||||
|
pointer-events: auto; /* Pins remain clickable */
|
||||||
|
z-index: 10;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
cursor: pointer;
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.15));
|
||||||
|
}
|
||||||
|
.custom-map .pin:hover {
|
||||||
|
transform: translate(-50%, -120%) scale(1.1);
|
||||||
|
filter: drop-shadow(0 6px 18px rgba(0,104,207,0.15)) brightness(1.15);
|
||||||
|
z-index: 18;
|
||||||
|
}
|
||||||
|
.custom-map .pin img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
border-radius: 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-label {
|
||||||
|
display: block;
|
||||||
|
min-width: 45px;
|
||||||
|
max-width: 168px;
|
||||||
|
margin-top: 7px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1452b9;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.11);
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: none; /* so clicks go to the pin anchor */
|
||||||
|
opacity: 0.90;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0%);
|
||||||
|
z-index: 15;
|
||||||
|
border: 1.5px solid #dde7fa;
|
||||||
|
}
|
||||||
|
.pin:hover .pin-label {
|
||||||
|
opacity: 1;
|
||||||
|
background: #f3f8ff;
|
||||||
|
color: #1d4ba0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-highlight {
|
||||||
|
animation: pinPulse 0.8s ease;
|
||||||
|
}
|
||||||
|
@keyframes pinPulse {
|
||||||
|
0% { box-shadow: 0 0 0 0px rgba(45,156,219,0.52);}
|
||||||
|
80% { box-shadow: 0 0 0 18px rgba(45,156,219,0);}
|
||||||
|
100% { box-shadow: 0 0 0 0px rgba(45,156,219,0);}
|
||||||
|
}
|
||||||
BIN
static/dungeon.png
Normal file
BIN
static/dungeon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
207
static/js/map.js
Normal file
207
static/js/map.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
function renderPins(pins) {
|
||||||
|
const pinsDiv = document.getElementById("pins");
|
||||||
|
pinsDiv.innerHTML = '';
|
||||||
|
const img = document.getElementById("main-map");
|
||||||
|
const imgWidth = img.clientWidth;
|
||||||
|
const imgHeight = img.clientHeight;
|
||||||
|
pins.forEach(pin => {
|
||||||
|
console.log(pin)
|
||||||
|
const el = document.createElement('a');
|
||||||
|
el.className = "pin";
|
||||||
|
el.href = pin.url;
|
||||||
|
el.title = pin.label;
|
||||||
|
el.target = "_blank";
|
||||||
|
el.style.left = (imgWidth * (pin.x / 100)) + 'px';
|
||||||
|
el.style.top = (imgHeight * (pin.y / 100)) + 'px';
|
||||||
|
el.dataset.pinid = pin.id;
|
||||||
|
el.dataset.x = pin.x;
|
||||||
|
el.dataset.y = pin.y;
|
||||||
|
if (pin.pin_type == "general"){
|
||||||
|
el.innerHTML = '<img src="/static/pin.svg" alt="general pin" />';
|
||||||
|
}
|
||||||
|
if (pin.pin_type == "town"){
|
||||||
|
el.innerHTML = '<img src="/static/town.png" alt="town pin" />';
|
||||||
|
}
|
||||||
|
if (pin.pin_type == "dungeon"){
|
||||||
|
el.innerHTML = '<img src="/static/dungeon.png" alt="dungeon pin" />';
|
||||||
|
}
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'pin-label';
|
||||||
|
label.innerText = pin.label;
|
||||||
|
|
||||||
|
el.appendChild(label);
|
||||||
|
|
||||||
|
pinsDiv.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPinsTable(pins)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchPins() {
|
||||||
|
fetch("/api/pins/")
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok) throw new Error("Pin fetch failed");
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then(renderPins)
|
||||||
|
.catch(() => {
|
||||||
|
// For demo/development: Show example
|
||||||
|
renderPins([
|
||||||
|
{x:15, y:30, label:'Demo Pin', url:'/'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPinsTable(pins) {
|
||||||
|
const tbody = document.querySelector('#pins-table tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
pins.forEach((pin, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${idx+1}</td>
|
||||||
|
<td>${escapeHTML(pin.label)}</td>
|
||||||
|
<td><a href="${pin.url}" target="_blank">${pin.url}</a></td>
|
||||||
|
<td>${pin.x.toFixed(2)}</td>
|
||||||
|
<td>${pin.y.toFixed(2)}</td>
|
||||||
|
<td>
|
||||||
|
<button class="uk-button uk-button-small uk-button-default" onclick="centerMapOnPin(${pin.x},${pin.y})">Center</button>
|
||||||
|
<button class="uk-button uk-button-small uk-button-primary" onclick="editPin('${pin.id}')">Edit</button>
|
||||||
|
<button class="uk-button uk-button-small uk-button-danger" onclick="deletePin('${pin.id}')">Delete</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function escapeHTML(str) {
|
||||||
|
// Prevent breaking the DOM if user enters < > or & etc
|
||||||
|
return (str||'').replace(/[&<>"'`]/g, s => ({
|
||||||
|
'&':'&', '<':'<', '>':'>', '"':'"', "'":''', '`':'`'
|
||||||
|
}[s]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function centerMapOnPin(xPercent, yPercent) {
|
||||||
|
const mapImg = document.getElementById('main-map');
|
||||||
|
const container = document.getElementById('map-container');
|
||||||
|
// Calculate pixel pos relative to image
|
||||||
|
const imgWidth = mapImg.clientWidth;
|
||||||
|
const imgHeight = mapImg.clientHeight;
|
||||||
|
const pinX = imgWidth * (xPercent / 100);
|
||||||
|
const pinY = imgHeight * (yPercent / 100);
|
||||||
|
// Center in the scrollable container
|
||||||
|
const containerWidth = container.clientWidth;
|
||||||
|
const containerHeight = container.clientHeight;
|
||||||
|
container.scrollLeft = Math.max(0, pinX - containerWidth / 2);
|
||||||
|
container.scrollTop = Math.max(0, pinY - containerHeight / 2);
|
||||||
|
// Highlight the right pin
|
||||||
|
const pinEl = Array.from(document.querySelectorAll('.pin')).find(
|
||||||
|
el => Math.abs(parseFloat(el.dataset.x) - xPercent) < 0.0001 &&
|
||||||
|
Math.abs(parseFloat(el.dataset.y) - yPercent) < 0.0001
|
||||||
|
);
|
||||||
|
if(pinEl) {
|
||||||
|
pinEl.classList.add('pin-highlight');
|
||||||
|
setTimeout(()=>pinEl.classList.remove('pin-highlight'), 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPins();
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('map-container').addEventListener('contextmenu', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const mapImg = document.getElementById('main-map');
|
||||||
|
const rect = mapImg.getBoundingClientRect();
|
||||||
|
if (!(e.target === mapImg || e.target.id === 'pins')) return;
|
||||||
|
const x = 100 * (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = 100 * (e.clientY - rect.top) / rect.height;
|
||||||
|
document.getElementById('pin-x').value = x;
|
||||||
|
document.getElementById('pin-y').value = y;
|
||||||
|
document.getElementById('add-pin-form').reset();
|
||||||
|
UIkit.modal('#add-pin-modal').show();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('add-pin-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const x = parseFloat(document.getElementById('pin-x').value);
|
||||||
|
const y = parseFloat(document.getElementById('pin-y').value);
|
||||||
|
const label = document.getElementById('pin-label').value;
|
||||||
|
const url = document.getElementById('pin-url').value;
|
||||||
|
const pin_type = document.getElementById('pin-type').value;
|
||||||
|
fetch('/api/pins/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
},
|
||||||
|
body: JSON.stringify({x, y, label, url, pin_type})
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp.ok) {
|
||||||
|
UIkit.modal('#add-pin-modal').hide();
|
||||||
|
fetchPins();
|
||||||
|
} else {
|
||||||
|
resp.json().then(data => alert(JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function resizePinsDiv() {
|
||||||
|
var mapImg = document.getElementById('main-map');
|
||||||
|
var pinsDiv = document.getElementById('pins');
|
||||||
|
// Set pins div to same px size as image
|
||||||
|
pinsDiv.style.width = mapImg.width + 'px';
|
||||||
|
pinsDiv.style.height = mapImg.height + 'px';
|
||||||
|
pinsDiv.style.position = 'absolute';
|
||||||
|
pinsDiv.style.top = '0';
|
||||||
|
pinsDiv.style.left = '0';
|
||||||
|
}
|
||||||
|
document.getElementById('main-map').addEventListener('load', resizePinsDiv);
|
||||||
|
window.addEventListener('resize', resizePinsDiv);
|
||||||
|
if (document.getElementById('main-map').complete) resizePinsDiv();
|
||||||
|
|
||||||
|
// This code handles the dragging of the map
|
||||||
|
const container = document.getElementById('map-container');
|
||||||
|
let isDragging = false;
|
||||||
|
let startX, startY, scrollLeft, scrollTop;
|
||||||
|
container.addEventListener('mousedown', function(e) {
|
||||||
|
isDragging = true;
|
||||||
|
container.classList.add('dragging');
|
||||||
|
startX = e.pageX - container.offsetLeft;
|
||||||
|
startY = e.pageY - container.offsetTop;
|
||||||
|
scrollLeft = container.scrollLeft;
|
||||||
|
scrollTop = container.scrollTop;
|
||||||
|
});
|
||||||
|
container.addEventListener('mouseleave', function() {
|
||||||
|
isDragging = false;
|
||||||
|
container.classList.remove('dragging');
|
||||||
|
});
|
||||||
|
container.addEventListener('mouseup', function() {
|
||||||
|
isDragging = false;
|
||||||
|
container.classList.remove('dragging');
|
||||||
|
});
|
||||||
|
container.addEventListener('mousemove', function(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const x = e.pageX - container.offsetLeft;
|
||||||
|
const y = e.pageY - container.offsetTop;
|
||||||
|
const walkX = x - startX;
|
||||||
|
const walkY = y - startY;
|
||||||
|
container.scrollLeft = scrollLeft - walkX;
|
||||||
|
container.scrollTop = scrollTop - walkY;
|
||||||
|
});
|
||||||
BIN
static/map.png
Normal file
BIN
static/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 MiB |
1
static/pin.svg
Normal file
1
static/pin.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#ff5151" d="M12 11.5A2.5 2.5 0 0 1 9.5 9A2.5 2.5 0 0 1 12 6.5A2.5 2.5 0 0 1 14.5 9a2.5 2.5 0 0 1-2.5 2.5M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7"/></svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
BIN
static/town.png
Normal file
BIN
static/town.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
18
test.py
18
test.py
@ -1,18 +0,0 @@
|
|||||||
from backend.models.users import UsersModel
|
|
||||||
|
|
||||||
UsersModel.create_table()
|
|
||||||
|
|
||||||
payload = UsersModel.Payload(
|
|
||||||
user_username="Mechseroms",
|
|
||||||
user_email="jadowyne.ulve@outlook.com",
|
|
||||||
user_hashed_password="test"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#UsersModel.insert_tuple(payload=payload.payload_dictionary())
|
|
||||||
|
|
||||||
authenticated, user, message = UsersModel.authenticate_login('Mechseroms', 'test')
|
|
||||||
|
|
||||||
print(authenticated)
|
|
||||||
print(user)
|
|
||||||
print(message)
|
|
||||||
156
test_character.json
Normal file
156
test_character.json
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"name": "Elira Sunshadow",
|
||||||
|
"level": 5,
|
||||||
|
"race": "Elf",
|
||||||
|
"subrace": "High Elf",
|
||||||
|
"class": "Ranger",
|
||||||
|
"subclass": "Hunter",
|
||||||
|
"background": "Outlander",
|
||||||
|
"alignment": "Chaotic Good",
|
||||||
|
"size": "Medium",
|
||||||
|
"age": 121,
|
||||||
|
"gender": "Female",
|
||||||
|
"height": "5'7\"",
|
||||||
|
"weight": 130,
|
||||||
|
"deity": "Sehanine Moonbow",
|
||||||
|
|
||||||
|
"strength_base": 10,
|
||||||
|
"dexterity_base": 16,
|
||||||
|
"constitution_base": 13,
|
||||||
|
"intelligence_base": 12,
|
||||||
|
"wisdom_base": 15,
|
||||||
|
"charisma_base": 9,
|
||||||
|
"armor_base": 11,
|
||||||
|
|
||||||
|
"strength": 11,
|
||||||
|
"dexterity": 18,
|
||||||
|
"constitution": 14,
|
||||||
|
"intelligence": 13,
|
||||||
|
"wisdom": 16,
|
||||||
|
"charisma": 10,
|
||||||
|
"armor": 15,
|
||||||
|
|
||||||
|
"strength_modifier": 0,
|
||||||
|
"dexterity_modifier": 4,
|
||||||
|
"constitution_modifier": 2,
|
||||||
|
"intelligence_modifier": 1,
|
||||||
|
"wisdom_modifier": 3,
|
||||||
|
"charisma_modifier": 0,
|
||||||
|
|
||||||
|
"proficiency": 3,
|
||||||
|
"inspiration": true,
|
||||||
|
"experience": 6500,
|
||||||
|
|
||||||
|
"proficiencies_armor": ["Light Armor", "Medium Armor", "Shields"],
|
||||||
|
"proficiencies_weapons": ["Simple Weapons", "Martial Weapons"],
|
||||||
|
"proficiencies_tools": ["Flute", "Herbalism Kit"],
|
||||||
|
"languages": ["Common", "Elvish", "Sylvan"],
|
||||||
|
|
||||||
|
"strength_save": 0,
|
||||||
|
"dexterity_save": 7,
|
||||||
|
"constitution_save": 2,
|
||||||
|
"intelligence_save": 1,
|
||||||
|
"wisdom_save": 6,
|
||||||
|
"charisma_save": 0,
|
||||||
|
|
||||||
|
"hp": 42,
|
||||||
|
"hp_max": 42,
|
||||||
|
"hp_temp": 5,
|
||||||
|
"hit_die": "1d10",
|
||||||
|
"initiative": 4,
|
||||||
|
|
||||||
|
"deathsaves_successes": 0,
|
||||||
|
"deathsaves_failures": 0,
|
||||||
|
|
||||||
|
"fire_resistance": false,
|
||||||
|
"poison_resistance": true,
|
||||||
|
"psychic_resistance": false,
|
||||||
|
"cold_resistance": false,
|
||||||
|
"thunder_resistance": false,
|
||||||
|
"acid_resistance": false,
|
||||||
|
"force_resistance": false,
|
||||||
|
"radiant_resistance": false,
|
||||||
|
"necrotic_resistance": false,
|
||||||
|
"bludgeoning_resistance": false,
|
||||||
|
"piercing_resistance": true,
|
||||||
|
"slashing_resistance": false,
|
||||||
|
"immunities": [],
|
||||||
|
"vulnerabilities": ["Radiant"],
|
||||||
|
|
||||||
|
"speed_base": 35,
|
||||||
|
"speed_type": ["Walk"],
|
||||||
|
"darkvision": 60,
|
||||||
|
"blindsight": 0,
|
||||||
|
"tremorsense": 0,
|
||||||
|
"truesight": 0,
|
||||||
|
|
||||||
|
"athletics": 2,
|
||||||
|
"acrobatics": 7,
|
||||||
|
"sleight_of_hand": 4,
|
||||||
|
"stealth": 7,
|
||||||
|
"arcana": 1,
|
||||||
|
"history": 1,
|
||||||
|
"investigation": 4,
|
||||||
|
"nature": 3,
|
||||||
|
"religion": 1,
|
||||||
|
"animal_handling": 6,
|
||||||
|
"insight": 6,
|
||||||
|
"medicine": 3,
|
||||||
|
"perception": 6,
|
||||||
|
"survival": 6,
|
||||||
|
"deception": 0,
|
||||||
|
"intimidation": 0,
|
||||||
|
"performance": 0,
|
||||||
|
"persuasion": 0,
|
||||||
|
|
||||||
|
"athletics_passive": 2,
|
||||||
|
"acrobatics_passive": 7,
|
||||||
|
"sleight_of_hand_passive": 4,
|
||||||
|
"stealth_passive": 7,
|
||||||
|
"arcana_passive": 1,
|
||||||
|
"history_passive": 1,
|
||||||
|
"investigation_passive": 4,
|
||||||
|
"nature_passive": 3,
|
||||||
|
"religion_passive": 1,
|
||||||
|
"animal_handling_passive": 6,
|
||||||
|
"insight_passive": 6,
|
||||||
|
"medicine_passive": 3,
|
||||||
|
"perception_passive": 16,
|
||||||
|
"survival_passive": 6,
|
||||||
|
"deception_passive": 0,
|
||||||
|
"intimidation_passive": 0,
|
||||||
|
"performance_passive": 0,
|
||||||
|
"persuasion_passive": 0,
|
||||||
|
"passive_perception": 16,
|
||||||
|
"passive_investigation": 14,
|
||||||
|
"passive_insight": 16,
|
||||||
|
|
||||||
|
"spellcasting_ability": "Wisdom",
|
||||||
|
"spell_save_dc": 14,
|
||||||
|
"spell_attack_bonus": 6,
|
||||||
|
"known_spells": ["Hunter's Mark", "Goodberry", "Speak with Animals"],
|
||||||
|
"prepared_spells": ["Hunter's Mark", "Goodberry"],
|
||||||
|
"spell_slots": {"1": 4, "2": 2},
|
||||||
|
"cantrips": ["Produce Flame", "Druidcraft"],
|
||||||
|
"spellcasting_class": "Ranger",
|
||||||
|
|
||||||
|
"exhaustion_level": 0,
|
||||||
|
"conditions_active": [],
|
||||||
|
|
||||||
|
"cp": 43,
|
||||||
|
"sp": 27,
|
||||||
|
"ep": 0,
|
||||||
|
"gp": 89,
|
||||||
|
"pp": 3,
|
||||||
|
"equipment": ["Longbow", "Quiver", "Studded Leather Armor", "Explorer's Pack", "Flute"],
|
||||||
|
|
||||||
|
"features": ["Favored Enemy", "Natural Explorer"],
|
||||||
|
"traits": ["Keen Senses", "Fey Ancestry"],
|
||||||
|
"personality_traits": ["I watch over my friends as if they were a litter of newborn pups."],
|
||||||
|
"ideals": ["Change: Life is like the seasons, in constant change, and we must change with it."],
|
||||||
|
"bonds": ["An injury to the unspoiled wilderness of my home is an injury to me."],
|
||||||
|
"flaws": ["I am slow to trust members of other races."],
|
||||||
|
|
||||||
|
"notes": "Carries a carved wooden pendant given by her mentor.",
|
||||||
|
"custom_attributes": {"Favorite Animal": "Stag", "Secret": "Afraid of deep water"}
|
||||||
|
}
|
||||||
164
webserver.py
164
webserver.py
@ -1,164 +0,0 @@
|
|||||||
from flask import Flask, jsonify, request, render_template, session, redirect, url_for
|
|
||||||
from functools import wraps
|
|
||||||
import requests
|
|
||||||
|
|
||||||
import markdown
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = "53016ca1157efe86174936227721c701048926d4c31c4531201bcfcff4725277"
|
|
||||||
|
|
||||||
backend_config = {
|
|
||||||
'host': '127.0.0.1',
|
|
||||||
'port': '5001'
|
|
||||||
}
|
|
||||||
|
|
||||||
def login_required(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if 'user' not in session or session['user'] == None:
|
|
||||||
return redirect(url_for('login'))
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
@login_required
|
|
||||||
def home():
|
|
||||||
return 'Hello, World!'
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/p/<uuid>', methods=['GET'])
|
|
||||||
@login_required
|
|
||||||
def package_main(uuid):
|
|
||||||
|
|
||||||
defined_types = [
|
|
||||||
{'value': '5e_class', 'string': '5e Class'},
|
|
||||||
{'value': '5e_race', 'string': '5e Race'},
|
|
||||||
{'value': '5e_background', 'string': '5e Background'},
|
|
||||||
{'value': '5e_subclass', 'string': '5e Subclass'},
|
|
||||||
]
|
|
||||||
|
|
||||||
if uuid == "new":
|
|
||||||
response = requests.post(url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/add')
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
return redirect(f'/p/{response.json()['package']['package_uuid']}')
|
|
||||||
|
|
||||||
response = requests.get(url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/get/{uuid}')
|
|
||||||
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
package = response.json()['package']
|
|
||||||
return render_template('package.html', package_object=package, defined_types=defined_types)
|
|
||||||
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
@app.route('/p/<uuid>/save', methods=['POST'])
|
|
||||||
def package_save(uuid):
|
|
||||||
if request.method != 'POST':
|
|
||||||
return jsonify(
|
|
||||||
error = True,
|
|
||||||
message = f"Incorrect Method attempted; {request.method}."
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
url = f'http://{backend_config["host"]}:{backend_config["port"]}/packages/update',
|
|
||||||
json = request.get_json()
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
return jsonify(
|
|
||||||
error=False,
|
|
||||||
message=f"{uuid} updated successfully!"
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
error=True,
|
|
||||||
message=f"Error while trying to update package {uuid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route('/f/<uuid>', methods=['GET'])
|
|
||||||
@login_required
|
|
||||||
def feature_main(uuid):
|
|
||||||
if uuid == "new":
|
|
||||||
response = requests.post(url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/add')
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
return redirect(f'/f/{response.json()['feature']['feature_uuid']}')
|
|
||||||
|
|
||||||
response = requests.get(url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/get/{uuid}')
|
|
||||||
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
feature = response.json()['feature']
|
|
||||||
return render_template('feature.html', feature=feature)
|
|
||||||
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
@app.route('/f/<uuid>/save', methods=['POST'])
|
|
||||||
def feature_save(uuid):
|
|
||||||
if request.method != 'POST':
|
|
||||||
return jsonify(
|
|
||||||
error = True,
|
|
||||||
message = f"Incorrect Method attempted; {request.method}."
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
url = f'http://{backend_config["host"]}:{backend_config["port"]}/features/update',
|
|
||||||
json = request.get_json()
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
return jsonify(
|
|
||||||
error=False,
|
|
||||||
message=f"{uuid} updated successfully!"
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
error=True,
|
|
||||||
message=f"Error while trying to update feature {uuid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.route('/login', methods=['GET'])
|
|
||||||
def login():
|
|
||||||
return render_template('login.html')
|
|
||||||
|
|
||||||
@app.route('/logout', methods=['GET'])
|
|
||||||
def logout():
|
|
||||||
if request.method != "GET":
|
|
||||||
return jsonify(
|
|
||||||
error = True,
|
|
||||||
message = f"Incorrect Method attempted; {request.method}."
|
|
||||||
)
|
|
||||||
|
|
||||||
if session['user']:
|
|
||||||
del session['user']
|
|
||||||
|
|
||||||
return redirect('/login')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/login/authenticate', methods=['POST'])
|
|
||||||
def basic_authenticate():
|
|
||||||
if request.method != 'POST':
|
|
||||||
return jsonify(
|
|
||||||
error = True,
|
|
||||||
message = f"Incorrect Method attempted; {request.method}."
|
|
||||||
)
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
url = f'http://{backend_config["host"]}:{backend_config["port"]}/users/authenticate',
|
|
||||||
json = request.get_json()
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200 and not response.json()['error']:
|
|
||||||
session['user'] = response.json()['user']
|
|
||||||
return jsonify(
|
|
||||||
error = False,
|
|
||||||
message = f"Successfully Logged in."
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(
|
|
||||||
error = True,
|
|
||||||
message = f"Username or Password Incorrect!"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(debug=True)
|
|
||||||
Loading…
x
Reference in New Issue
Block a user