Initial Commit

This commit is contained in:
Neill Cox 2024-06-26 14:29:06 +10:00
commit 715224653d
58 changed files with 7760 additions and 0 deletions

View 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.

7
gurps_character/admin.py Normal file
View file

@ -0,0 +1,7 @@
from django.contrib import admin
# Register your models here.
from .models import GURPSCharacter, GameSystem
admin.site.register(GURPSCharacter)
admin.site.register(GameSystem)

6
gurps_character/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class GurpsCharacterConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "gurps_character"

28
gurps_character/forms.py Normal file
View file

@ -0,0 +1,28 @@
import json
from django import forms
from django.core.exceptions import ValidationError
def validate_file(value):
if value.size > 10**6:
raise ValidationError("File too large")
try:
data = json.loads(value.read())
except json.JSONDecodeError:
raise ValidationError("Not a GCS file - json decode")
try:
version = data['version']
except KeyError:
import bpdb;bpdb.set_trace()
raise ValidationError("Not a GCS file - key error")
if version < 4:
raise ValidationError(
f"The file version ({version}) is too old. Please use a newer version (5.20.3) of GCS."
)
class UploadFileForm(forms.Form):
file = forms.FileField(validators=[validate_file])

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.1 on 2024-01-08 10:25
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="GURPSCharacter",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uuid", models.CharField(max_length=128, unique=True)),
("name", models.CharField(max_length=255, unique=True)),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.1 on 2024-01-10 07:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="gurpscharacter",
name="details",
field=models.JSONField(default={}),
preserve_default=False,
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.1 on 2024-01-16 07:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0002_gurpscharacter_details"),
]
operations = [
migrations.CreateModel(
name="GameSystem",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
("description", models.TextField(null=True)),
],
),
]

View file

391
gurps_character/models.py Normal file
View file

