Implements web-based character creation.
This commit is contained in:
parent
0531afceef
commit
40f5f283ad
5 changed files with 244 additions and 4 deletions
|
|
@ -2,6 +2,7 @@ from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.forms import UserCreationForm, UsernameField
|
from django.contrib.auth.forms import UserCreationForm, UsernameField
|
||||||
from evennia.utils import class_from_module
|
from evennia.utils import class_from_module
|
||||||
|
from random import choice, randint
|
||||||
|
|
||||||
class AccountCreationForm(UserCreationForm):
|
class AccountCreationForm(UserCreationForm):
|
||||||
|
|
||||||
|
|
@ -11,3 +12,131 @@ class AccountCreationForm(UserCreationForm):
|
||||||
field_classes = {'username': UsernameField}
|
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]
|
||||||
|
|
@ -43,7 +43,7 @@ folder and edit it to add/remove links to the menu.
|
||||||
<li class="nav-item dropdown">
|
<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>
|
<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">
|
<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>
|
<a class="dropdown-item" href="#">Manage</a>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
{% for character in user.characters %}
|
{% for character in user.characters %}
|
||||||
|
|
|
||||||
51
evennia/web/website/templates/website/chargen_form.html
Normal file
51
evennia/web/website/templates/website/chargen_form.html
Normal 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 %}
|
||||||
|
|
@ -16,6 +16,9 @@ urlpatterns = [
|
||||||
url(r'^auth/', include('django.contrib.auth.urls')),
|
url(r'^auth/', include('django.contrib.auth.urls')),
|
||||||
url(r'^auth/register', website_views.AccountCreationView.as_view(), name="register"),
|
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
|
# Django original admin page. Make this URL is always available, whether
|
||||||
# we've chosen to use Evennia's custom admin or not.
|
# we've chosen to use Evennia's custom admin or not.
|
||||||
url(r'django_admin/', website_views.admin_wrapper, name="django_admin"),
|
url(r'django_admin/', website_views.admin_wrapper, name="django_admin"),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from django.contrib.admin.sites import site
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import authenticate
|
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.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
@ -19,7 +20,7 @@ from evennia import SESSION_HANDLER
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
from evennia.utils import logger
|
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
|
from django.contrib.auth import login
|
||||||
|
|
||||||
|
|
@ -176,3 +177,59 @@ class AccountCreationView(FormView):
|
||||||
|
|
||||||
messages.success(self.request, "Your account '%s' was successfully created! You may log in using it now." % account.name)
|
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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue