Compare commits
No commits in common. "main" and "feature/js" have entirely different histories.
main
...
feature/js
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -60,4 +60,3 @@ target/
|
|||
# sqlite database
|
||||
*.sqlite3
|
||||
|
||||
fear_tracker/migrations
|
||||
|
|
|
@ -6,7 +6,7 @@ from .models import Game, Player
|
|||
class PlayerForm(forms.Form):
|
||||
name = forms.CharField(max_length=80, label="Name", required=False)
|
||||
spirit = forms.TypedChoiceField(label="Spirit",
|
||||
choices=Player.enumerate_spirit_names(),
|
||||
choices=enumerate(Player.SPIRIT_NAMES),
|
||||
empty_value=None, coerce=int)
|
||||
|
||||
|
||||
|
@ -35,12 +35,6 @@ class NewGameForm(forms.Form):
|
|||
combined_growth_spirit = forms.BooleanField(
|
||||
required=False, initial=True,
|
||||
label="Combine Growth and Spirit phases into a single phase")
|
||||
enable_events = forms.BooleanField(
|
||||
required=False, initial=False,
|
||||
label="Enable event phase (Branch and Claw expansion)")
|
||||
combined_event = forms.BooleanField(
|
||||
required=False, initial=False,
|
||||
label="Combine three event phase steps into one phase")
|
||||
england_build = forms.BooleanField(
|
||||
required=False, initial=False,
|
||||
label="High Immigration (extra build phase for England level 3+)")
|
||||
|
|
|
@ -247,42 +247,18 @@ class Player(models.Model):
|
|||
game = models.ForeignKey(Game, on_delete=models.CASCADE, db_index=True)
|
||||
name = models.CharField(max_length=80)
|
||||
SPIRIT_NAMES = [
|
||||
# From https://spiritislandwiki.com/index.php?title=List_of_Spirits
|
||||
# Base game
|
||||
("Lightning's Swift Strike", "SI", "L"),
|
||||
("River Surges in Sunlight", "SI", "L"),
|
||||
("Vital Strength of the Earth", "SI", "L"),
|
||||
("Shadows Flicker Like Flame", "SI", "L"),
|
||||
("Thunderspeaker", "SI", "M"),
|
||||
("A Spread of Rampant Green", "SI", "M"),
|
||||
("Ocean's Hungry Grasp", "SI", "H"),
|
||||
("Bringer of Dreams and Nightmares", "SI", "H"),
|
||||
# Branch and Claw
|
||||
("Keeper of the Forbidden Wilds", "BC", "M"),
|
||||
("Sharp Fangs Behind the Leaves", "BC", "M"),
|
||||
# Promo Pack 1
|
||||
("Serpent Slumbering Beneath the Island", "P1", "H"),
|
||||
("Heart of the Wildfire", "P1", "H"),
|
||||
# Jagged Earth
|
||||
("Stone's Unyielding Defiance", "JE", "M"),
|
||||
("Shifting Memory of Ages", "JE", "M"),
|
||||
("Grinning Trickster Stirs Up Trouble", "JE", "M"),
|
||||
("Lure of the Deep Wilderness", "JE", "M"),
|
||||
("Many Minds Move as One", "JE", "M"),
|
||||
("Volcano Looming High", "JE", "M"),
|
||||
("Shroud of Silent Mist", "JE", "H"),
|
||||
("Vengeance as a Burning Plague", "JE", "H"),
|
||||
("Starlight Seeks Its Form", "JE", "V"),
|
||||
("Fractured Days Split the Sky", "JE", "V"),
|
||||
# Promo Pack 2
|
||||
("Downpour Drenches the World", "P2", "H"),
|
||||
("Finder of Paths Unseen", "P2", "V"),
|
||||
# Horizons of Spirit Island
|
||||
("Devouring Teeth Lurk Underfoot", "HS", "L"),
|
||||
("Eyes Watch From the Trees", "HS", "L"),
|
||||
("Fathomless Mud of the Swamp", "HS", "L"),
|
||||
("Rising Heat of Stone and Sand", "HS", "L"),
|
||||
("Sun-Bright Whirlwind", "HS", "L"),
|
||||
"Lightning's Swift Strike",
|
||||
"River Surges in Sunlight",
|
||||
"Vital Strength of the Earth",
|
||||
"Shadows Flicker Like Flame",
|
||||
"Thunderspeaker",
|
||||
"A Spread of Rampant Green",
|
||||
"Ocean's Hungry Grasp",
|
||||
"Bringer of Dreams and Nightmares",
|
||||
"Keeper of the Forbidden Wilds",
|
||||
"Sharp Fangs Behind the Leaves",
|
||||
"Serpent Slumbering Beneath the Island",
|
||||
"Heart of the Wildfire",
|
||||
]
|
||||
spirit = models.IntegerField()
|
||||
order = models.IntegerField()
|
||||
|
@ -291,12 +267,7 @@ class Player(models.Model):
|
|||
unique_together = (("game", "name"), ("game", "order"))
|
||||
|
||||
def get_spirit_name(self):
|
||||
return Player.SPIRIT_NAMES[self.spirit][0]
|
||||
|
||||
@staticmethod
|
||||
def enumerate_spirit_names():
|
||||
return [(i, f"[{spirit[1]},{spirit[2]}] {spirit[0]}") for (i, spirit)
|
||||
in enumerate(Player.SPIRIT_NAMES)]
|
||||
return Player.SPIRIT_NAMES[self.spirit]
|
||||
|
||||
|
||||
class Phase(models.Model):
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
|
||||
import django
|
||||
from django.conf.urls import url
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
# TODO Importing views breaks if django.setup() isn't called first...
|
||||
# ... but this isn't the way this is supposed to work.
|
||||
django.setup()
|
||||
|
||||
from . import views
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": URLRouter([
|
||||
url(r'^(?P<access_code>[a-zA-Z]{6})/', URLRouter([
|
||||
url(r"status/(?P<hashcode>[a-z0-9]{64})/",
|
||||
views.StatusLongPollConsumer.as_asgi(), name='status'),
|
||||
])),
|
||||
url(r"", get_asgi_application()),
|
||||
]),
|
||||
})
|
|
@ -1,7 +1,3 @@
|
|||
body {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
footer hr {
|
||||
border-width: 0;
|
||||
border-top: 1px solid lightgray;
|
||||
|
@ -152,25 +148,8 @@ footer a, footer a:visited {
|
|||
|
||||
.player-body {
|
||||
display: none;
|
||||
margin-left: 2ex;
|
||||
}
|
||||
|
||||
.player-summary {
|
||||
padding-left: 3ex;
|
||||
text-indent: -3ex;
|
||||
}
|
||||
|
||||
.player-visible:checked ~ .player-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
select, option {
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
Spirit Island Fear Tracker was built by
|
||||
<a href="https://aweirdimagination.net/~perelman/">Daniel Perelman</a>
|
||||
|
|
||||
<a href="https://git.aweirdimagination.net/perelman/fear_tracker">source
|
||||
<a href="https://git.aweirdimagination.net/perelman/fear-tracker">source
|
||||
code</a> licensed under
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3+</a>
|
||||
</p>
|
||||
|
|
|
@ -61,26 +61,26 @@
|
|||
<div class="player-effects" id="player-{{ player.order }}-effects">
|
||||
{% for effect_num, effect in player.fear.items %}
|
||||
<div class="effect effect-{{ effect_num }}">
|
||||
#{{ effect_num|add:1 }}
|
||||
Effect #{{ effect_num|add:1 }}
|
||||
|
||||
<input type="hidden" name="player-{{ player.order }}-effect-{{ effect_num }}-fear-orig" value="{{ effect.pure_fear }}">
|
||||
<select name="player-{{ player.order }}-effect-{{ effect_num }}-fear"{% if player.ready %} disabled{% endif %}>
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == effect.pure_fear %} selected="selected"{% endif %}>{{ val }}😱</option>
|
||||
<option value="{{ val }}"{% if val == effect.pure_fear %} selected="selected"{% endif %}>{{ val }} 😱</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<input type="hidden" name="player-{{ player.order }}-effect-{{ effect_num }}-towns-orig" value="{{ effect.towns }}">
|
||||
<select name="player-{{ player.order }}-effect-{{ effect_num }}-towns"{% if player.ready %} disabled{% endif %}>
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == effect.towns %} selected="selected"{% endif %}>{{ val }}🏠</option>
|
||||
<option value="{{ val }}"{% if val == effect.towns %} selected="selected"{% endif %}>{{ val }} 🏠</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<input type="hidden" name="player-{{ player.order }}-effect-{{ effect_num }}-cities-orig" value="{{ effect.cities }}">
|
||||
<select name="player-{{ player.order }}-effect-{{ effect_num }}-cities"{% if player.ready %} disabled{% endif %}>
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == effect.cities %} selected="selected"{% endif %}>{{ val }}🏙️</option>
|
||||
<option value="{{ val }}"{% if val == effect.cities %} selected="selected"{% endif %}>{{ val }} 🏙️</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -106,26 +106,26 @@
|
|||
{% if not results_only %}
|
||||
<template id="effect-template">
|
||||
<div class="effect">
|
||||
#<span class="effect-num">?</span>
|
||||
Effect #<span class="effect-num">?</span>
|
||||
|
||||
<input type="hidden" class="fear-orig" value="0">
|
||||
<select class="fear">
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }}😱</option>
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }} 😱</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<input type="hidden" class="towns-orig" value="0">
|
||||
<select class="towns">
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }}🏠</option>
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }} 🏠</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<input type="hidden" class="cities-orig" value="0">
|
||||
<select class="cities">
|
||||
{% for val in range %}
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }}🏙️</option>
|
||||
<option value="{{ val }}"{% if val == 0 %} selected="selected"{% endif %}>{{ val }} 🏙️</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
@ -360,23 +360,13 @@
|
|||
el.addEventListener("change", formElementChanged);
|
||||
}
|
||||
|
||||
function checkStatus() {
|
||||
setInterval(function() {
|
||||
if(activeRequests.size != 0) return;
|
||||
// From https://stackoverflow.com/a/50101022
|
||||
|
||||
const abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
|
||||
// 50 second timeout:
|
||||
const timeoutId = setTimeout(() => abort.abort(), 50000);
|
||||
|
||||
fetch(new Request("{% url 'status' access_code=access_code %}"
|
||||
+ (statusObj.hash != ""
|
||||
? statusObj.hash + "/"
|
||||
: "")),
|
||||
{signal})
|
||||
: "")))
|
||||
.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
if(response.status === 304) {
|
||||
// TODO Just skip the next step?
|
||||
return statusObj;
|
||||
|
@ -391,14 +381,8 @@
|
|||
} else {
|
||||
statusObj = data;
|
||||
}
|
||||
})
|
||||
.then(checkStatus)
|
||||
.catch(() => {
|
||||
// If something went wrong, wait a few seconds before retrying.
|
||||
setTimeout(checkStatus, 5000);
|
||||
});
|
||||
}
|
||||
checkStatus();
|
||||
}, 5000);
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
@ -27,5 +27,7 @@ urlpatterns = [
|
|||
path('update/', views.update_game, name='update_game'),
|
||||
path('qr/', views.qr_code, name='qr_code'),
|
||||
path('status/', views.status, name='status'),
|
||||
url('^status/(?P<hashcode>[a-z0-9]{64})/',
|
||||
views.status, name='status'),
|
||||
])),
|
||||
]
|
||||
|
|
|
@ -5,11 +5,6 @@ import hashlib
|
|||
import json
|
||||
import qrcode
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
from channels.generic.http import AsyncHttpConsumer
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
@ -59,8 +54,6 @@ def new_game(request):
|
|||
game = Game()
|
||||
game.combined_growth_spirit =\
|
||||
form.cleaned_data.get('combined_growth_spirit')
|
||||
game.enable_events = form.cleaned_data.get('enable_events')
|
||||
game.combined_event = form.cleaned_data.get('combined_event')
|
||||
game.england_build = form.cleaned_data.get('england_build')
|
||||
game.fear_per_card =\
|
||||
form.cleaned_data.get('fear_per_player') * num_players
|
||||
|
@ -81,7 +74,7 @@ def new_game(request):
|
|||
form = NewGameForm()
|
||||
|
||||
initial_player_data = [{'spirit': i}
|
||||
for (i, _) in Player.enumerate_spirit_names()]
|
||||
for i in enumerate(Player.SPIRIT_NAMES)]
|
||||
formset = PlayerFormSet(initial=initial_player_data)
|
||||
for player_form in formset:
|
||||
player_form.player_id = int(player_form.prefix[5:]) + 1
|
||||
|
@ -262,7 +255,7 @@ def handle_game_request(request, game, update):
|
|||
f"#{effect_num} {effect_kind} " +
|
||||
"was changed by another user to " +
|
||||
f"{current_value}, so " +
|
||||
"your attempt to change it from " +
|
||||
f"your attempt to change it from " +
|
||||
f"{orig} to {amount} " +
|
||||
"was not processed.")
|
||||
else:
|
||||
|
@ -293,22 +286,12 @@ def handle_game_request(request, game, update):
|
|||
res['value'] = current_value
|
||||
else:
|
||||
res = {'success': True}
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
"%s_status" % game.access_code,
|
||||
{"type": "fear_tracker.invalidate_status"})
|
||||
return HttpResponse(json.dumps(res))
|
||||
|
||||
players = get_players_with_fear(game, current_phase, players)
|
||||
status_obj = game_status_object(game, current_phase, players)
|
||||
status_string = json.dumps(status_obj)
|
||||
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
"%s_status" % game.access_code, {
|
||||
"type": "fear_tracker.hashcode_seen",
|
||||
"hashcode": status_obj['hash'],
|
||||
"status_string": status_string,
|
||||
})
|
||||
|
||||
for player in players.values():
|
||||
info = player.fear
|
||||
if not info:
|
||||
|
@ -339,57 +322,4 @@ def status(request, game, hashcode=None):
|
|||
return HttpResponse(status=HTTPStatus.NOT_MODIFIED)
|
||||
else:
|
||||
status_string = json.dumps(status_obj)
|
||||
async_to_sync(get_channel_layer().group_send)(
|
||||
"%s_status" % game.access_code, {
|
||||
"type": "fear_tracker.hashcode_seen",
|
||||
"hashcode": status_obj['hash'],
|
||||
"status_string": status_string,
|
||||
})
|
||||
return HttpResponse(status_string)
|
||||
|
||||
|
||||
class StatusLongPollConsumer(AsyncHttpConsumer):
|
||||
async def handle(self, body):
|
||||
self.access_code = self.scope["url_route"]["kwargs"]["access_code"]
|
||||
self.hashcode = self.scope["url_route"]["kwargs"]["hashcode"]
|
||||
|
||||
await self.channel_layer.group_add("%s_status" % self.access_code,
|
||||
self.channel_name)
|
||||
await self.channel_layer.group_send(
|
||||
"%s_status" % self.access_code, {
|
||||
"type": "fear_tracker.hashcode_seen",
|
||||
"hashcode": self.hashcode,
|
||||
})
|
||||
|
||||
async def http_request(self, message):
|
||||
"""
|
||||
Async entrypoint - concatenates body fragments and hands off control
|
||||
to ``self.handle`` when the body has been completely received.
|
||||
"""
|
||||
if "body" in message:
|
||||
self.body.append(message["body"])
|
||||
if not message.get("more_body"):
|
||||
await self.handle(b"".join(self.body))
|
||||
|
||||
async def disconnect(self):
|
||||
await self.channel_layer.group_discard("%s_status" % self.access_code,
|
||||
self.channel_name)
|
||||
|
||||
async def fear_tracker_hashcode_seen(self, event):
|
||||
if self.hashcode != event["hashcode"]:
|
||||
if "status_string" in event and event["status_string"]:
|
||||
body = event["status_string"].encode('utf-8')
|
||||
await self.send_response(200, body)
|
||||
await self.disconnect()
|
||||
await self.channel_layer.group_send(
|
||||
"%s_status" % self.access_code, {
|
||||
"type": "fear_tracker.invalidate_status",
|
||||
})
|
||||
|
||||
async def fear_tracker_invalidate_status(self, event):
|
||||
no_hash_status = reverse('status',
|
||||
kwargs={'access_code': self.access_code})
|
||||
await self.send_response(302, b'', headers=[
|
||||
(b"Location", no_hash_status.encode('utf-8'))
|
||||
])
|
||||
await self.http_disconnect(None)
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
"""
|
||||
ASGI entrypoint. Configures Django and then runs the application
|
||||
defined in the ASGI_APPLICATION setting.
|
||||
"""
|
||||
|
||||
import django
|
||||
|
||||
import fear_tracker.routing
|
||||
|
||||
django.setup()
|
||||
application = fear_tracker.routing.application
|
|
@ -1,3 +0,0 @@
|
|||
import fear_tracker.routing
|
||||
|
||||
application = fear_tracker.routing.application
|
|
@ -31,7 +31,6 @@ ALLOWED_HOSTS = []
|
|||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'channels',
|
||||
'fear_tracker',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
|
@ -70,7 +69,6 @@ TEMPLATES = [
|
|||
]
|
||||
|
||||
WSGI_APPLICATION = 'fear_tracker_site.wsgi.application'
|
||||
ASGI_APPLICATION = 'fear_tracker_site.routing.application'
|
||||
|
||||
|
||||
# Database
|
||||
|
@ -83,12 +81,6 @@ DATABASES = {
|
|||
}
|
||||
}
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels.layers.InMemoryChannelLayer"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
|
Loading…
Reference in New Issue
Block a user