diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1866483..ff492e0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v3 - name: Pull existing images - run: docker-compose pull + run: docker compose pull - name: Build image run: ./build @@ -92,15 +92,15 @@ jobs: - name: Load docker image run: docker load < image.tar.gz - - name: Test docker-compose + - name: Test docker compose run: | for k in "SECRET_KEY" "EMAIL_HOST_PASSWORD" "FACEBOOK_ACCESS_TOKEN"; do echo "$k=foo" >> ".env" done docker network create traefik - docker-compose up -d + docker compose up -d sleep 10 - res=$(docker-compose ps) + res=$(docker compose ps) echo "$res" if echo "$res" | grep Exit &>/dev/null; then exit 1 diff --git a/games/achievements.py b/games/achievements.py index aeb016f..16f53f8 100644 --- a/games/achievements.py +++ b/games/achievements.py @@ -1,5 +1,6 @@ import datetime import zoneinfo +from enum import StrEnum from django.db.models import Q @@ -11,13 +12,25 @@ TIMEZONE_FILENAME = zoneinfo._tzpath.find_tzfile("Europe/Copenhagen") with open(TIMEZONE_FILENAME, "rb") as f: TIMEZONE_DATA = zoneinfo._common.load_data(f) - +VALID_DATES = filter(lambda t: t > 0, TIMEZONE_DATA[1]) DST_TRANSITION_TIMES = [ - datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc) - for t in TIMEZONE_DATA[1] + datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc) for t in VALID_DATES ] +class AchievementLevel(StrEnum): + """Each level must correspond to a style in styles.css""" + + GOLD = "gold" + SILVER = "silver" + BRONZE = "bronze" + BASE = "base" + NO_LEVEL = "no_level" + + def __gt__(self, other): + return list(AchievementLevel).index(self) < list(AchievementLevel).index(other) + + class AchievementMetaClass(type): def __init__(self, name, *args, **kwargs): super().__init__(name, *args, **kwargs) @@ -29,7 +42,7 @@ class Achievement(metaclass=AchievementMetaClass): __slots__ = ["name", "description", "icon"] @staticmethod - def has_achieved(user): + def get_level(user): raise NotImplementedError @staticmethod @@ -42,50 +55,66 @@ class DNFAchievement(Achievement): description = "Participated in a game that completed, where you didn't" icon = "coffin.svg" - def has_achieved(user): - return ( + def get_level(user): + if ( user.gameplayer_set.filter(dnf=True) .filter(game__dnf=False, game__end_datetime__isnull=False) .exists() - ) + ): + return AchievementLevel.GOLD + return AchievementLevel.NO_LEVEL -class Top10Achievement(Achievement): - name = "Top 10" - description = "Placed top 10 total sips in a season" +class TopAchievement(Achievement): + name = "Top" + description = "Placed top (10/5/3/1) total sips in a season" icon = "trophy-cup.svg" - def has_achieved(user): + def get_level(user): current_season = Season.current_season() + highest_tier = AchievementLevel.NO_LEVEL for i in range(1, current_season.number): # Exclude current season - top10 = ( + top10 = list( PlayerStat.objects.filter(season_number=i) .order_by("-total_sips")[:10] .values("user") ) - if {"user": user.id} in top10: - return True - - return False + try: + rank = top10.index({"user": user.id}) + if rank < 1: + return AchievementLevel.GOLD + elif rank < 3: + highest_tier = max(AchievementLevel.SILVER, highest_tier) + elif rank < 5: + highest_tier = max(AchievementLevel.BRONZE, highest_tier) + elif rank < 10: + highest_tier = max(AchievementLevel.BASE, highest_tier) + except ValueError: + continue + return highest_tier class FastGameAchievement(Achievement): name = "Fast Game" - description = "Finished a game in less than 30 minutes" + description = "Finished a game in less than (30/20/15/10) minutes" icon = "stopwatch.svg" - def has_achieved(user): - return ( - Game.add_durations( - Game.objects.filter( - gameplayer__in=user.gameplayer_set.filter(dnf=False).filter( - game__dnf=False - ) - ) + def get_level(user): + short_games = Game.add_durations( + Game.objects.filter( + gameplayer__in=user.gameplayer_set.filter(dnf=False, game__dnf=False) ) - .filter(duration__lt=datetime.timedelta(minutes=30)) - .exists() - ) + ).filter(duration__lt=datetime.timedelta(minutes=30)) + if short_games.filter(duration__lt=datetime.timedelta(minutes=10)).exists(): + return AchievementLevel.GOLD + elif short_games.filter(duration__lt=datetime.timedelta(minutes=15)).exists(): + return AchievementLevel.SILVER + elif short_games.filter(duration__lt=datetime.timedelta(minutes=20)).exists(): + return AchievementLevel.BRONZE + elif short_games.exists(): + return AchievementLevel.BASE + else: + return AchievementLevel.NO_LEVEL class DanishDSTAchievement(Achievement): @@ -93,41 +122,70 @@ class DanishDSTAchievement(Achievement): description = "Participated in a game, while a DST transition happened in Denmark" icon = "backward-time.svg" - def has_achieved(user): + def get_level(user): query = Q() for dt in DST_TRANSITION_TIMES: query |= Q(start_datetime__lt=dt, end_datetime__gt=dt) - return user.games.filter(query).exists() + if user.games.filter(query).exists(): + return AchievementLevel.BASE + return AchievementLevel.NO_LEVEL class TheBarrelAchievement(Achievement): name = "The Barrel" - description = "Consumed 100 beers in-game" + description = "Consumed (100/1000/2000/4000) beers in-game" icon = "barrel.svg" - def has_achieved(user): - return (user.stats_for_season(all_time_season).total_sips / 14) >= 100 + def get_level(user): + total_sips = user.stats_for_season(all_time_season).total_sips / 14 + if total_sips >= 4000: + return AchievementLevel.GOLD + elif total_sips >= 2000: + return AchievementLevel.SILVER + elif total_sips >= 1000: + return AchievementLevel.BRONZE + elif total_sips >= 100: + return AchievementLevel.BASE + return AchievementLevel.NO_LEVEL class BundeCampAchievement(Achievement): name = "Chug Camp" - description = "Got 50 chugs in-game" + description = "Got (50/100/250/500) chugs in-game" icon = "ace.svg" - def has_achieved(user): - return user.stats_for_season(all_time_season).total_chugs >= 50 + def get_level(user): + total_chugs = user.stats_for_season(all_time_season).total_chugs + if total_chugs >= 500: + return AchievementLevel.GOLD + elif total_chugs >= 250: + return AchievementLevel.SILVER + elif total_chugs >= 100: + return AchievementLevel.BRONZE + elif total_chugs >= 50: + return AchievementLevel.BASE + return AchievementLevel.NO_LEVEL class StudyHardAchievement(Achievement): name = "Study Hard" description = ( - "Spend at least the amount of time corresponding to 2.5 ECTS in game (56 hours)" + "Spend at least the amount of time corresponding to (2.5/15/30/60) ECTS in game" ) icon = "diploma.svg" - def has_achieved(user): - return user.stats_for_season(all_time_season).approx_ects >= 2.5 + def get_level(user): + ects = user.stats_for_season(all_time_season).approx_ects + if ects >= 60: + return AchievementLevel.GOLD + elif ects >= 30: + return AchievementLevel.SILVER + elif ects >= 15: + return AchievementLevel.BRONZE + elif ects >= 2.5: + return AchievementLevel.BASE + return AchievementLevel.NO_LEVEL class PilfingerAchievement(Achievement): @@ -135,8 +193,10 @@ class PilfingerAchievement(Achievement): description = "Stille stille stille, Pille pille pille" icon = "pilfinger.jpg" - def has_achieved(user): - return user.id == 2030 + def get_level(user): + if user.id == 2030: + return AchievementLevel.GOLD + return AchievementLevel.NO_LEVEL def is_hidden(user): - return not PilfingerAchievement.has_achieved(user) + return PilfingerAchievement.get_level(user) != AchievementLevel.GOLD diff --git a/web/static/css/styles.css b/web/static/css/styles.css index 2c4dc2b..05aa57a 100644 --- a/web/static/css/styles.css +++ b/web/static/css/styles.css @@ -168,11 +168,32 @@ tr.in-game a { margin: 12px; height: 90px; width: 90px; + background-color: black; +} + +.achievements img.no_level { opacity: 0.2; } -.achievements img.active { - opacity: 1; +.achievements img.base { +} + +.achievements img.bronze { + border-color: #cd7f32; + border-style: solid; + border-width: 5px; +} + +.achievements img.silver { + border-color: #c0c0c0; + border-style: solid; + border-width: 5px; +} + +.achievements img.gold { + border-color: #d4af37; + border-style: solid; + border-width: 5px; } .invalid-feedback { diff --git a/web/templates/player_detail.html b/web/templates/player_detail.html index f657432..7d9915b 100644 --- a/web/templates/player_detail.html +++ b/web/templates/player_detail.html @@ -46,7 +46,7 @@

Achievements

{% for achievement in achievements %} - + {% endfor %}
diff --git a/web/views.py b/web/views.py index a9f3a60..2f32706 100644 --- a/web/views.py +++ b/web/views.py @@ -4,7 +4,6 @@ from collections import Counter from collections.abc import Iterable from urllib.parse import urlencode - from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.mixins import LoginRequiredMixin @@ -292,7 +291,7 @@ def get_context_data(self, **kwargs): context["achievements"].append( { - "achieved": achievement.has_achieved(self.object), + "level": achievement.get_level(self.object), "name": achievement.name, "description": achievement.description, "icon_url": static(f"achievements/{achievement.icon}"),