Tidy up after a long absence

This commit is contained in:
Neill Cox 2025-03-09 17:58:06 +11:00
parent 3931d2ccbd
commit fea7c0eeb9
28 changed files with 2444 additions and 376 deletions

2
.gitignore vendored
View file

@ -4,5 +4,7 @@ db.sqlite3
.hidden_notes.txt .hidden_notes.txt
env/ env/
*pyc *pyc
*.py[cod]
__pycache__/ __pycache__/
xxxthemexxx/ xxxthemexxx/
gurps_vars.sh

View file

@ -21,7 +21,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-x%rs&gy9iphje-l+!^!g@s@w$4@oc0^honvnr-!edwm+uujiu2" SECRET_KEY = (
"django-insecure-x%rs&gy9iphje-l+!^!g@s@w$4@oc0^honvnr-!edwm+uujiu2"
)
# 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
@ -30,20 +32,17 @@ ALLOWED_HOSTS = [
"gcs.neill.id.au", "gcs.neill.id.au",
"localhost", "localhost",
"127.0.0.1", "127.0.0.1",
] ]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
"https://gcs.neill.id.au", "https://gcs.neill.id.au",
] ]
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
# Needed to log in by username in Django admin, regardless of `allauth` # Needed to log in by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend', "django.contrib.auth.backends.ModelBackend",
# `allauth` specific authentication methods, such as login by email # `allauth` specific authentication methods, such as login by email
'allauth.account.auth_backends.AuthenticationBackend', "allauth.account.auth_backends.AuthenticationBackend",
] ]
# Application definition # Application definition
@ -55,20 +54,18 @@ INSTALLED_APPS = [
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"allauth",
'allauth', "allauth.account",
'allauth.account', "allauth.socialaccount",
'allauth.socialaccount',
# ... include the providers you want to enable: # ... include the providers you want to enable:
'allauth.socialaccount.providers.amazon', "allauth.socialaccount.providers.amazon",
'allauth.socialaccount.providers.apple', "allauth.socialaccount.providers.apple",
'allauth.socialaccount.providers.google', "allauth.socialaccount.providers.google",
"django.contrib.staticfiles", "django.contrib.staticfiles",
# "tailwind", # "tailwind",
# "theme", # "theme",
"django_browser_reload", "django_browser_reload",
'django_bootstrap_icons', "django_bootstrap_icons",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -97,7 +94,7 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
'django.template.context_processors.request', "django.template.context_processors.request",
], ],
}, },
}, },
@ -169,7 +166,7 @@ DATABASES = {
} }
} }
TAILWIND_APP_NAME = 'theme' TAILWIND_APP_NAME = "theme"
INTERNAL_IPS = [ INTERNAL_IPS = [
"127.0.0.1", "127.0.0.1",

29
docs/design.md Normal file
View file

@ -0,0 +1,29 @@
# Design Notes
## Stories
As a user I want to upload a character so my GM can see it
As a user I want to update a character so my GM can see it
As a GM I want to invite players
As a GM I want to review characters and pssibly appreove them
As a GM I want to remove players from my campaign
As a GM I want to send feedback on a character to a players
As an admin I want to create campaigns
As an admin I want to invite people to GM a campaign
As an admin I want to remove players
As an admin I want to remove campaigns
As an admin I want to remove GM from a campaign
As a player I want to add session notes to a campaign. (It would be really cool if multiple players could edit these notes. Bonus points for uploading images)

View file

@ -1,8 +1,13 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. # Register your models here.
from .models import GURPSCharacter, GameSystem, Campaign from .models import GURPSCharacter, GameSystem, Campaign, GM, Player, CampaignPlayer
admin.site.register(GURPSCharacter) admin.site.register(GURPSCharacter)
admin.site.register(GameSystem) admin.site.register(GameSystem)
admin.site.register(Campaign) admin.site.register(Campaign)
admin.site.register(GM)
admin.site.register(Player)
admin.site.register(CampaignPlayer)

View file

@ -13,14 +13,14 @@ def validate_file(value):
raise ValidationError("Not a GCS file - json decode") raise ValidationError("Not a GCS file - json decode")
try: try:
version = data['version'] version = data["version"]
except KeyError: except KeyError:
import bpdb;bpdb.set_trace()
raise ValidationError("Not a GCS file - key error") raise ValidationError("Not a GCS file - key error")
if version < 4: if version < 4:
raise ValidationError( raise ValidationError(
f"The file version ({version}) is too old. Please use a newer version (5.20.3) of GCS." f"The file version ({version}) is too old. Please use a newer "
"version (5.20.3) of GCS."
) )

View file

@ -1,24 +1,24 @@
from django.contrib.auth.models import User from django.db import connection
from django.core.management import BaseCommand from django.core.management import BaseCommand
def create_users(): def drop_tables():
user = User.objects.create_user( with connection.cursor() as cursor:
username="neill", for table in [
is_superuser=True, "gurps_character",
first_name="Neill", "gurps_character_campaign",
last_name="Cox", "gurps_character_campaign_gm",
email="neill@neill.id.au", "gurps_character_campaignplayer",
is_staff=True, "gurps_character_gm",
is_active=True, "gurps_character_player",
date_joined="2024-07-31T00:00:00.000Z", "gurps_character_gamesystem",
password="password", "gurps_character_gurpscharacter"]:
) cursor.execute(f'DROP TABLE IF EXISTS {table} cascade')
# user.save() cursor.execute("DELETE FROM django_migrations WHERE app = 'gurps_character'")
class Command(BaseCommand): class Command(BaseCommand):
help = "Load some initial data" help = "Drop all the tables!"
def handle(self, *args, **options): def handle(self, *args, **options):
create_users() drop_tables()

View file

@ -1,14 +1,120 @@
# Generated by Django 5.0.1 on 2024-01-08 10:25 # Generated by Django 5.0.6 on 2024-09-07 01:08
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ 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)),
],
),
migrations.CreateModel(
name="Campaign",
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)),
(
"game_system",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.gamesystem",
),
),
],
),
migrations.CreateModel(
name="GM",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"campaign",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="campaign",
to="gurps_character.campaign",
),
),
(
"gm",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name="campaign",
name="gm",
field=models.ManyToManyField(
related_name="GM", to="gurps_character.gm"
),
),
migrations.CreateModel(
name="Player",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.CharField(max_length=255, unique=True),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel( migrations.CreateModel(
name="GURPSCharacter", name="GURPSCharacter",
fields=[ fields=[
@ -23,6 +129,61 @@ class Migration(migrations.Migration):
), ),
("uuid", models.CharField(max_length=128, unique=True)), ("uuid", models.CharField(max_length=128, unique=True)),
("name", models.CharField(max_length=255, unique=True)), ("name", models.CharField(max_length=255, unique=True)),
("details", models.JSONField()),
(
"campaign",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.campaign",
),
),
(
"player",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.player",
),
),
],
),
migrations.CreateModel(
name="CampaignPlayer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"status",
models.CharField(
choices=[
("I", "Invited"),
("A", "Accepted"),
("D", "Declined"),
]
),
),
(
"campaign",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.campaign",
),
),
(
"player",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.player",
),
),
], ],
), ),
] ]

