Reworked how level completions work

This commit is contained in:
Walter 2025-02-14 03:43:17 +01:00
parent 94bf0654a8
commit cb3752dcb7
19 changed files with 365 additions and 77 deletions

View File

@ -4,9 +4,9 @@ from django.views.generic import TemplateView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from bloonsa_game.models import Level, LevelRating
from bloonsa_game.models import Level, LevelRating, LevelScore
from users.models import Player
from users.util import tag_player
from users.util import bloonsa_util, actions
class CSRFexemptTemplateView(TemplateView):
@ -17,18 +17,21 @@ class CSRFexemptTemplateView(TemplateView):
class LoadLevel(CSRFexemptTemplateView):
def post(self, request, *args, **kwargs):
levelId = request.POST.get("levelId")
if levelId is None or not levelId.isdigit():
level_id = request.POST.get("level_id")
if level_id is None or not level_id.isdigit():
return HttpResponseBadRequest()
level = Level.objects.filter(levelId=int(levelId)).first()
level = Level.objects.filter(levelId=int(level_id)).first()
if level is None:
return HttpResponseBadRequest()
if request.user.is_authenticated:
tag_player(request=request)
bloonsa_util.tag_player(request=request)
player = Player.objects.get(user=request.user)
player.bloonsa_levelsPlayed.add(level)
bloonsa_util.log(player=player,
action=actions.bloonsa_load_level_by_id,
note=level.levelId)
flashVars = level.getFlashVars(seperator="&")
return HttpResponse(flashVars)
@ -40,9 +43,12 @@ class RandomLevel(CSRFexemptTemplateView):
flashVars = level.getFlashVars(seperator="&")
if request.user.is_authenticated:
tag_player(request=request)
bloonsa_util.tag_player(request=request)
player = Player.objects.get(user=request.user)
player.bloonsa_levelsPlayed.add(level)
bloonsa_util.log(player=player,
action=actions.bloonsa_load_random_level,
note=level.levelId)
return HttpResponse(flashVars)
@ -52,11 +58,31 @@ class RandomLevel(CSRFexemptTemplateView):
class CompleteLevel(CSRFexemptTemplateView):
def post(self, request, *args, **kwargs):
if request.user.is_authenticated:
tag_player(request=request)
levelId = int(request.POST.get("levelId"))
level = Level.objects.get(levelId=levelId)
bloonsa_util.tag_player(request=request)
level_id = int(request.POST.get("level_id"))
darts_left = int(request.POST.get("darts_left"))
pops = int(request.POST.get("pops"))
level = Level.objects.get(levelId=level_id)
player = Player.objects.get(user=request.user)
player.bloonsa_levelsBeaten.add(level)
prevScore = player.bloonsa_level_scores.first()
if prevScore is None \
or pops > prevScore.pops \
or pops == prevScore.pops and darts_left > prevScore.darts_left:
score = LevelScore.objects.create(level=level,
clear=True,
darts_left=darts_left,
pops=pops)
if prevScore:
player.bloonsa_level_scores.remove(prevScore)
player.bloonsa_level_scores.add(score)
score.save()
player.save()
bloonsa_util.log(player=player,
action=actions.bloonsa_submit_score,
note=level.levelId)
# Sending empty content means error in as3
return HttpResponse(content="GG", status=200)
@ -64,16 +90,15 @@ class CompleteLevel(CSRFexemptTemplateView):
class RateLevel(CSRFexemptTemplateView):
def post(self, request, *args, **kwargs):
if request.user.is_authenticated:
tag_player(request=request)
bloonsa_util.tag_player(request=request)
rating = int(request.POST.get("rating"))
levelId = int(request.POST.get("levelId"))
level = Level.objects.get(levelId=levelId)
level_id = int(request.POST.get("level_id"))
level = Level.objects.get(levelId=level_id)
player = Player.objects.get(user=request.user)
ratingObject = Player.objects.filter(bloonsa_levelRatings__level=level).first()
if ratingObject:
ratingObject.rating = rating
ratingObject.save()
return HttpResponse(content="OK", status=200)
else:
rating = LevelRating.objects.create(level=level,
@ -81,6 +106,10 @@ class RateLevel(CSRFexemptTemplateView):
player.bloonsa_levelRatings.add(rating)
rating.save()
player.save()
bloonsa_util.log(player=player,
action=actions.bloonsa_rate_level,
note=level.levelId)
return HttpResponse(content="OK", status=200)
return HttpResponse(status=400)

View File

@ -1,6 +1,7 @@
from django.contrib import admin
from .models import Level, Author
from .models import Level, Author, LevelScore
admin.site.register(Level)
admin.site.register(Author)
admin.site.register(LevelScore)

View File

@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-02-13 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bloonsa_game', '0011_levelrating'),
]
operations = [
migrations.CreateModel(
name='LevelScore',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('clear', models.BooleanField(default=True)),
('darts_left', models.PositiveSmallIntegerField(null=True)),
('pops', models.PositiveSmallIntegerField(null=True)),
],
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-02-13 22:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bloonsa_game', '0012_levelscore'),
]
operations = [
migrations.AddField(
model_name='levelscore',
name='level',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='bloonsa_game.level'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.6 on 2025-02-13 22:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bloonsa_game', '0013_levelscore_level'),
]
operations = [
migrations.AlterField(
model_name='levelscore',
name='level',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='bloonsa_game.level'),
preserve_default=False,
),
]