@ -0,0 +1,391 @@
from django.db import models
# Create your models here.
class GameSystem(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True)
class GURPSCharacter(models.Model):
uuid = models.CharField(max_length=128, unique=True)
name = models.CharField(max_length=255, unique=True)
details = models.JSONField()
# owner => user
# campaign => campaign
def adv_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages',[]) if a['calc']['points'] > 0 ])
def disadv_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages', []) if a['calc']['points'] < -1 ])
def quirks_points(self):
return sum([ a['calc']['points'] for a in self.details.get('advantages',[]) if a['calc']['points'] == -1 ])
def attr_points(self):
return sum([ a['calc']['points'] for a in self.details['attributes'] ])
def skills_points(self):
return sum([ s['points'] for s in self.details.get('skills',[]) ])
def spells_points(self):
return 0
def race_points(self):
return 0
def unspent_points(self):
return self.details['total_points'] - self.adv_points() - \
self.disadv_points() - self.attr_points() - \
self.skills_points() - self.spells_points() - \
self.race_points() - self.quirks_points()
def get_primary_attr(self, attr_id):
return [ a['calc'] for a in self.details['attributes'] if a['attr_id'] == attr_id ][0]
def st(self):
return self.get_primary_attr('st')
def iq(self):
return self.get_primary_attr('iq')
def dx(self):
return self.get_primary_attr('dx')
def ht(self):
return self.get_primary_attr('ht')
def will(self):
return self.get_primary_attr('will')
def fright_check(self):
return self.get_primary_attr('fright_check')
def per(self):
return self.get_primary_attr('per')
def vision(self):
return self.get_primary_attr('vision')
def hearing(self):
return self.get_primary_attr('hearing')
def taste_smell(self):
return self.get_primary_attr('taste_smell')
def touch(self):
return self.get_primary_attr('touch')
def basic_move(self):
return self.get_primary_attr('basic_move')
def basic_speed(self):
return self.get_primary_attr('basic_speed')
def hp(self):
return self.get_primary_attr('hp')
def fp(self):
return self.get_primary_attr('fp')
def weight_carried(self):
items = [ i['calc']['extended_weight'] for i in self.details['equipment'] ]
total_weight = 0
for i in items:
total_weight += float(i.split()[0])
return total_weight
def enc_level(self):
if self.weight_carried() <= self.basic_lift():
return 0
elif self.weight_carried() <= self.basic_lift() * 2:
return 1
elif self.weight_carried() <= self.basic_lift() * 3:
return 2
elif self.weight_carried() <= self.basic_lift() * 6:
return 3
else:
return 4
def enc_levels(self):
enc_level = self.enc_level()
dodge = self.details["calc"]["dodge"][0]
basic_move = self.basic_move()["value"]
return [
{ "max_load": round(self.basic_lift()), "move": basic_move, "dodge": dodge, "current": "current" * (enc_level == 0) },
{ "max_load": round(self.basic_lift()) * 2, "move": basic_move -2, "dodge": dodge -1, "current": "current" * (enc_level == 1) },
{ "max_load": round(self.basic_lift()) * 3, "move": basic_move -3, "dodge": dodge -2, "current": "current" * (enc_level == 2) },
{ "max_load": round(self.basic_lift()) * 6, "move": basic_move -4, "dodge": dodge -3, "current": "current" * (enc_level == 3) },
{ "max_load": round(self.basic_lift()) * 10, "move": basic_move -5, "dodge": dodge -4, "current": "current" * (enc_level == 4) },
]
def basic_lift(self):
return float(self.details["calc"]["basic_lift"].split()[0])
def weight_unit(self):
return self.details['settings']['default_weight_units']
def lift_table(self):
return [
{"value": round(self.basic_lift()), "label":"Basic Lift"},
{"value": round(self.basic_lift()) * 2, "label": "One-Handed Lift" },
{"value": round(self.basic_lift()) * 8, "label":"Two-Handed Lift"},
{"value": round(self.basic_lift()) * 12, "label":"Shove &ampersand; Knock Over"},
{"value": round(self.basic_lift()) * 24, "label":"Running Shove & Knock Over"},
{"value": round(self.basic_lift()) * 15, "label":"Carry on Back"},
{"value": round(self.basic_lift()) * 50, "label":"Shift Slightly"},
]
def reaction_modifiers(self):
modifiers = []
for a in self.details.get('advantages',[]):
if "features" in a:
for f in a["features"]:
if f["type"] == "reaction_bonus":
modifiers.append(f)
return modifiers
def conditional_modifiers(self):
modifiers = []
for a in self.details.get('advantages', []):
if "features" in a:
for f in a["features"]:
if f["type"] == "conditional_modifier":
modifiers.append(f)
return modifiers
def weapons(self):
return [ w for w in self.details['equipment'] if "weapons" in w] + \
[ a for a in self.details.get("advantages",[]) if "weapons" in a ] + \
[ a for a in self.details.get("traits",[]) if "weapons" in a ] + \
[ s for s in self.details.get("spells", []) if "weapons" in s ]
def melee_weapons(self):
mw = []
for item in self.weapons():
for weapon in item["weapons"]:
if weapon["type"] == "melee_weapon":
if "description" in item:
name = item["description"]
else:
name = item["name"]
mw.append(
{
"name":name,
"usage": weapon["usage"],
"skill_level":weapon["calc"].get("level", 0),
"parry":weapon["calc"].get("parry", "No"),
"block":weapon["calc"].get("block", "No"),
"damage":weapon["calc"]["damage"],
"reach":weapon["calc"].get("reach", ""),
"strength":weapon.get("strength", " ")
}
)
return mw
def ranged_weapons(self):
def muscle_range(wpn_range):
if wpn_range.startswith("x"):
ranges = wpn_range.split("/")
new_ranges = []
for wpn_range in ranges:
wpn_range = float(wpn_range[1:])
wpn_range = wpn_range * self.st()["value"]
wpn_range = str(wpn_range)
wpn_range = wpn_range[:wpn_range.index(".")]
new_ranges.append(wpn_range)
return "/".join(new_ranges)
else:
return wpn_range
rw = []
for item in self.weapons():
for weapon in item["weapons"]:
if weapon["type"] == "ranged_weapon":
if "description" in item:
name = item["description"]
else:
name = item["name"]
rw.append(
{
"name":name,
"bulk": weapon.get("bulk", " "),
"usage": weapon.get("usage", " "),
"skill_level":weapon["calc"]["level"],
"damage":weapon["calc"]["damage"],
"strength":weapon.get("strength", " "),
"acc":weapon.get("accuracy", 0),
"range": muscle_range(weapon.get("range", " ")),
"rof": weapon.get("rate_of_fire", " "),
"shots": weapon.get("shots", " "),
"recoil": weapon.get("recoil", " ")
}
)
return rw
def traits(self):
traits = []
for advantage in self.details.get("advantages",[]):
cost = advantage["calc"]["points"]
name = advantage["name"]
if "categories" in advantage and "Language" in advantage["categories"]:
levels = [ m for m in advantage['modifiers'] if "disabled" not in m ]
notes = ""
for level in levels:
notes += f"{level['name']}"
if "notes" in level:
notes += f" ({level['notes']}); "
else:
notes += "; "
notes = notes[:-2]
elif "notes" in advantage:
notes = advantage["notes"]
elif "modifiers" in advantage:
notes = [ m for m in advantage['modifiers'] if m["cost"] == cost ][0]["name"]
else:
notes = ""
traits.append({"name":name, "notes":notes, "points":cost, "reference":advantage["reference"]})
return traits
def spells(self):
def get_casting_details(spell):
level = spell['calc']['level']
if level < 10:
descr = (
"Ritual: Need both hands and both feet "
"free, and must speak .Time: 2x."
)
elif level <15:
descr = (
"Ritual: Must speak a few quiet words "
"and make a gesture."
)
elif level < 20:
descr = (
"Ritual: Must speak a word or two or make "
"a small gesture. May move one yard per second "
"while concentrating. Cost: -1."
)
elif level < 25:
descr = "Ritual: None. Time: / 2 (round up). Cost: -2."
elif level < 30:
descr = (
"Ritual: None. Time: / 4 (round up). Cost: -3."
)
else:
delta = int((level - 25) / 5)
power = 2 + delta
divisor = 2**power
cost = 3 + delta
descr = (
f"Ritual: None. Time: / {power} round up. Cost: "
f"-{cost}"
)
return descr
spells = []
for spell in self.details.get('spells',[]):
notes = (
f"{get_casting_details(spell)}<br> Class: {spell['spell_class']}; "
f"Cost: {spell['casting_cost']}; Maintain: {spell['maintenance_cost']}; Time: {spell['casting_time']}; Duration: {spell['duration']}; "
)
spells.append(
{
'name': spell["name"],
"college":", ".join(spell["college"]),
"level": spell["calc"]["level"],
"rsl":spell["calc"]["rsl"],
"points":spell["points"],
"reference":spell["reference"],
"notes":notes
}
)
return spells
def skills(self):
skills = []
for skill in self.details.get('skills',[]):
skills.append({'name': skill["name"], "level": skill["calc"]["level"], "rsl":skill["calc"]["rsl"], "points":skill["points"], "reference":skill["reference"] })
return skills
def equipment(self):
def get_children(parent, level=1):
children = []
for item in parent["children"]:
equipment = {
"name": item["description"],
"uses":"",
"tech_level":item["tech_level"],
"lc":"",
"value":item["value"],
"weight":item["weight"],
"quantity":item.get("quantity",1),
"ref":item["reference"],
"ext_weight": item["calc"]["extended_weight"],
"ext_value": item["calc"]["extended_value"],
"notes": item.get("notes",""),
"equipped": item["equipped"],
"level": level
}
children.append(equipment)
if "children" in item:
children += get_children(item, level +1)
return children
equipment = self.details['equipment']
equipment_list = []
for item in equipment:
equipment_list.append(
{
"name": item["description"],
"uses":"",
"tech_level":item["tech_level"],
"lc":"",
"level": 0,
"value":item["value"],
"weight":item["weight"],
"quantity":item.get("quantity",1),
"ref":item.get("reference",""),
"ext_weight": item["calc"]["extended_weight"],
"ext_value": item["calc"]["extended_value"],
"notes": item.get("notes",""),
"equipped": item["equipped"]
}
)
if 'children' in item:
equipment_list += get_children(item)
return equipment_list
def hit_locations(self):
try:
# v2
return self.details["settings"]["hit_locations"]["locations"]
except KeyError:
# v4
return self.details["settings"]["body_type"]["locations"]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<ul>
{% for character in characters %}
<li>
<a href="{% url 'details' character.uuid %}">{{ character.name }}</a>
<a href="{% url 'download' character.uuid %}">Download</a>
</li>
{% endfor %}
</ul>
<a href="{% url 'upload' %}">Upload new character</a>
<form method="POST" enctype="multipart/form-data" action="{% url 'index' %}">
{% csrf_token %}
{{ form }}
<input type="submit" value="Upload">
</form>
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" enctype="multipart/form-data" action="{% url 'upload' %}">
{% csrf_token %}
{{ form }}
</form>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<h3>Welcome to the character store</h3>
<p<This is a webapp for storing characters.<p>
<p>Currently it works with GURPS, specifically characters created by GCS</p>
{% endblock %}