View file

@ -0,0 +1,25 @@
# Generated by Django 5.0.6 on 2024-09-07 01:22
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="gm",
name="gm",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -1,18 +0,0 @@
# 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,67 @@
# Generated by Django 5.1.7 on 2025-03-08 09:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0002_alter_gm_gm"),
]
operations = [
migrations.AlterField(
model_name="campaign",
name="gm",
field=models.ManyToManyField(
blank=True, related_name="GM", to="gurps_character.gm"
),
),
migrations.AlterField(
model_name="gm",
name="campaign",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="campaign",
to="gurps_character.campaign",
),
),
migrations.AlterField(
model_name="player",
name="status",
field=models.CharField(
choices=[
("I", "Invited"),
("A", "Accepted"),
("D", "Declined"),
("R", "Requested"),
]
),
),
migrations.CreateModel(
name="CampaignNotes",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("timestamp", models.DateTimeField(auto_now_add=True)),
("notes", models.TextField(null=True)),
(
"campaign",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.campaign",
),
),
],
),
]

View file

@ -1,28 +0,0 @@
# 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

@ -1,57 +0,0 @@
# Generated by Django 5.0.6 on 2024-07-14 09:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0003_gamesystem"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="gurpscharacter",
name="player",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
migrations.CreateModel(
name="Campaign",
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)),
(
"game_system",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.gamesystem",
),
),
],
),
migrations.AddField(
model_name="gurpscharacter",
name="campaign",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="gurps_character.campaign",
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 5.0.6 on 2024-07-14 09:25
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("gurps_character", "0004_gurpscharacter_player_campaign_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="campaign",
name="gm",
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
]

View file

@ -1,5 +1,6 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
# Create your models here. # Create your models here.
@ -12,120 +13,194 @@ class GameSystem(models.Model):
class GM(models.Model): class GM(models.Model):
gm = models.ForeignKey(User, on_delete=models.CASCADE) gm = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
campaign = models.ForeignKey("Campaign", null=True, on_delete=models.CASCADE, related_name='campaign') campaign = models.ForeignKey(
"Campaign",
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="campaign",
)
def __str__(self):
return self.campaign.name + " - " + self.gm.get_full_name()
class Player(models.Model): class Player(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
status = models.CharField(max_length=255, unique=True) status = models.CharField(
choices={"I": "Invited", "A": "Accepted", "D": "Declined", "R": "Requested"}
)
# def __str__(self):
# class Players(models.Model): return self.user.get_full_name() + " - " + self.get_status_display()
# player = models.ForeignKey(User, on_delete=models.CASCADE)
# status = models.CharField(max_length=255, unique=True)
class Campaign(models.Model): class Campaign(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
description = models.TextField(null=True) description = models.TextField(null=True)
game_system = models.ForeignKey(GameSystem, on_delete=models.CASCADE) game_system = models.ForeignKey(
gm = models.ManyToManyField('GM', related_name='GM') GameSystem, on_delete=models.CASCADE
player = models.ManyToManyField('Player') )
gm = models.ManyToManyField("GM", related_name="GM", blank=True)
# player = models.ManyToManyField('Player')
def __str__(self): def __str__(self):
return self.name return self.name
class CampaignPlayer(models.Model):
campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE)
player = models.ForeignKey(
Player, on_delete=models.CASCADE
) # TODO: Check this behaviour
status = models.CharField(
choices={"I": "Invited", "A": "Accepted", "D": "Declined"}
) # This should be one of invited, accepted, rejected?
def __str__(self):
return self.campaign.name + " - " + self.player.user.get_full_name() + " - " + self.get_status_display()
class CampaignNotes(models.Model):
campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
notes = models.TextField(null=True)
class GURPSCharacter(models.Model): class GURPSCharacter(models.Model):
uuid = models.CharField(max_length=128, unique=True) uuid = models.CharField(max_length=128, unique=True)
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
player = models.ForeignKey(Player, null=True, on_delete=models.CASCADE) player = models.ForeignKey(
campaign = models.ForeignKey(Campaign, null=True, on_delete=models.CASCADE) Player, null=True, on_delete=models.CASCADE
)
campaign = models.ForeignKey(
Campaign, null=True, on_delete=models.CASCADE
)
details = models.JSONField() details = models.JSONField()
# owner => user
# campaign => campaign
def __str__(self): def __str__(self):
return self.name + " Player " + str(self.player) + " Campaign " + str(self.campaign) return (
str(self.name)
+ " Player "
+ str(self.player)
+ " Campaign "
+ str(self.campaign)
)
def adv_points(self): def adv_points(self):
return sum([a['calc']['points'] for a in self.details.get('advantages', []) if a['calc']['points'] > 0]) return sum(
[
a["calc"]["points"]
for a in self.details.get("advantages", [])
if a["calc"]["points"] > 0
]
)
def disadv_points(self): def disadv_points(self):
return sum([a['calc']['points'] for a in self.details.get('advantages', []) if a['calc']['points'] < -1]) return sum(
[
a["calc"]["points"]
for a in self.details.get("advantages", [])
if a["calc"]["points"] < -1
]
)
def quirks_points(self): def quirks_points(self):
return sum([a['calc']['points'] for a in self.details.get('advantages', []) if a['calc']['points'] == -1]) return sum(
[
a["calc"]["points"]
for a in self.details.get("advantages", [])
if a["calc"]["points"] == -1
]
)
def attr_points(self): def attr_points(self):
return sum([a['calc']['points'] for a in self.details['attributes']]) return sum(
[a["calc"]["points"] for a in self.details["attributes"]]
)
def skills_points(self): def skills_points(self):
return sum([s['points'] for s in self.details.get('skills', [])]) return sum(
[s["points"] for s in self.details.get("skills", [])]
)
def spells_points(self): def spells_points(self):
return sum([s['points'] for s in self.details.get('spells', [])]) return sum(
[s["points"] for s in self.details.get("spells", [])]
)
def race_points(self): def race_points(self):
return sum([s['points'] for s in self.details.get('race', [])]) # Are we sure? # Are we sure?
return sum([s["points"] for s in self.details.get("race", [])])
def unspent_points(self): def unspent_points(self):
return self.details['total_points'] - self.adv_points() - \ return (
self.disadv_points() - self.attr_points() - \ self.details["total_points"]
self.skills_points() - self.spells_points() - \ - self.adv_points()
self.race_points() - self.quirks_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): def get_primary_attr(self, attr_id):
return [a['calc'] for a in self.details['attributes'] if a['attr_id'] == attr_id][0] return [
a["calc"]
for a in self.details["attributes"]
if a["attr_id"] == attr_id
][0]
def st(self): def st(self):
return self.get_primary_attr('st') return self.get_primary_attr("st")
def iq(self): def iq(self):
return self.get_primary_attr('iq') return self.get_primary_attr("iq")
def dx(self): def dx(self):
return self.get_primary_attr('dx') return self.get_primary_attr("dx")
def ht(self): def ht(self):
return self.get_primary_attr('ht') return self.get_primary_attr("ht")
def will(self): def will(self):
return self.get_primary_attr('will') return self.get_primary_attr("will")
def fright_check(self): def fright_check(self):
return self.get_primary_attr('fright_check') return self.get_primary_attr("fright_check")
def per(self): def per(self):
return self.get_primary_attr('per') return self.get_primary_attr("per")
def vision(self): def vision(self):
return self.get_primary_attr('vision') return self.get_primary_attr("vision")
def hearing(self): def hearing(self):
return self.get_primary_attr('hearing') return self.get_primary_attr("hearing")
def taste_smell(self): def taste_smell(self):
return self.get_primary_attr('taste_smell') return self.get_primary_attr("taste_smell")
def touch(self): def touch(self):
return self.get_primary_attr('touch') return self.get_primary_attr("touch")
def basic_move(self): def basic_move(self):
return self.get_primary_attr('basic_move') return self.get_primary_attr("basic_move")
def basic_speed(self): def basic_speed(self):
return self.get_primary_attr('basic_speed') return self.get_primary_attr("basic_speed")
def hp(self): def hp(self):
return self.get_primary_attr('hp') return self.get_primary_attr("hp")
def fp(self): def fp(self):
return self.get_primary_attr('fp') return self.get_primary_attr("fp")
def weight_carried(self): def weight_carried(self):
items = [i['calc']['extended_weight'] for i in self.details['equipment']] items = [
i["calc"]["extended_weight"]
for i in self.details["equipment"]
]
total_weight = 0 total_weight = 0
for i in items: for i in items:
@ -154,31 +229,31 @@ class GURPSCharacter(models.Model):
"max_load": round(self.basic_lift()), "max_load": round(self.basic_lift()),
"move": basic_move, "move": basic_move,
"dodge": dodge, "dodge": dodge,
"current": "current" * (enc_level == 0) "current": "current" * (enc_level == 0),
}, },
{ {
"max_load": round(self.basic_lift()) * 2, "max_load": round(self.basic_lift()) * 2,
"move": basic_move - 2, "move": basic_move - 2,
"dodge": dodge - 1, "dodge": dodge - 1,
"current": "current" * (enc_level == 1) "current": "current" * (enc_level == 1),
}, },
{ {
"max_load": round(self.basic_lift()) * 3, "max_load": round(self.basic_lift()) * 3,
"move": basic_move - 3, "move": basic_move - 3,
"dodge": dodge - 2, "dodge": dodge - 2,
"current": "current" * (enc_level == 2) "current": "current" * (enc_level == 2),
}, },
{ {
"max_load": round(self.basic_lift()) * 6, "max_load": round(self.basic_lift()) * 6,
"move": basic_move - 4, "move": basic_move - 4,
"dodge": dodge - 3, "dodge": dodge - 3,
"current": "current" * (enc_level == 3) "current": "current" * (enc_level == 3),
}, },
{ {
"max_load": round(self.basic_lift()) * 10, "max_load": round(self.basic_lift()) * 10,
"move": basic_move - 5, "move": basic_move - 5,
"dodge": dodge - 4, "dodge": dodge - 4,
"current": "current" * (enc_level == 4) "current": "current" * (enc_level == 4),
}, },
] ]
@ -186,23 +261,41 @@ class GURPSCharacter(models.Model):
return float(self.details["calc"]["basic_lift"].split()[0]) return float(self.details["calc"]["basic_lift"].split()[0])
def weight_unit(self): def weight_unit(self):
return self.details['settings']['default_weight_units'] return self.details["settings"]["default_weight_units"]
def lift_table(self): def lift_table(self):
return [ return [
{"value": round(self.basic_lift()), "label": "Basic Lift"}, {"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()) * 2,
{"value": round(self.basic_lift()) * 12, "label": "Shove &ampersand; Knock Over"}, "label": "One-Handed Lift",
{"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"}, "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): def reaction_modifiers(self):
modifiers = [] modifiers = []
for a in self.details.get('advantages', []): for a in self.details.get("advantages", []):
if "features" in a: if "features" in a:
for f in a["features"]: for f in a["features"]:
if f["type"] == "reaction_bonus": if f["type"] == "reaction_bonus":
@ -213,7 +306,7 @@ class GURPSCharacter(models.Model):
def conditional_modifiers(self): def conditional_modifiers(self):
modifiers = [] modifiers = []
for a in self.details.get('advantages', []): for a in self.details.get("advantages", []):
if "features" in a: if "features" in a:
for f in a["features"]: for f in a["features"]:
if f["type"] == "conditional_modifier": if f["type"] == "conditional_modifier":
@ -222,10 +315,24 @@ class GURPSCharacter(models.Model):
return modifiers return modifiers
def weapons(self): def weapons(self):
return [w for w in self.details['equipment'] if "weapons" in w] + \ return (
[a for a in self.details.get("advantages", []) if "weapons" in a] + \ [w for w in self.details["equipment"] if "weapons" in w]
[a for a in self.details.get("traits", []) if "weapons" in a] + \ + [
[s for s in self.details.get("spells", []) if "weapons" in s] 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): def melee_weapons(self):
mw = [] mw = []
@ -240,12 +347,14 @@ class GURPSCharacter(models.Model):
{ {
"name": name, "name": name,
"usage": weapon["usage"], "usage": weapon["usage"],
"skill_level": weapon["calc"].get("level", 0), "skill_level": weapon["calc"].get(
"level", 0
),
"parry": weapon["calc"].get("parry", "No"), "parry": weapon["calc"].get("parry", "No"),
"block": weapon["calc"].get("block", "No"), "block": weapon["calc"].get("block", "No"),
"damage": weapon["calc"]["damage"], "damage": weapon["calc"]["damage"],
"reach": weapon["calc"].get("reach", ""), "reach": weapon["calc"].get("reach", ""),
"strength": weapon.get("strength", " ") "strength": weapon.get("strength", " "),
} }
) )
@ -260,7 +369,7 @@ class GURPSCharacter(models.Model):
wpn_range = float(wpn_range[1:]) wpn_range = float(wpn_range[1:])
wpn_range = wpn_range * self.st()["value"] wpn_range = wpn_range * self.st()["value"]
wpn_range = str(wpn_range) wpn_range = str(wpn_range)
wpn_range = wpn_range[:wpn_range.index(".")] wpn_range = wpn_range[: wpn_range.index(".")]
new_ranges.append(wpn_range) new_ranges.append(wpn_range)
return "/".join(new_ranges) return "/".join(new_ranges)
@ -284,10 +393,12 @@ class GURPSCharacter(models.Model):
"damage": weapon["calc"]["damage"], "damage": weapon["calc"]["damage"],
"strength": weapon.get("strength", " "), "strength": weapon.get("strength", " "),
"acc": weapon.get("accuracy", 0), "acc": weapon.get("accuracy", 0),
"range": muscle_range(weapon.get("range", " ")), "range": muscle_range(
weapon.get("range", " ")
),
"rof": weapon.get("rate_of_fire", " "), "rof": weapon.get("rate_of_fire", " "),
"shots": weapon.get("shots", " "), "shots": weapon.get("shots", " "),
"recoil": weapon.get("recoil", " ") "recoil": weapon.get("recoil", " "),
} }
) )
@ -299,8 +410,15 @@ class GURPSCharacter(models.Model):
cost = advantage["calc"]["points"] cost = advantage["calc"]["points"]
name = advantage["name"] name = advantage["name"]
if "categories" in advantage and "Language" in advantage["categories"]: if (
levels = [m for m in advantage['modifiers'] if "disabled" not in m] "categories" in advantage
and "Language" in advantage["categories"]
):
levels = [
m
for m in advantage["modifiers"]
if "disabled" not in m
]
notes = "" notes = ""
for level in levels: for level in levels:
@ -315,17 +433,28 @@ class GURPSCharacter(models.Model):
elif "notes" in advantage: elif "notes" in advantage:
notes = advantage["notes"] notes = advantage["notes"]
elif "modifiers" in advantage: elif "modifiers" in advantage:
notes = [m for m in advantage['modifiers'] if m["cost"] == cost][0]["name"] notes = [
m
for m in advantage["modifiers"]
if m["cost"] == cost
][0]["name"]
else: else:
notes = "" notes = ""
traits.append({"name": name, "notes": notes, "points": cost, "reference": advantage["reference"]}) traits.append(
{
"name": name,
"notes": notes,
"points": cost,
"reference": advantage["reference"],
}
)
return traits return traits
def spells(self): def spells(self):
def get_casting_details(spell_details): def get_casting_details(spell_details):
level = spell_details['calc']['level'] level = spell_details["calc"]["level"]
if level < 10: if level < 10:
descr = ( descr = (
@ -346,51 +475,55 @@ class GURPSCharacter(models.Model):
elif level < 25: elif level < 25:
descr = "Ritual: None. Time: / 2 (round up). Cost: -2." descr = "Ritual: None. Time: / 2 (round up). Cost: -2."
elif level < 30: elif level < 30:
descr = ( descr = "Ritual: None. Time: / 4 (round up). Cost: -3."
"Ritual: None. Time: / 4 (round up). Cost: -3."
)
else: else:
delta = int((level - 25) / 5) delta = int((level - 25) / 5)
power = 2 + delta power = 2 + delta
# divisor = 2**power # divisor = 2**power
cost = 3 + delta cost = 3 + delta
descr = ( descr = (
f"Ritual: None. Time: / {power} round up. Cost: " f"Ritual: None. Time: / {
power} round up. Cost: "
f"-{cost}" f"-{cost}"
) )
return descr return descr
spells = [] spells = []
for spell in self.details.get('spells', []): for spell in self.details.get("spells", []):
notes = ( notes = (
f"{get_casting_details(spell)}<br> Class: {spell['spell_class']}; " f"{get_casting_details(spell)}<br> Class: {
f"Cost: {spell['casting_cost']}; Maintain: {spell['maintenance_cost']};" spell['spell_class']}; "
f" Time: {spell['casting_time']}; Duration: {spell['duration']}; " f"Cost: {spell['casting_cost']}; Maintain: {
spell['maintenance_cost']};"
f" Time: {spell['casting_time']}; Duration: {
spell['duration']}; "
) )
spells.append( spells.append(
{ {
'name': spell["name"], "name": spell["name"],
"college": ", ".join(spell["college"]), "college": ", ".join(spell["college"]),
"level": spell["calc"]["level"], "level": spell["calc"]["level"],
"rsl": spell["calc"]["rsl"], "rsl": spell["calc"]["rsl"],
"points": spell["points"], "points": spell["points"],
"reference": spell["reference"], "reference": spell["reference"],
"notes": notes "notes": notes,
} }
) )
return spells return spells
def skills(self): def skills(self):
skills = [] skills = []
for skill in self.details.get('skills', []): for skill in self.details.get("skills", []):
skills.append({ skills.append(
'name': skill["name"], {
"name": skill["name"],
"level": skill["calc"]["level"], "level": skill["calc"]["level"],
"rsl": skill["calc"]["rsl"], "rsl": skill["calc"]["rsl"],
"points": skill["points"], "points": skill["points"],
"reference": skill["reference"] "reference": skill["reference"],
}) }
)
return skills return skills
def equipment(self): def equipment(self):
@ -407,11 +540,13 @@ class GURPSCharacter(models.Model):
"weight": item_details["weight"], "weight": item_details["weight"],
"quantity": item_details.get("quantity", 1), "quantity": item_details.get("quantity", 1),
"ref": item_details["reference"], "ref": item_details["reference"],
"ext_weight": item_details["calc"]["extended_weight"], "ext_weight": item_details["calc"][
"extended_weight"
],
"ext_value": item_details["calc"]["extended_value"], "ext_value": item_details["calc"]["extended_value"],
"notes": item_details.get("notes", ""), "notes": item_details.get("notes", ""),
"equipped": item_details["equipped"], "equipped": item_details["equipped"],
"level": level "level": level,
} }
children.append(equipment_details) children.append(equipment_details)
@ -419,7 +554,8 @@ class GURPSCharacter(models.Model):
children += get_children(item_details, level + 1) children += get_children(item_details, level + 1)
return children return children
equipment = self.details['equipment']
equipment = self.details["equipment"]
equipment_list = [] equipment_list = []
for item in equipment: for item in equipment:
@ -437,11 +573,11 @@ class GURPSCharacter(models.Model):
"ext_weight": item["calc"]["extended_weight"], "ext_weight": item["calc"]["extended_weight"],
"ext_value": item["calc"]["extended_value"], "ext_value": item["calc"]["extended_value"],
"notes": item.get("notes", ""), "notes": item.get("notes", ""),
"equipped": item["equipped"] "equipped": item["equipped"],
} }
) )
if 'children' in item: if "children" in item:
equipment_list += get_children(item) equipment_list += get_children(item)
return equipment_list return equipment_list
@ -449,7 +585,9 @@ class GURPSCharacter(models.Model):
def hit_locations(self): def hit_locations(self):
try: try:
# v2 # v2
return self.details["settings"]["hit_locations"]["locations"] return self.details["settings"]["hit_locations"][
"locations"
]
except KeyError: except KeyError:
# v4 # v4
return self.details["settings"]["body_type"]["locations"] return self.details["settings"]["body_type"]["locations"]

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<h1>{{ campaign.name }}</h1>
<h2>Players</h2>
<ul>
{% for player in campaign.campaignplayer_set.all %}
<li><a href="#">{{ player }}</a></li>
{% endfor %}
</ul>
<h2>Characters</h2>
<ul>
{% for character in campaign.gurpscharacter_set.all %}
<a href="#">{{ character.name }} </a>[ <a href="#"> {{ character.player.user.get_full_name }} </a>]
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<ul>
{% for campaign in campaign %}
<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

