Implements web-based character creation.

This commit is contained in:
Johnny 2018-10-05 18:59:55 +00:00
parent 0531afceef
commit 40f5f283ad
5 changed files with 244 additions and 4 deletions

View file

@ -2,6 +2,7 @@ from django import forms
from django.conf import settings
from django.contrib.auth.forms import UserCreationForm, UsernameField
from evennia.utils import class_from_module
from random import choice, randint
class AccountCreationForm(UserCreationForm):
@ -10,4 +11,132 @@ class AccountCreationForm(UserCreationForm):
fields = ("username", "email")
field_classes = {'username': UsernameField}
email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False)
email = forms.EmailField(help_text="A valid email address. Optional; used for password resets.", required=False)
class CharacterCreationForm(forms.Form):
name = forms.CharField(help_text="The name of your intended character.")
age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.")
description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False)
@classmethod
def assign_attributes(cls, attribute_list, points, min_points, max_points):
"""
Randomly distributes a number of points across the given attributes,
while also ensuring each attribute gets at least a certain amount
and at most a certain amount.
Args:
attribute_list (iterable): List or tuple of attribute names to assign
points to.
points (int): Starting number of points
min_points (int): Least amount of points each attribute should have
max_points (int): Most amount of points each attribute should have
Returns:
spread (dict): Dict of attributes and a point assignment.
"""
num_buckets = len(attribute_list)
point_spread = (x for x in self.random_distribution(points, num_buckets, min_points, max_points))
# For each field, get the point calculation for the next attribute value generated
return {attribute: next(point_spread) for k in attribute_list}
@classmethod
def random_distribution(cls, points, num_buckets, min_points, max_points):
"""
Distributes a set number of points randomly across a number of 'buckets'
while also attempting to ensure each bucket's value finishes within a
certain range.
If your math doesn't add up (you try to distribute 5 points across 100
buckets and insist each bucket has at least 20 points), the algorithm
will return the best spread it could achieve but will not raise an error
(so in this case, 5 random buckets would get 1 point each and that's all).
Args:
points (int): The number of points to distribute.
num_buckets (int): The number of 'buckets' (or stats, skills, etc)
you wish to distribute points to.
min_points (int): The least amount of points each bucket should have.
max_points (int): The most points each bucket should have.
Returns:
buckets (list): List of random point assignments.
"""
buckets = [0 for x in range(num_buckets)]
indices = [i for (i, value) in enumerate(buckets)]
# Do this while we have eligible buckets, points to assign and we haven't
# maxed out all the buckets.
while indices and points and sum(buckets) <= (max_points * num_buckets):
# Pick a random bucket index
index = choice(indices)
# Add to bucket
buckets[index] = buckets[index] + 1
points = points - 1
# Get the indices of eligible buckets
indices = [i for (i, value) in enumerate(buckets) if (value < min_points) or (value < max_points)]
return buckets
class ExtendedCharacterCreationForm(forms.Form):
GENDERS = (
('male', 'Male'),
('female', 'Female'),
('androgynous', 'Androgynous'),
('special', 'Special')
)
RACES = (
('human', 'Human'),
('elf', 'Elf'),
('orc', 'Orc'),
)
CLASSES = (
('civilian', 'Civilian'),
('warrior', 'Warrior'),
('thief', 'Thief'),
('cleric', 'Cleric')
)
PERKS = (
('strong', 'Extra strength'),
('nimble', 'Quick on their toes'),
('diplomatic', 'Fast talker')
)
name = forms.CharField(help_text="The name of your intended character.")
age = forms.IntegerField(min_value=3, max_value=99, help_text="How old your character should be once spawned.")
gender = forms.ChoiceField(choices=GENDERS, help_text="Which end of the multidimensional spectrum does your character most closely align with, in terms of gender?")
race = forms.ChoiceField(choices=RACES, help_text="What race does your character belong to?")
job = forms.ChoiceField(choices=CLASSES, help_text="What profession or role does your character fulfill or is otherwise destined to?")
perks = forms.MultipleChoiceField(choices=PERKS, help_text="What extraordinary abilities does your character possess?")
description = forms.CharField(widget=forms.Textarea(attrs={'rows': 3}), max_length=2048, min_length=160, required=False)
strength = forms.IntegerField(min_value=1, max_value=10)
perception = forms.IntegerField(min_value=1, max_value=10)
intelligence = forms.IntegerField(min_value=1, max_value=10)
dexterity = forms.IntegerField(min_value=1, max_value=10)
charisma = forms.IntegerField(min_value=1, max_value=10)
vitality = forms.IntegerField(min_value=1, max_value=10)
magic = forms.IntegerField(min_value=1, max_value=10)
def __init__(self, *args, **kwargs):
# Do all the normal initizliation stuff that would otherwise be happening
super(ExtendedCharacterCreationForm, self).__init__(*args, **kwargs)
# Given a pool of points, let's randomly distribute them across attributes.
# First get a list of attributes
attributes = ('strength', 'perception', 'intelligence', 'dexterity', 'charisma', 'vitality', 'magic')
# Distribute a random number of points across them
attrs = self.assign_attributes(attributes, 50, 1, 10)
# Initialize the form with the results of the point distribution
for field in attrs.keys():
self.initial[field] = attrs[field]

