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 %} - - - - - - - - -
- Move the mouse to aim the arrow, and press and hold the left mouse button to select the power of your throw. Pop the minimum number of bloons to pass each level. You can rate the levels you play by clicking the 'rate level' button in the game screen. You can also go to another random level by clicking 'Go random' at any time.
- Copyright Kaiparasoft 2007, all rights reserved
- Ninjakiwi - - Terms of Use - - Contact Us -
- -{% endblock content %} \ No newline at end of file