diff --git a/app/bloonsa_api/views.py b/app/bloonsa_api/views.py index c957f8b..70c5ba1 100644 --- a/app/bloonsa_api/views.py +++ b/app/bloonsa_api/views.py @@ -2,6 +2,7 @@ import json from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q from django.shortcuts import render, HttpResponse from django.http import HttpResponseBadRequest, JsonResponse from django.views.generic import TemplateView @@ -43,20 +44,24 @@ class LoadLevel(CSRFexemptTemplateView): class RandomLevel(CSRFexemptTemplateView): def post(self, request, *args, **kwargs): - level: Level = Level.objects.order_by("?").first() - flashVars = level.get_flash_vars(seperator="&") - if request.user.is_authenticated: bloonsa_util.tag_player(request=request) player: Player = Player.objects.get(user=request.user) + level: Level = Level.objects.order_by("?").filter(~Q(played_by=player)).first() + if not level: + level: Level = Level.objects.order_by("?").first() + if not player.bloonsa_levels_played.filter(pk=level.pk).exists(): level.played_by.add(player) level.save() bloonsa_util.log(player=player, action=actions.bloonsa_load_random_level, note=level.level_id) + else: + level: Level = Level.objects.order_by("?").first() + flashVars = level.get_flash_vars(seperator="&") return HttpResponse(flashVars) def get(self, request, *args, **kwargs): @@ -79,25 +84,45 @@ class CompleteLevel(CSRFexemptTemplateView): level: Level = Level.objects.get(level_id=level_id) player: Player = Player.objects.get(user=request.user) - prevScore = LevelScore.objects.filter(player=player, - level=level).first() - if prevScore is None \ - or pops > prevScore.pops \ - or pops == prevScore.pops and darts_left + int(dart_glitch) > prevScore.darts_left: + score = LevelScore.objects.filter(player=player, + level=level).first() + clear = pops >= level.target + + if score is None: score = LevelScore.objects.create(level=level, - clear=True, + clear=clear, darts_left=darts_left, dart_glitch=dart_glitch, + dart_glitch_ever=dart_glitch, pops=pops) - if prevScore: - prevScore.delete() - player.bloonsa_level_scores.add(score) score.save() player.save() - bloonsa_util.log(player=player, - action=actions.bloonsa_submit_score, - note=level.level_id) + + # Check if score is an improvement + elif any(( + pops > score.pops, + pops == score.pops and darts_left + int(dart_glitch) > score.darts_left, + clear and not score.clear + )): + # Has dart glitch ever been done in a score? + dart_glitch_ever = bool(dart_glitch or score.dart_glitch_ever or score.dart_glitch) + + score.update(commit=True, + clear=clear, + darts_left=darts_left, + dart_glitch=dart_glitch, + dart_glitch_ever=dart_glitch_ever, + pops=pops) + + # Check if we just did a dart glitch for fun + elif dart_glitch: + score.update(commit=True, + dart_glitch_ever=True) + + bloonsa_util.log(player=player, + action=actions.bloonsa_submit_score, + note=level.level_id) # Sending empty content means error in as3 return HttpResponse(content="GG", status=200) @@ -137,22 +162,13 @@ class GetStatusData(CSRFexemptTemplateView): level_id = int(json.loads(request.body).get("level_id")) level: Level = Level.objects.get(level_id=level_id) - player: Player = Player.objects.get(user=request.user) - score: LevelScore = LevelScore.objects.filter(player=player, - level=level).first() - total_levels = Level.objects.count() - level_cleared = player.has_beaten_bloonsa_level(level=level) level_plays = level.play_count level_wins = level.win_count level_win_percentage = 0.00 if level_wins == 0 else level_wins / level_plays dart_glitch_count = level.dart_glitch_count jdata = { - "bloonsa_levels_played": player.bloonsa_levels_played_count, - "bloonsa_levels_beaten": player.bloonsa_levels_beaten_count, - "bloonsa_total_levels": total_levels, - "level_author_name": level.author.name, "level_author_id": level.author.id, "level_title": level.title, @@ -161,19 +177,31 @@ class GetStatusData(CSRFexemptTemplateView): "level_legacy_rating": level.rating, "level_darts": level.darts, "level_target": level.target, - "level_cleared": level_cleared, "level_rating": level.stars, "level_plays": level_plays, "level_wins": level_wins, "level_win_percentage": level_win_percentage, "dart_glitch_count": dart_glitch_count, - } - if score: + + if request.user.is_authenticated: + player: Player = Player.objects.get(user=request.user) + score: LevelScore = LevelScore.objects.filter(player=player, + level=level).first() + level_cleared = player.has_beaten_bloonsa_level(level=level) + jdata.update({ - "score_darts_left": score.darts_left, - "score_dart_glitch": score.dart_glitch, - "score_pops": score.pops, + "bloonsa_levels_played": player.bloonsa_levels_played_count, + "bloonsa_levels_beaten": player.bloonsa_levels_beaten_count, + "bloonsa_total_levels": total_levels, + "level_cleared": level_cleared, }) + if score: + jdata.update({ + "score_darts_left": score.darts_left, + "score_dart_glitch": score.dart_glitch, + "score_dart_glitch_ever": score.dart_glitch_ever, + "score_pops": score.pops, + }) return JsonResponse(jdata) \ No newline at end of file diff --git a/app/bloonsa_game/migrations/0023_levelscore_dart_glitch_ever.py b/app/bloonsa_game/migrations/0023_levelscore_dart_glitch_ever.py new file mode 100644 index 0000000..d002d6a --- /dev/null +++ b/app/bloonsa_game/migrations/0023_levelscore_dart_glitch_ever.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 on 2025-02-17 23:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bloonsa_game', '0022_alter_levelrating_level'), + ] + + operations = [ + migrations.AddField( + model_name='levelscore', + name='dart_glitch_ever', + field=models.BooleanField(default=False), + ), + ] diff --git a/app/bloonsa_game/models.py b/app/bloonsa_game/models.py index 5e0398f..2bbb975 100644 --- a/app/bloonsa_game/models.py +++ b/app/bloonsa_game/models.py @@ -4,8 +4,17 @@ from django.db.models import Avg from users.models import Player +class ModelWithUpdate(models.Model): + class Meta: + abstract = True -class Author(models.Model): + def update(self, commit=False, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + if commit: + self.save() + +class Author(ModelWithUpdate): name = models.CharField(max_length=255) author_id = models.IntegerField() @@ -19,7 +28,7 @@ class Level(models.Model): darts = models.SmallIntegerField() target = models.SmallIntegerField() plays = models.IntegerField() - beats = models.IntegerField() # TODO can be removed? + beats = models.IntegerField() rating = models.FloatField(null=True, blank=True) data = models.CharField(max_length=288) @@ -35,7 +44,7 @@ class Level(models.Model): @property def dart_glitch_count(self): - return LevelScore.objects.filter(level=self, dart_glitch=True).count() + return LevelScore.objects.filter(level=self, dart_glitch_ever=True).count() @property def stars(self): @@ -44,7 +53,7 @@ class Level(models.Model): def __str__(self): - return f"{self.author} - {self.title}" + return f"{self.level_id}: {self.author.name} - {self.title}" def get_flash_vars(self, seperator): flashVars = { @@ -61,7 +70,7 @@ class Level(models.Model): return flashVarsStr -class LevelRating(models.Model): +class LevelRating(ModelWithUpdate): player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name="bloonsa_level_ratings", null=True) level = models.ForeignKey(Level, on_delete=models.CASCADE, related_name="bloonsa_level_ratings") rating = models.SmallIntegerField(validators=[MinValueValidator(1), @@ -74,16 +83,18 @@ class LevelRating(models.Model): # There should only be 1 score per player # Highest popcount wins -class LevelScore(models.Model): +class LevelScore(ModelWithUpdate): player = models.ForeignKey(Player, on_delete=models.CASCADE, related_name="bloonsa_level_scores", null=True) 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() dart_glitch = models.BooleanField(default=False) # This is triggered when the player lingers their last dart + dart_glitch_ever = models.BooleanField(default=False) # If they ever performed dart glitch in the past pops = models.PositiveSmallIntegerField() def __str__(self): - clearState = "✅" if self.clear else "❌" - scoreView = f"🎈{self.pops} | 🎯{self.darts_left} {'✏' if self.dart_glitch else ''}" + clear_state = "✅" if self.clear else "❌" + dart_glitch_state = "✏" if self.dart_glitch else "🖋" if self.dart_glitch_ever else "" + score_view = f"🎈{self.pops} | 🎯{self.darts_left} {dart_glitch_state}" return (f"{self.player.user.username if self.player else ''}'s " - f"{clearState} @ {self.level.title}: {scoreView}") \ No newline at end of file + f"{clear_state} @ {self.level}: {score_view}") \ No newline at end of file diff --git a/app/bloonsa_game/static/bloonsa_game/css/game.css b/app/bloonsa_game/static/bloonsa_game/css/game.css new file mode 100644 index 0000000..4eb0bc3 --- /dev/null +++ b/app/bloonsa_game/static/bloonsa_game/css/game.css @@ -0,0 +1,5 @@ +#bloonsa-game { + width: 640px; + height: 480px; + background-color: #20B0FF; +} \ No newline at end of file diff --git a/app/bloonsa_game/static/bloonsa_game/css/levelinfo.css b/app/bloonsa_game/static/bloonsa_game/css/levelinfo.css index e980a31..f687f05 100644 --- a/app/bloonsa_game/static/bloonsa_game/css/levelinfo.css +++ b/app/bloonsa_game/static/bloonsa_game/css/levelinfo.css @@ -87,19 +87,25 @@ } .filter-red { - filter: invert(61%) sepia(72%) saturate(7291%) hue-rotate(338deg) brightness(108%) contrast(101%); // red + filter: invert(61%) sepia(72%) saturate(7291%) hue-rotate(338deg) brightness(108%) contrast(101%); } .filter-grey { - filter: invert(17%) sepia(2%) saturate(72%) hue-rotate(66deg) brightness(94%) contrast(89%); // grey + filter: invert(17%) sepia(2%) saturate(72%) hue-rotate(66deg) brightness(94%) contrast(89%); } .filter-black { - filter: invert(0%) sepia(96%) saturate(15%) hue-rotate(246deg) brightness(105%) contrast(105%); // black + filter: invert(0%) sepia(96%) saturate(15%) hue-rotate(246deg) brightness(105%) contrast(105%); } .filter-softblue { filter: invert(53%) sepia(93%) saturate(488%) hue-rotate(182deg) brightness(103%) contrast(103%); } .filter-orange { - filter: invert(75%) sepia(23%) saturate(6404%) hue-rotate(355deg) brightness(98%) contrast(107%) !important; // orange + filter: invert(75%) sepia(23%) saturate(6404%) hue-rotate(355deg) brightness(98%) contrast(107%) !important; +} +.filter-purple { + filter: invert(14%) sepia(92%) saturate(7496%) hue-rotate(299deg) brightness(91%) contrast(106%); !important; +} +.filter-pink { + filter: invert(65%) sepia(38%) saturate(4716%) hue-rotate(268deg) brightness(102%) contrast(102%); } .invisible { diff --git a/app/bloonsa_game/static/bloonsa_game/js/flash_handler.js b/app/bloonsa_game/static/bloonsa_game/js/flash_handler.js index ad47741..7601766 100644 --- a/app/bloonsa_game/static/bloonsa_game/js/flash_handler.js +++ b/app/bloonsa_game/static/bloonsa_game/js/flash_handler.js @@ -4,6 +4,9 @@ function bloonsa_new_level_started(level_id) { } function bloonsa_level_completed(level_id) { + if (document.getElementById("login_box") != null) { + return + } console.log("call to bloonsa_level_completed"); bloonsa_update_data(level_id) } @@ -13,8 +16,14 @@ function bloonsa_rated_level_success(level_id) { bloonsa_update_data(level_id) } +// TODO only update neccessary parts +// eg: new_level_started -> level info box +// level_completed -> all +// level_rated -> level info box + function bloonsa_update_data(level_id) { + const xhr = new XMLHttpRequest(); console.log("call to update_data"); xhr.open("POST", "/bloonsa_api/get_status_data", true); @@ -70,6 +79,10 @@ function bloonsa_update_html(r) { document.getElementById("level-glitch-number").textContent = r.dart_glitch_count; // Personal best + if (document.getElementById("login_box") != null) { + document.getElementById("level-pb").remove(); + return + } var darts_img = document.getElementById("level-pb-darts-img"); if ("score_pops" in r) { document.getElementById("level-pb-pops-number").textContent = r.score_pops; @@ -77,7 +90,7 @@ function bloonsa_update_html(r) { document.getElementById("level-pb-darts-number").textContent = r.score_darts_left; document.getElementById("level-pb-darts-glitch").textContent = r.score_dart_glitch ? "+1": ""; - if (r.score_dart_glitch) { + if (r.score_dart_glitch | r.score_dart_glitch_ever) { darts_img.classList.add("filter-orange"); } else { darts_img.classList.remove("filter-orange"); diff --git a/app/bloonsa_game/templates/bloonsa_game/game.html b/app/bloonsa_game/templates/bloonsa_game/game.html index c07570d..849efce 100644 --- a/app/bloonsa_game/templates/bloonsa_game/game.html +++ b/app/bloonsa_game/templates/bloonsa_game/game.html @@ -20,6 +20,7 @@ }; + {% endblock head %} {% block content %} @@ -32,9 +33,9 @@ const container = document.getElementById("bloonsa-game"); container.appendChild(player); player.load({ - url: "{% static 'bloonsa_game/misc/bloons_unlimited.swf' %}?{{ flashVars |safe }}", + url: "{% static 'bloonsa_game/misc/bloons_unlimited.swf' %}?{{ flashVars | safe }}", allowScriptAccess: true, - backgroundColor: "#000", + // backgroundColor: "#000", }); player.style.width = "640px"; player.style.height = "480px"; diff --git a/app/bloonsa_game/templates/bloonsa_game/level.html b/app/bloonsa_game/templates/bloonsa_game/level.html deleted file mode 100644 index 7a5c4a4..0000000 --- a/app/bloonsa_game/templates/bloonsa_game/level.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends 'game/base.html' %} -{% load static %} -{% block content %} - - - -