View file

@ -43,7 +43,7 @@ folder and edit it to add/remove links to the menu.
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" id="user_options" aria-expanded="false">Logged in as {{user.username}} <span class="caret"></span></a>
<div class="dropdown-menu" aria-labelledby="user_options">
<a class="dropdown-item" href="#">Create</a>
<a class="dropdown-item" href="{% url 'chargen' %}">Create</a>
<a class="dropdown-item" href="#">Manage</a>
<div class="dropdown-divider"></div>
{% for character in user.characters %}

View file

@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block titleblock %}
Character Creation
{% endblock %}
{% block content %}
{% load addclass %}
<div class="container main-content mt-4" id="main-copy">
<div class="row">
<div class="col-lg-12 col-sm-12">
<div class="card mt-3">
<div class="card-body">
<h1 class="card-title">Character Creation</h1>
<hr />
{% if form.errors %}
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger" role="alert">{{ error }}</div>
{% endfor %}
{% endfor %}
{% endif %}
<form method="post" action="?">
{% csrf_token %}
{% for field in form %}
<div class="form-field my-3">
{{ field.label_tag }}
{{ field | addclass:"form-control" }}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text|safe }}</small>
{% endif %}
</div>
{% endfor %}
<hr />
<div class="form-group">
<input class="form-control btn btn-outline-secondary" type="submit" value="Submit" />
<input type="hidden" name="next" value="{{ next }}" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -15,6 +15,9 @@ urlpatterns = [
# User Authentication (makes login/logout url names available)
url(r'^auth/', include('django.contrib.auth.urls')),
url(r'^auth/register', website_views.AccountCreationView.as_view(), name="register"),
# Character management
url(r'^characters/create/', website_views.CharacterCreationView.as_view(), name="chargen"),
# Django original admin page. Make this URL is always available, whether
# we've chosen to use Evennia's custom admin or not.

View file

@ -9,6 +9,7 @@ from django.contrib.admin.sites import site
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.admin.views.decorators import staff_member_required
from django.http import HttpResponseRedirect
from django.shortcuts import render
@ -19,7 +20,7 @@ from evennia import SESSION_HANDLER
from evennia.objects.models import ObjectDB
from evennia.accounts.models import AccountDB
from evennia.utils import logger
from evennia.web.website.forms import AccountCreationForm
from evennia.web.website.forms import AccountCreationForm, CharacterCreationForm
from django.contrib.auth import login
@ -175,4 +176,60 @@ class AccountCreationView(FormView):
account.save()
messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name)
return HttpResponseRedirect(self.success_url)
return HttpResponseRedirect(self.success_url)
class CharacterCreationView(LoginRequiredMixin, FormView):
form_class = CharacterCreationForm
template_name = 'website/chargen_form.html'
success_url = '/'#reverse_lazy('character-manage')
def form_valid(self, form):
# Get account ref
account = self.request.user
character = None
# Get attributes from the form
self.attributes = {k: form.cleaned_data[k] for k in form.cleaned_data.keys()}
charname = self.attributes.pop('name')
description = self.attributes.pop('description')
# Create a character
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
from evennia.utils import create
try:
character = create.create_object(typeclass, key=charname, home=default_home, permissions=permissions)
# set playable character list
account.db._playable_characters.append(character)
# allow only the character itself and the account to puppet this character (and Developers).
character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" %
(character.id, account.id))
# If no description is set, set a default description
if not description:
character.db.desc = "This is a character."
else:
character.db.desc = description
# We need to set this to have @ic auto-connect to this character
account.db._last_puppet = character
# Assign attributes from form
[setattr(character.db, field, self.attributes[field]) for field in self.attributes.keys()]
character.creator_id = account.id
character.save()
except Exception as e:
messages.error(self.request, "There was an error creating your character. If this problem persists, contact an admin.")
logger.log_trace()
return self.form_invalid(form)
if character:
messages.success(self.request, "Your character '%s' was created!" % character.name)
return HttpResponseRedirect(self.success_url)
else:
messages.error(self.request, "Your character could not be created. Please contact an admin.")
return self.form_invalid(form)