@ -8,11 +8,11 @@
{% if user.is_authenticated %} {% if user.is_authenticated %}
<h1>Welcome {% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}</h1> <h1>Welcome {% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}</h1>
{% if user.campaign_set.all %} {% if user.gm_set.all %}
<h2>Campaigns you run</h2> <h2>Campaigns you run</h2>
<ul> <ul>
{% for campaign in user.campaign_set.all %} {% for gm in user.gm_set.all %}
<li><a href="{% url 'campaign' campaign.id %}">{{ campaign.name }}</a></li> <li><a href="{% url 'campaign' gm.campaign.id %}">{{ gm.campaign.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -20,7 +20,7 @@
<h2>Characters you play</h2> <h2>Characters you play</h2>
<ul> <ul>
{% for character in user.gurpscharacter_set.all %} {% for character in user.player.gurpscharacter_set.all %}
<li> <li>
{{ character.name }} in {{ character.campaign }} {{ character.name }} in {{ character.campaign }}
<a href="{% url 'details' character.uuid %}" title="view {{ character.name }}">{% bs_icon 'eye' %}</a> <a href="{% url 'details' character.uuid %}" title="view {{ character.name }}">{% bs_icon 'eye' %}</a>

View file

@ -4,7 +4,7 @@ from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from .models import GURPSCharacter from .models import GURPSCharacter, Campaign
from .forms import UploadFileForm from .forms import UploadFileForm
@ -20,7 +20,7 @@ def index(request):
else: else:
form = UploadFileForm() form = UploadFileForm()
context['form'] = form context["form"] = form
return render(request, "characters/list.html", context) return render(request, "characters/list.html", context)
@ -47,7 +47,7 @@ def handle_uploaded_file(f):
f.seek(0) # We read the file in the validator f.seek(0) # We read the file in the validator
data = json.loads(f.read()) data = json.loads(f.read())
uuid = data['id'] uuid = data["id"]
name = data["profile"]["name"] name = data["profile"]["name"]
try: try:
@ -60,19 +60,26 @@ def handle_uploaded_file(f):
character.save() character.save()
def download(request, uuid): def download(_, uuid):
mime_type = "application/x-gcs-gcs" mime_type = "application/x-gcs-gcs"
character = GURPSCharacter.objects.get(uuid=uuid) character = GURPSCharacter.objects.get(uuid=uuid)
response = HttpResponse(json.dumps(character.details), content_type=mime_type) response = HttpResponse(
response['Content-Disposition'] = "attachment; filename=%s.gcs" % character.name json.dumps(character.details), content_type=mime_type
)
response["Content-Disposition"] = (
"attachment; filename=%s.gcs" % character.name
)
return response return response
def character(request, uuid): def character(request, uuid):
response = HttpResponse("Charcater") response = HttpResponse("Character")
return response return response
def campaign(request, uuid): def campaign(request, uuid):
response = HttpResponse("Campaign") campaign = Campaign.objects.get(id=uuid)
return response
context = {"campaign": campaign}
return render(request, "campaigns/details.html", context)

1705
gurps_fixture.json Normal file

File diff suppressed because it is too large Load diff

14
pyproject.toml Normal file
View file

@ -0,0 +1,14 @@
[tool.black]
line-length = 72
[tool.ruff]
line-length = 72
[tool.autopep8]
max_line_length = 72
[tool.pycodestyle]
max_line_length = 72
[tool.pyright]
typeCheckingMode = "off"

View file

@ -1,6 +1,7 @@
django Django==5.1.7
tailwind psycopg2-binary==2.9.10
django_browser_reload django-environ-0.12.0
psycopg django-allauth[socialaccount]==65.4.1
django-allauth[socialaccount] django-bootstrap-icons==0.9.0
django-bootstrap-icons django-browser-reload==1.18.0

1
requirements_dev.txt Normal file
View file

@ -0,0 +1 @@
pdbpp==0.10.3