View File

@ -42,4 +42,18 @@ class LevelRating(models.Model):
rating = models.SmallIntegerField()
def __str__(self):
return f"{self.player.first()} - [<{int(self.rating) * ''}> - {self.level}]"
return (f"{self.player.first().user.username}"
f" - [<{int(self.rating) * ''}> - {self.level.title}]")
# There should only be 1 score per player
# Highest popcount wins
class LevelScore(models.Model):
level = models.ForeignKey(Level, on_delete=models.CASCADE)
clear = models.BooleanField(default=True) # This is for if we ever submit scores for failed attempts
darts_left = models.PositiveSmallIntegerField(null=True)
pops = models.PositiveSmallIntegerField(null=True)
def __str__(self):
clearState = "" if self.clear else ""
return (f"{self.player.first().user.username}'s {clearState} @ {self.level.title}"
f"[🎈{self.pops} | 🎯{self.darts_left}]")

View File

@ -5,39 +5,41 @@ from django.views.generic import TemplateView
from django.views.static import serve
from bloonsa_game.models import Level
from users.util import tag_player, init_player
from users.util import bloonsa_util, actions
class IndexView(TemplateView):
def get(self, request, *args, **kwargs):
tag_player(request=request)
bloonsa_util.tag_player(request=request)
return render(request, "bloonsa_game/index.html", context={})
class TermsView(TemplateView):
def get(self, request, *args, **kwargs):
tag_player(request=request)
bloonsa_util.tag_player(request=request)
return render(request, "bloonsa_game/terms.html", context={})
class GameView(TemplateView):
def get(self, request, *args, **kwargs):
tag_player(request=request)
bloonsa_util.tag_player(request=request)
# This init is for accounts made with 'createsuperuser' or originating from bloonsb
player = init_player(request=request)
player = bloonsa_util.init_player(request=request)
# TODO get player object here with init_player to use in html template03.3.005
if type(kwargs.get("pk")) is int:
level = Level.objects.get(id=kwargs["pk"])
if level:
bloonsa_util.log(player=player,
action=actions.bloonsa_load_level_by_url,
note=level)
return render(request, "bloonsa_game/level.html", context={
"player": player,
"flashVars": level.getFlashVars(seperator="&amp;"),
"levelTitle": level.title,
"levelAuthor": level.author,
})
return render(request, "bloonsa_game/game.html", context={"player": player})
class WIPView(TemplateView):

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from .models import Player, LevelRating
from .models import Player, LevelRating, Log
class PlayerAdmin(admin.ModelAdmin):
@ -9,3 +9,4 @@ class PlayerAdmin(admin.ModelAdmin):
admin.site.register(LevelRating)
admin.site.register(Player, PlayerAdmin)
admin.site.register(Log)