View file

@ -0,0 +1,48 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/gurps">GURPS</a></li>
<li class="nav-item"><a class="nav-link disabled" aria-current="page" href="/rq">RuneQuest</a></li>
<li class="nav-item">
<a class="nav-link" href="/admin">Admin</a>
</li>
<!--<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
-->
</div>
<div class="navbar-text float-end">
{% if user.is_authenticated %}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit">logout</button>
</form>
{% else %}
<a class="nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</div>
</div>
</nav>

View file

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{# Assumes you set up the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
{% endblock %}

3
gurps_character/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
gurps_character/urls.py Normal file
View file

@ -0,0 +1,10 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("details/<str:uuid>/", views.details, name="details"),
path("upload", views.upload_file, name="upload"),
path("download/<str:uuid>/", views.download, name="download"),
]

67
gurps_character/views.py Normal file
View file

@ -0,0 +1,67 @@
import json
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import GURPSCharacter
from .forms import UploadFileForm
def index(request):
characters = GURPSCharacter.objects.all()
context = {"characters":characters}
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES["file"])
return HttpResponseRedirect(reverse("index"))
else:
form = UploadFileForm()
context['form'] = form
return render(request, "characters/list.html", context)
def details(request, uuid):
character = GURPSCharacter.objects.get(uuid=uuid)
context = {"character": character}
#import bpdb;bpdb.set_trace()
return render(request, "characters/embedded.html", context)
def upload_file(request):
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
handle_uploaded_file(request.FILES["file"])
return HttpResponseRedirect("/success/url/")
else:
form = UploadFileForm()
return render(request, "characters/upload.html", {"form": form})
def handle_uploaded_file(f):
import bpdb;bpdb.set_trace()
f.seek(0) # We read the file in the validator
data = json.loads(f.read())
uuid = data['id']
name = data["profile"]["name"]
try:
character = GURPSCharacter.objects.get(uuid=uuid)
character.details = data
character.name = name
character.save()
except GURPSCharacter.DoesNotExist:
character = GURPSCharacter(uuid=uuid, name=name, details = data)
character.save()
def download(irequest, uuid):
mime_type = "application/x-gcs-gcs"
character = GURPSCharacter.objects.get(uuid=uuid)
response = HttpResponse(json.dumps(character.details), content_type=mime_type)
response['Content-Disposition'] = "attachment; filename=%s.gcs" % character.name
return response