View File

@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-02-13 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bloonsa_game', '0012_levelscore'),
('users', '0013_alter_player_user'),
]
operations = [
migrations.RemoveField(
model_name='player',
name='bloonsa_levelsBeaten',
),
migrations.AddField(
model_name='player',
name='bloonsa_level_scores',
field=models.ManyToManyField(blank=True, related_name='player', to='bloonsa_game.levelscore'),
),
migrations.AlterField(
model_name='player',
name='bloonsa_levelsPlayed',
field=models.ManyToManyField(blank=True, related_name='player', to='bloonsa_game.level'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.1.6 on 2025-02-14 02:16
import django.db.models.deletion
import django.utils.timezone
import users.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0014_remove_player_bloonsa_levelsbeaten_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='player',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='player', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='Log',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.PositiveSmallIntegerField(verbose_name=users.models.Log.Actions)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('note', models.TextField(null=True)),
('player', models.ManyToManyField(related_name='log', to='users.player')),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-02-14 02:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0015_alter_player_user_log'),
]
operations = [
migrations.RemoveField(
model_name='log',
name='player',
),
migrations.AddField(
model_name='log',
name='player',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='log', to='users.player'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-02-14 02:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0016_remove_log_player_log_player'),
]
operations = [
migrations.AlterField(
model_name='log',
name='player',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='log', to='users.player'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-14 02:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0017_alter_log_player'),
]
operations = [
migrations.AlterField(
model_name='log',
name='action',
field=models.PositiveSmallIntegerField(choices=[(0, 'login'), (1, 'register'), (2, 'logout'), (100, 'bloonsa_load_level_by_id'), (101, 'bloonsa_load_level_by_url'), (102, 'bloonsa_load_random_level'), (103, 'bloonsa_submit_score'), (104, 'bloonsa_rate_level')]),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-02-14 02:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0018_alter_log_action'),
]
operations = [
migrations.AlterField(
model_name='log',
name='action',
field=models.IntegerField(choices=[(0, 'login'), (1, 'register'), (2, 'logout'), (100, 'bloonsa_load_level_by_id'), (101, 'bloonsa_load_level_by_url'), (102, 'bloonsa_load_random_level'), (103, 'bloonsa_submit_score'), (104, 'bloonsa_rate_level')]),
),
]

View File

@ -2,14 +2,14 @@ from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from bloonsa_game.models import Level, LevelRating
from bloonsa_game.models import Level, LevelRating, LevelScore
class Player(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name="player")
user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True, related_name="player")
# Savedata
bloonsa_levelsPlayed = models.ManyToManyField(Level, blank=True, related_name="levelsPlayed")
bloonsa_levelsBeaten = models.ManyToManyField(Level, blank=True, related_name="levelsBeaten")
bloonsa_levelsPlayed = models.ManyToManyField(Level, blank=True, related_name="player")
bloonsa_level_scores = models.ManyToManyField(LevelScore, blank=True, related_name="player")
bloonsa_levelRatings = models.ManyToManyField(LevelRating, blank=True, related_name="player")
# Logging
creationIP = models.GenericIPAddressField()
@ -22,13 +22,15 @@ class Player(models.Model):
banned = models.BooleanField(default=False) # Account gets logged out upon logging in
admin = models.BooleanField(default=False) # Ability to suspend/ban other players
# If bloonsa_levels_played gets replaced then this needs to be updated
@property
def levels_played(self):
return self.bloonsa_levelsPlayed.count()
@property
def levels_beaten(self):
return self.bloonsa_levelsBeaten.count()
return self.bloonsa_level_scores.filter(clear=True).count()
@property
def total_levels(self):
@ -41,10 +43,25 @@ class Player(models.Model):
"banned": "" if self.banned else "",
"admin": "👑" if self.admin else "",
}
states = "".join(statesDict.values())
if states:
states += " "
return f"{states}{self.user} - {self.latestIP}"
states = "".join(statesDict.values()) + " "
return f"{states}{self.user} - {self.latestIP}".strip(" ")
class Log(models.Model):
class Actions(models.IntegerChoices):
login = 0, "Logged in"
register = 1, "Registered"
logout = 2, "Logged out"
bloonsa_load_level_by_id = 100, "Loaded a level via ingame ID box"
bloonsa_load_level_by_url = 101, "Loaded a level via URL"
bloonsa_load_random_level = 102, "Loaded a random level"
bloonsa_submit_score = 103, "Submitted a score on level_id"
bloonsa_rate_level = 104, "Rated a level"
player = models.ForeignKey(Player, related_name="log", on_delete=models.CASCADE, null=True)
action = models.IntegerField(choices=Actions)
timestamp = models.DateTimeField(default=timezone.now)
note = models.TextField(null=True)
def __str__(self):
return f"{self.player.user.username} - {self.get_action_display()} <{self.note or ''}>"

View File

@ -1,11 +1,17 @@
from django.contrib.auth import logout
from django.utils import timezone
from users.models import Player
from users.models import Player, Log
tracking = False
actions = Log.Actions
def get_ip(request):
class BloonsaUtil:
def __init__(self):
self.tracking = True
@staticmethod
def get_ip(request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[-1].strip()
@ -13,36 +19,48 @@ def get_ip(request):
ip = request.META.get("REMOTE_ADDR")
return ip
# Create a Player object for a User
def init_player(request):
# Create a Player object for a User
def init_player(self, request):
if not request.user.is_authenticated:
return
player = Player.objects.filter(user=request.user).first()
if player:
return player
ip = get_ip(request=request)
ip = self.get_ip(request=request)
player = Player(user=request.user,
creationIP=ip,
latestIP=ip)
player.save()
return player
# Update activity timestamp and IP
def tag_player(request):
if not tracking:
# Update activity timestamp and IP
def tag_player(self, request):
if not self.tracking:
return
if not request.user.is_authenticated:
return
player = Player.objects.filter(user=request.user).first()
if not player:
init_player(request=request)
self.init_player(request=request)
if player.banned:
# TODO message popup?
logout(request)
return
player.latestActivity = timezone.now()
player.latestIP = get_ip(request=request)
player.latestIP = self.get_ip(request=request)
player.save()
def log(self, action, note, player=None, request=None):
if not self.tracking:
return
if not player:
if not request:
return
player = self.init_player(request=request)
item = Log(player=player,
action=action,
note=note)
item.save()
bloonsa_util = BloonsaUtil()

View File

@ -5,7 +5,7 @@ from django.views.generic import TemplateView
from users.forms import UserRegisterForm, UserLoginForm
from users.models import Player
from users.util import init_player, tag_player
from users.util import bloonsa_util, actions
class LoginView(TemplateView):
@ -19,8 +19,10 @@ class LoginView(TemplateView):
if not form.is_valid():
return render(request=request, template_name="users/login.html", context={"form": form})
user = form.get_user()
init_player(request=request)
player = bloonsa_util.init_player(request=request)
login(request=request, user=user)
bloonsa_util.log(player=player,
action=actions.login)
return redirect("bloonsa_game:game")
@ -35,8 +37,10 @@ class RegisterView(TemplateView):
if not form.is_valid():
return render(request=request, template_name="users/register.html", context={"form": form})
user = form.save()
init_player(request=request)
player = bloonsa_util.init_player(request=request)
login(request=request, user=user)
bloonsa_util.log(player=player,
action=actions.login)
return redirect("bloonsa_game:game")
@ -45,7 +49,9 @@ class LogoutView(TemplateView):
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
tag_player(request=request)
bloonsa_util.tag_player(request=request)
bloonsa_util.log(request=request,
action=actions.logout)
logout(request)
return redirect("bloonsa_game:game")