diff --git a/.env b/.env index a736a5e..38c3e97 100644 --- a/.env +++ b/.env @@ -1,10 +1,9 @@ TOKEN_URL='https://api.intra.42.fr/oauth/token' USER_INFO_URL='https://api.intra.42.fr/v2/me' CLIENT_ID='u-s4t2ud-b82e9fe438563f339a906cd3bc30001cbb1785649c08b26fca0e4c1e2e7eddab' -CLIENT_SECRET='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' -REDIRECT_URI='http://localhost:8002/home42/' +CLIENT_SECRET='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' POSTGRES_USER="anaraujo" POSTGRES_PASSWORD="1234" PGADMIN_DEFAULT_EMAIL="admin@email.com" PGADMIN_DEFAULT_PASSWORD="1234" -PGADMIN_LISTEN_PORT="8080" \ No newline at end of file +PGADMIN_LISTEN_PORT="8080" diff --git a/Makefile b/Makefile index c0ef9bf..67b2b58 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,8 @@ clean: backend/pong/__pycache__/ \ backend/pong/migrations/__pycache__/ \ backend/pong/templatetags/__pycache__/ \ - backend/pong/migrations/*_initial.py + backend/pong/migrations/*_initial.py \ + backend/pong/migrations/0*.py mkdir data/ diff --git a/backend/Dockerfile b/backend/Dockerfile index 481a8c2..5194539 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,7 +16,7 @@ RUN apk add --update --no-cache python3-dev python3 py3-pip gcc musl-dev libpq-d && pip install "channels[daphne]" \ && rm -rf /var/cache/apk/* - - - -CMD source /.venv/bin/activate && python3 ./manage.py runserver 0.0.0.0:8002 \ No newline at end of file +CMD source /.venv/bin/activate \ + && python3 manage.py makemigrations \ + && python3 manage.py migrate \ + && python3 ./manage.py runserver 0.0.0.0:8002 \ No newline at end of file diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 6c417f8..8ae8ba9 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -27,7 +27,6 @@ TOKEN_URL_A=os.getenv("TOKEN_URL") CLIENT_ID_A=os.getenv("CLIENT_ID") CLIENT_SECRET_A=os.getenv("CLIENT_SECRET") -REDIRECT_URI_A=os.getenv("REDIRECT_URI") USER_INFO_URL_A=os.getenv("USER_INFO_URL") diff --git a/backend/media/upload/Screenshot_from_2024-06-26_22-00-29.png b/backend/media/upload/Screenshot_from_2024-06-26_22-00-29.png new file mode 100644 index 0000000..7b85d4c Binary files /dev/null and b/backend/media/upload/Screenshot_from_2024-06-26_22-00-29.png differ diff --git a/backend/media/upload/Screenshot_from_2024-07-24_19-18-59.png b/backend/media/upload/Screenshot_from_2024-07-24_19-18-59.png new file mode 100644 index 0000000..b502303 Binary files /dev/null and b/backend/media/upload/Screenshot_from_2024-07-24_19-18-59.png differ diff --git a/backend/media/upload/Screenshot_from_2024-10-03_14-33-32.png b/backend/media/upload/Screenshot_from_2024-10-03_14-33-32.png new file mode 100644 index 0000000..b916d8a Binary files /dev/null and b/backend/media/upload/Screenshot_from_2024-10-03_14-33-32.png differ diff --git a/backend/media/upload/Screenshot_from_2024-10-11_17-48-12.png b/backend/media/upload/Screenshot_from_2024-10-11_17-48-12.png new file mode 100644 index 0000000..26575ef Binary files /dev/null and b/backend/media/upload/Screenshot_from_2024-10-11_17-48-12.png differ diff --git a/backend/pong/consumers.py b/backend/pong/consumers.py index 6a78cf0..d66755b 100644 --- a/backend/pong/consumers.py +++ b/backend/pong/consumers.py @@ -1,45 +1,170 @@ import json +from .views import game_create_helper, game_update_helper from .models import TournamentsUsers, Users from .serializers import TournamentsUsersSerializer, UsersSerializer from icecream import ic from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer +from channels.layers import get_channel_layer +import random class TournamentConsumer(WebsocketConsumer): - def connect(self): - self.room_group_name = f'{self.scope["url_route"]["kwargs"]["tournament_id"]}' - self.user = self.scope["user"] + def connect(self): + self.room_group_name = f'{self.scope["url_route"]["kwargs"]["tournament_id"]}' - async_to_sync(self.channel_layer.group_add)( - self.room_group_name, self.channel_name - ) + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, self.channel_name + ) - self.accept() + self.accept() - def disconnect(self, code): - async_to_sync(self.channel_layer.group_discard)( - self.room_group_name, self.channel_name - ) - return super().disconnect(code) - - def receive(self, text_data): - tournament_id = self.room_group_name - all_tour_users = TournamentsUsers.objects.filter(tournament_id=tournament_id) - serializer = TournamentsUsersSerializer(all_tour_users, many=True) + def disconnect(self, code): + async_to_sync(self.channel_layer.group_discard)( + self.room_group_name, self.channel_name + ) + return super().disconnect(code) + + def receive(self, text_data): + tournament_id = self.room_group_name + all_tour_users = TournamentsUsers.objects.filter(tournament_id=tournament_id) + serializer = TournamentsUsersSerializer(all_tour_users, many=True) - tour_users_data = serializer.data + tour_users_data = serializer.data - for tour_user in tour_users_data: - user = Users.objects.get(pk=tour_user['user_id']) - user_data = UsersSerializer(user).data - tour_user['user'] = user_data + for tour_user in tour_users_data: + user = Users.objects.get(pk=tour_user['user_id']) + user_data = UsersSerializer(user).data + tour_user['user'] = user_data - async_to_sync(self.channel_layer.group_send)( - self.room_group_name, {"type": "send.users", "message": json.dumps(tour_users_data)} - ) + async_to_sync(self.channel_layer.group_send)( + self.room_group_name, {"type": "send.users", "message": json.dumps(tour_users_data)} + ) - def send_users(self, event): - self.send(text_data=event["message"]) + def send_users(self, event): + self.send(text_data=event["message"]) + +class RemoteGameQueueConsumer(WebsocketConsumer): + queue = {} + + def connect(self): + self.accept() + self.user = self.scope['user'] + self.room_name = '' + self.game_id = 0 + + # if the queue is empty: (no room available) + # - create a new channel_name and add it to the object + # - push the new object alongside the channel name to the queue + # else: (available rooms) + # - Pop the first available room in the queue + # - Add the client to the room + # - Broadcast a message to the channel with a starting command + if self.user.id in self.queue: + return + + if len(self.queue) == 0: + ic('adding player to queue') + self.add_player_to_queue() + else: + ic('adding player to waiting room') + self.add_player_to_waiting_room() + ic(self.queue) + + def add_player_to_queue(self): + """ + This will add the new player to the queue and also create a channel group + (a 'waiting room') to allow another player to join in + """ + self.room_name = "room_%s" % self.user.id + + self.queue[self.user.id] = { + 'id': self.user.id, + 'username': self.scope['user'].username, + 'room_name': self.room_name + } + async_to_sync(self.channel_layer.group_add)(self.room_name, self.channel_name) + + def add_player_to_waiting_room(self): + """ + Since the queue is not empty, this means there is already at least + 1 waiting room. We add the current player to the waiting room and + send a START command to initiate the game. This also removes the + waiting room from the queue. + """ + + host_id = list(self.queue.keys())[0] + host_player = self.queue[host_id] + self.room_name = host_player['room_name'] + curr_player = { + 'id': self.user.id, + 'username': self.scope['user'].username, + 'room_name': self.room_name + } + + new_game_data = { + "user1_id": host_id, + "user2_id": self.user.id, + "type": "Remote" + } + + new_game = json.loads(game_create_helper(new_game_data).content) + async_to_sync(self.channel_layer.group_add)(host_player['room_name'], self.channel_name) + async_to_sync(self.channel_layer.group_send)( + self.room_name, { + "type": "send.start.game.message", + "message": json.dumps({ + 'gameID': new_game['id'], + 'player1': host_player, + 'player2': curr_player, + 'ball': { + 'direction': { + 'x': 1 if random.randint(0, 1) == 1 else -1, + 'y': 1 if random.randint(0, 1) == 1 else -1 + } + } + }) + } + ) + del self.queue[host_id] + + def disconnect(self, code): + if self.user.id in self.queue: + del self.queue[self.user.id] + return super().disconnect(code) + + def receive(self, text_data=None): + handlers = { + 'UPDATE': 'send.update.paddle.message', + 'SYNC': 'send.ball.sync.message', + 'FINISH': 'send.end.game.message' + } + data = json.loads(text_data) + event = data['event'] + + if event == 'FINISH': + game_data = data['data'] + game_id = game_data['id'] + del game_data['id'] + game_update_helper(data['data'], game_id) + + async_to_sync(self.channel_layer.group_send)( + self.room_name, { + "type": handlers[event], + "message": text_data + } + ) + + def send_start_game_message(self, event): + self.send(event['message']) + + def send_ball_sync_message(self, event): + self.send(event['message']) + + def send_update_paddle_message(self, event): + self.send(event['message']) + + def send_end_game_message(self, event): + self.send(event['message']) \ No newline at end of file diff --git a/backend/pong/migrations/0001_initial.py b/backend/pong/migrations/0001_initial.py index d144183..4b30e93 100644 --- a/backend/pong/migrations/0001_initial.py +++ b/backend/pong/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.1 on 2024-10-03 17:50 +# Generated by Django 5.1.2 on 2024-10-23 19:47 import django.db.models.deletion from django.conf import settings @@ -130,9 +130,13 @@ class Migration(migrations.Migration): ('nb_goals_scored', models.IntegerField(default=0)), ('nb_goals_suffered', models.IntegerField(default=0)), ('max_ball_speed', models.IntegerField(default=0)), + ('date_max_ball_speed', models.DateTimeField(null=True)), ('max_rally_length', models.IntegerField(default=0)), + ('date_max_rally_length', models.DateTimeField(null=True)), ('quickest_game', models.IntegerField(default=2147483647)), + ('date_quickest_game', models.DateTimeField(null=True)), ('longest_game', models.IntegerField(default=0)), + ('date_longest_game', models.DateTimeField(null=True)), ('num_first_goals', models.IntegerField(default=0)), ('remote_time_played', models.IntegerField(default=0)), ('local_time_played', models.IntegerField(default=0)), diff --git a/backend/pong/models.py b/backend/pong/models.py index ecfa382..35013bf 100644 --- a/backend/pong/models.py +++ b/backend/pong/models.py @@ -155,9 +155,13 @@ class UserStats(models.Model): nb_goals_scored = models.IntegerField(default=0) nb_goals_suffered = models.IntegerField(default=0) max_ball_speed = models.IntegerField(default=0) + date_max_ball_speed = models.DateTimeField(null=True) max_rally_length = models.IntegerField(default=0) + date_max_rally_length = models.DateTimeField(null=True) quickest_game = models.IntegerField(default=2147483647) + date_quickest_game = models.DateTimeField(null=True) longest_game = models.IntegerField(default=0) + date_longest_game = models.DateTimeField(null=True) num_first_goals = models.IntegerField(default=0) remote_time_played = models.IntegerField(default=0) local_time_played =models.IntegerField(default=0) diff --git a/backend/pong/serializers.py b/backend/pong/serializers.py index f007437..e392ccb 100644 --- a/backend/pong/serializers.py +++ b/backend/pong/serializers.py @@ -62,7 +62,6 @@ class Meta: fields = ['id', 'type', 'status', 'description', 'user_id', 'other_user_id', 'created_at'] class GamesSerializer(serializers.ModelSerializer): - class Meta: model = Games fields = '__all__' diff --git a/backend/pong/templates/navs.html b/backend/pong/templates/navs.html index 7288969..ee1b8b8 100644 --- a/backend/pong/templates/navs.html +++ b/backend/pong/templates/navs.html @@ -15,7 +15,6 @@ -
diff --git a/backend/pong/templates/pages/game_stats.html b/backend/pong/templates/pages/game_stats.html index 1ce322a..c38e77b 100644 --- a/backend/pong/templates/pages/game_stats.html +++ b/backend/pong/templates/pages/game_stats.html @@ -1,29 +1,222 @@ {% extends 'navs.html' %} {% load static %} - +{% load custom_filters %} {% block main_content %} -
-

Game {{ game_id }}

-

Longer Rally {{ stats.data.longer_rally }}

-

Shorter Rally {{ stats.data.shorter_rally }}

-

Average Rally {{ stats.data.average_rally }}

-

Max Ball Speed {{ stats.data.max_ball_speed }}

-

Min Ball Speed {{ stats.data.min_ball_speed }}

-

Average Ball Speed {{ stats.data.average_ball_speed }}

-

Greatest Deficit Overcome {{ stats.data.greatest_deficit_overcome }}

-

User GDO {{ stats.data.gdo_user.username }}

-

Most Consecutive Goals {{ stats.data.most_consecutive_goals }}

-

User MCG {{ stats.data.mcg_user.username }}

-

Biggest Lead {{ stats.data.biggest_lead }}

-

User BG {{ stats.data.bg_user.username }}

-

Goals:

- {% for goal in goals.data %} -

Goal Rally Length: {{ goal.rally_length }}

-

Ball Speed: {{ goal.ball_speed }}

-

Timestamp: {{ goal.timestamp }}

-

User: {{ goal.user }}

- {% endfor %} + + + +
+
+
+
+
+
+ {% if "http" in game.user1_id.picture %} + Profile + {% else %} + Profile + {% endif %} + {{game.user1_id.username}} +
+
+ {% if user.id == game.user1_id.id or user.id == game.user2_id.id %} + {% if user.id == game.winner_id%} + VICTORY + {% else %} + DEFEAT + {% endif %} + {% endif %} + + {{game.nb_goals_user1}} - {{game.nb_goals_user2}} +
+
+ {% if "http" in game.user2_id.picture %} + Profile + {% else %} + Profile + {% endif %} + {{game.user2_id.username}} +
+
+
+
+
Score History
+
+
+
+
+
Rally Length per Goal
+
+
+
0
+ 1 +
+
+
0
+ 2 +
+
+
0
+ 3 +
+
+
0
+ 4 +
+
+
0
+ 5 +
+
+
0
+ 6 +
+
+
0
+ 7 +
+
+
0
+ 8 +
+
+
0
+ 9 +
+
+
+
+ +
+
+
+
{{stats.shorter_rally}}
+ Min +
+
+
{{stats.average_rally}}
+ Average +
+
+
{{stats.longer_rally}}
+ Max +
+ +
+
+
+
Ball Speed per Goal
+
+
+
0
+ 1 +
+
+
0
+ 2 +
+
+
0
+ 3 +
+
+
0
+ 4 +
+
+
0
+ 5 +
+
+
0
+ 6 +
+
+
0
+ 7 +
+
+
0
+ 8 +
+
+
0
+ 9 +
+
+
+
+ +
+
+
+
{{stats.min_ball_speed}}
+ Min +
+
+
{{stats.average_ball_speed}}
+ Average +
+
+
{{stats.max_ball_speed}}
+ Max +
+ +
+
+
+
+
+
Greateast Deficit Overcome
+
Who bounced back the most goals.
+
+
+ {% if "http" in stats.gdo_user.picture %} + Profile + {% else %} + Profile + {% endif %} +
{{stats.gdo_user.username}}
+
+
{{stats.greatest_deficit_overcome}}
+
+
+
+
Most Consecutive Goals
+
Who scored more goals in a row.
+
+
+ {% if "http" in stats.mcg_user.picture %} + Profile + {% else %} + Profile + {% endif %} +
{{stats.mcg_user.username}}
+
+
{{stats.most_consecutive_goals}}
+
+
+
+
Biggest Lead
+
Who had the biggest goal lead.
+
+
+ {% if "http" in stats.bg_user.picture %} + Profile + {% else %} + Profile + {% endif %} +
{{stats.bg_user.username}}
+
+
{{stats.biggest_lead}}
+
+
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/backend/pong/templates/pages/gamelocal.html b/backend/pong/templates/pages/gamelocal.html index be85113..160c9d3 100644 --- a/backend/pong/templates/pages/gamelocal.html +++ b/backend/pong/templates/pages/gamelocal.html @@ -19,8 +19,13 @@
+
+
+
0 : 0
+
+
+ data-user-id="{{ user.id }}" data-username="{{ user.username }}" game-type="Local"> {% endblock %} \ No newline at end of file diff --git a/backend/pong/templates/pages/gameonline.html b/backend/pong/templates/pages/gameonline.html index 8962230..682c3cb 100644 --- a/backend/pong/templates/pages/gameonline.html +++ b/backend/pong/templates/pages/gameonline.html @@ -1,6 +1,32 @@ {% extends 'navs.html' %} {% load static %} +{% block head %} + + + + +{% endblock %} + + {% block main_content %} +
+
+
+
+
+
0 : 0
+
+
+
+ {% endblock %} \ No newline at end of file diff --git a/backend/pong/templates/pages/ongoing-tourn.html b/backend/pong/templates/pages/ongoing-tourn.html index 231353d..4f70862 100644 --- a/backend/pong/templates/pages/ongoing-tourn.html +++ b/backend/pong/templates/pages/ongoing-tourn.html @@ -5,10 +5,10 @@
-
+
-

{{ tournament_name }} Name

+

{{ tournament_name }}

Waiting for players to enter...

-
+ {% if tournament_size == 4 %} +
+
+
+

Group A

+
+
+ Player 1 + Player 1 +
+
+ 0 +
+
+
+
+
+
+
+
+ Player 2 + Player 2 +
+
+ 0 +
+
+
+
+
+

Group B

+
+
+ Player 3 + Player 3 +
+
+ 0 +
+
+
+
+
+
+
+
-
-
-

Group A

-
-
+ Player 4 + Player 4 +
+
+ 0 +
+
+
+
+
+
+
+
+
+ + + Semi 1 + + Group A Winner +
+
+ 0 +
+
+
+
+
+
+
+
+ + + Semi 2 + + Group B Winner +
+
+ 0 +
+
+
+
+
+
+
+
+
+ + + Final Winner + + Tournament Winner +
+
+ Final Winner - Player 1 - Player 1 -
-
- 0 -
-
-
-
- Player 2 - Player 2 -
-
- 0 -
-
-
-
-
-

Group B

-
-
- - Player 3 - Player 3 +
+
-
- 0 +
+
+ + {% elif tournament_size == 8 %} +
+
+
+
+

Group A

+
+
+ Player 1 + Player 1 +
+
+ 0 +
-
-
-
- - Player 4 - Player 4 +
+
+
+
+
+
+ Player 2 + Player 2 +
+
+ 0 +
-
- 0 +
+
+
+

Group B

+
+
+ Player 3 + Player 3 +
+
+ 0 +
-
-
-
-
+
+
+
+
+
+
-
-
-
-
- - Semi 1 - Group A Winner + Player 4 + Player 4 +
+
+ 0 +
+
+
+
+
+
+
+
+
+ + + Semi 1 + + Group A Winner +
+
+ 0 +
-
- 0 +
+
+
+
+
+
+ + + Semi 2 + + Group B Winner +
+
+ 0 +
-
-
-
+
+
+
+
+
+
+
+ + + Final Winner + + Semi Finals Winner +
+
+ 0 +
+
+
+
+
+
+
+
+
+
+
+
+ + + Final Winner + + Tournament Winner +
+
+ Final Winner - Semi 2 - Group B Winner +
-
- 0 +
+
+
+
+
+
+

Group C

+
+
+ Player 1 + Player 5 +
+
+ 0 +
-
-
-
-
+
+
+
+
+
+
+ Player 2 + Player 6 +
+
+ 0 +
+
+
+
+
+

Group D

+
+
+ Player 3 + Player 7 +
+
+ 0 +
+
+
+
+
+
+
+
-
-
-
-
- Tournament Winner - Final Winner -
-
+ Player 4 + Player 8 +
+
+ 0 +
+
+
-
+
+
+
+
+ + + Semi 1 + + Group C Winner +
+
+ 0 +
+
+
+
+
+
+
+
+ + + Semi 2 + + Group D Winner +
+
+ 0 +
+
+
+
+
+
+
+
+
+ + + Final Winner + + Semi Finals Winner +
+
+ 0 +
+
+
+
+
+
+
+ {% else %} + + {% endif %} +
-
{% endblock %} diff --git a/backend/pong/templates/pages/search_users.html b/backend/pong/templates/pages/search_users.html index 87edbac..0298199 100644 --- a/backend/pong/templates/pages/search_users.html +++ b/backend/pong/templates/pages/search_users.html @@ -6,32 +6,42 @@
{% if searched %} -

Search Results

-
-
Your search for "{{ searched }}" has retrieved {{ numbers}} - {% if numbers == 1 %} - result. - {% else %} - results. - {% endif %} -
-
-
diff --git a/backend/pong/templates/pages/tournament_overview.html b/backend/pong/templates/pages/tournament_overview.html new file mode 100644 index 0000000..80baae3 --- /dev/null +++ b/backend/pong/templates/pages/tournament_overview.html @@ -0,0 +1,454 @@ +{% extends 'navs.html' %} +{% load static %} +{% load custom_filters %} + +{% block main_content %} + + +
+
+
+
+

{{ tournament_name }}

+

This tournament is over.

+
+
+
+
+

Leaderboard

+
+ Placement + User + Score +
+ {% for tour_user in tour_users %} + + {% endfor %} + +
+ {% if tournament_size == 4 %} +
+
+
+

Group A

+
+
+ {% if "http" in tour_games.0.game_id.user1_id.picture.url %} + Player 1 + {% else %} + Player 1 + {% endif %} + {{ tour_games.0.game_id.user1_id.username }} +
+
+ {{ tour_games.0.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.0.game_id.user2_id.picture.url %} + Player 2 + {% else %} + Player 2 + {% endif %} + {{ tour_games.0.game_id.user2_id.username }} +
+
+ {{ tour_games.0.game_id.nb_goals_user2 }} +
+
+
+
+
+

Group B

+
+
+ {% if "http" in tour_games.1.game_id.user1_id.picture.url %} + Player 3 + {% else %} + Player 3 + {% endif %} + {{ tour_games.1.game_id.user1_id.username }} +
+
+ {{ tour_games.1.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.1.game_id.user2_id.picture.url %} + Player 4 + {% else %} + Player 4 + {% endif %} + {{ tour_games.1.game_id.user2_id.username }} +
+
+ {{ tour_games.1.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.2.game_id.user1_id.picture.url %} + Semi 1 + {% else %} + Semi 1 + {% endif %} + {{ tour_games.2.game_id.user1_id.username }} +
+
+ {{ tour_games.2.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.2.game_id.user2_id.picture.url %} + Semi 2 + {% else %} + Semi 2 + {% endif %} + {{ tour_games.2.game_id.user2_id.username }} +
+
+ {{ tour_games.2.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.2.game_id.winner_id.picture.url %} + Final Winner + {% else %} + Final Winner + {% endif %} + {{ tour_games.2.game_id.winner_id.username }} +
+
+ Final Winner + +
+
+
+
+
+ + {% elif tournament_size == 8 %} +
+
+
+
+

Group A

+
+
+ {% if "http" in tour_games.0.game_id.user1_id.picture.url %} + Player 1 + {% else %} + Player 1 + {% endif %} + {{ tour_games.0.game_id.user1_id.username }} +
+
+ {{ tour_games.0.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.0.game_id.user2_id.picture.url %} + Player 2 + {% else %} + Player 2 + {% endif %} + {{ tour_games.0.game_id.user2_id.username }} +
+
+ {{ tour_games.0.game_id.nb_goals_user2 }} +
+
+
+
+
+

Group B

+
+
+ {% if "http" in tour_games.1.game_id.user1_id.picture.url %} + Player 3 + {% else %} + Player 3 + {% endif %} + {{ tour_games.1.game_id.user1_id.username }} +
+
+ {{ tour_games.1.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.1.game_id.user2_id.picture.url %} + Player 4 + {% else %} + Player 4 + {% endif %} + {{ tour_games.1.game_id.user2_id.username }} +
+
+ {{ tour_games.1.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.4.game_id.user1_id.picture.url %} + Semi 1 + {% else %} + Semi 1 + {% endif %} + {{ tour_games.4.game_id.user1_id.username }} +
+
+ {{ tour_games.4.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.4.game_id.user2_id.picture.url %} + Semi 2 + {% else %} + Semi 2 + {% endif %} + {{ tour_games.4.game_id.user2_id.username }} +
+
+ {{ tour_games.4.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.6.game_id.user1_id.picture.url %} + Player 6 + {% else %} + Player 6 + {% endif %} + {{ tour_games.6.game_id.user1_id.username }} +
+
+ {{ tour_games.6.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.6.game_id.winner_id.picture.url %} + Player 6 + {% else %} + Player 6 + {% endif %} + {{ tour_games.6.game_id.winner_id.username }} +
+
+ Final Winner + +
+
+
+
+
+
+
+
+

Group C

+
+
+ {% if "http" in tour_games.2.game_id.user1_id.picture.url %} + Player 5 + {% else %} + Player 5 + {% endif %} + {{ tour_games.2.game_id.user1_id.username }} +
+
+ {{ tour_games.2.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.2.game_id.user2_id.picture.url %} + Player 6 + {% else %} + Player 6 + {% endif %} + {{ tour_games.2.game_id.user2_id.username }} +
+
+ {{ tour_games.2.game_id.nb_goals_user2 }} +
+
+
+
+
+

Group D

+
+
+ {% if "http" in tour_games.3.game_id.user1_id.picture.url %} + Player 7 + {% else %} + Player 7 + {% endif %} + {{ tour_games.3.game_id.user1_id.username }} +
+
+ {{ tour_games.3.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.3.game_id.user2_id.picture.url %} + Player 8 + {% else %} + Player 8 + {% endif %} + {{ tour_games.3.game_id.user2_id.username }} +
+
+ {{ tour_games.3.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.5.game_id.user1_id.picture.url %} + Semi 1 + {% else %} + Semi 1 + {% endif %} + {{ tour_games.5.game_id.user1_id.username }} +
+
+ {{ tour_games.5.game_id.nb_goals_user1 }} +
+
+
+
+
+
+
+
+ {% if "http" in tour_games.5.game_id.user2_id.picture.url %} + Semi 2 + {% else %} + Semi 2 + {% endif %} + {{ tour_games.5.game_id.user2_id.username }} +
+
+ {{ tour_games.5.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+
+
+ {% if "http" in tour_games.6.game_id.user2_id.picture.url %} + Final 2 + {% else %} + Final 2 + {% endif %} + {{ tour_games.6.game_id.user2_id.username }} +
+
+ {{ tour_games.6.game_id.nb_goals_user2 }} +
+
+
+
+
+
+
+ {% else %} + + {% endif %} +
+ +
+
+{% endblock %} diff --git a/backend/pong/templates/pages/tournament_stats.html b/backend/pong/templates/pages/tournament_stats.html deleted file mode 100644 index 364f5a1..0000000 --- a/backend/pong/templates/pages/tournament_stats.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'navs.html' %} -{% load static %} - - -{% block main_content %} -
-

-
- - -{% endblock %} \ No newline at end of file diff --git a/backend/pong/templates/pages/view_profile.html b/backend/pong/templates/pages/view_profile.html index 834362f..8277aa5 100755 --- a/backend/pong/templates/pages/view_profile.html +++ b/backend/pong/templates/pages/view_profile.html @@ -5,390 +5,510 @@ {% block main_content %} + + -
-
-
- -
-
-

Remove friend

-

You're about to remove {{ user_view.username }} from your friends list. Are you sure?

-
-
- {% csrf_token %} - -
- -
-
-
-
-
- Success -

Password changed

-

Your password has been reset successfully.

-
- -
-
-
-
-
- {% if "http" in user_view.picture.url %} - Profile - {% else %} - Profile - {% endif %} -

{{ user_view.username }}

-
- {{ user_view.status }} - {% if is_own_profile %} -
- - - {% if user_view.user_42 is None %} - - {% endif %} -
- - {% elif is_friend %} - {% if friendship_status %} - - {% else %} - {% if me %} - - {% elif notification.status == 'Pending' %} -
- - -
- {% else %} -
- {% csrf_token %} - -
- {% endif %} - {% endif %} - {% else %} -
- {% csrf_token %} - -
- {% endif %} - -
-
- {% if user_view.description is None or user_view.description == "" %} - - {% else %} - {{ user_view.description }} - {% endif %} - -
- - -
-
-
-
-
- -
-
-
-
-
-
-

Wins

-
{{ stats.nb_games_won }}
-
-
-

Games Played

-
{{ stats.nb_games_played }}
-
-
-

First Round Goals

-
{{ stats.num_first_goals }}
-
-
-

Goals Scored / Goals Suffered Ratio

-
{{ goals_scored_suffered_ratio }}
-
-
-
-
-
-
-
Time Spent per Mode
-
Remote Game: {{ stats.remote_time_played }}
-
AI Game: {{ stats.ai_time_played }}
-
Local Game: {{ stats.local_time_played }}
-
Tournaments Game: {{ stats.tournament_time_played }}
-
-
-
-
-
-
-
-
-
Win Rate and Games Played / Day
-
{{ graph.0.win_rate }}
-
{{ graph.0.total_games }}
-
-
-
-
-
-
-
-
-
Records
-
Maximum Ball Speed: {{ stats.max_ball_speed }}
-
Maximum Rally Length: {{ stats.max_rally_length }}
-
Longest Game: {{ stats.longest_game }}
-
Quickest Game: {{ stats.quickest_game }}
-
-
-
-
-
- - -
-
+
+
+
+ +
+
+

Remove friend

+

You're about to remove {{ user_view.username }} from your friends list. Are you sure?

+
+
+ {% csrf_token %} + +
+ +
+
+
+
+
+ Success +

Password changed

+

Your password has been reset successfully.

+
+ +
+
+
+
+
+ {% if "http" in user_view.picture.url %} + Profile + {% else %} + Profile + {% endif %} +

{{ user_view.username }}

+
+ {{ user_view.status }} +
+ {% if is_own_profile %} +
+ + + {% if user_view.user_42 is None %} + + {% endif %} +
+ {% elif is_friend %} + {% if friendship_status %} + + {% else %} + {% if me %} + + {% elif notification.status == 'Pending' %} +
+ + +
+ {% else %} +
+ {% csrf_token %} + +
+ {% endif %} + {% endif %} + {% else %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ + +
+
+ {% if user_view.description is None or user_view.description == "" %} + + {% else %} + {{ user_view.description }} + {% endif %} +
+ + +
+
+ Enable 2FA (2-Factor Authentication) +
+ +
+
+ We'll ask for a code anytime you login again. This will take effect once you logout. +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+

Wins

+
{{ stats.nb_games_won }}
+
+ +
+
+
+

Games Played

+
{{ stats.nb_games_played }}
+
+ +
+
+
+
+
+

First Round Goals

+
{{ stats.num_first_goals }}
+
+ +
+
+
+

Goals Scored / Goals Suffered Ratio

+
{{ goals_scored_suffered_ratio }}
+
+ +
+
+
+ {% if games|length > 0 %} +
+
Time Spent per Mode
+
+
+ {% else %} +
+ +

No Available Data

+ You haven't played any games so far! +
+ {% endif %} +
+
+ {% if no_week_games %} +
+ +

No games this week

+ It seems you haven't played this week yet +
+ {% else %} +
+
Win Rate and Games Played / Day
+
+
+ {% endif %} +
+
Records
+
+
+ Max Speed +
+
Maximum Ball Speed
+
{{ stats.date_max_ball_speed }}
+
+
+
{{ stats.max_ball_speed }}
+
+
+
+ Max Speed +
+
Maximum Rally Length
+
{{ stats.date_max_rally_length }}
+
+
+
{{ stats.max_rally_length }}
+
+
+
+ Max Speed +
+
Longest Game
+
{{ stats.date_longest_game }}
+
+
+
{{ stats.longest_game }}
+
+
+
+ Max Speed +
+
Quickest Game
+
{{ stats.date_quickest_game }}
+
+
+
{{ stats.quickest_game }}
+
+
+
+
+
+ + +
+
{% endblock %} diff --git a/backend/pong/urls.py b/backend/pong/urls.py index 2bcd8c8..7d90138 100644 --- a/backend/pong/urls.py +++ b/backend/pong/urls.py @@ -74,14 +74,9 @@ path('games/create', views.game_create, name='game-create'), path('games/update/', views.game_update, name='game-update'), - #! Games Stats - path('debug/games//stats', views.game_stats, name='debug-game-stats'), - path('debug/games/stats', views.game_stats_all, name='debug-game-stats-all'), - #! Goals #path('games//goals/add', views.game_goals_create, name='game-goals-create'), - path('debug/games//goals', views.game_goals, name='game-goals'), - path('debug/games/goals', views.game_goals_all, name='game-goals-all'), + path('games//goals', views.game_goals, name='game-goals'), #! Tournaments path('tournaments/create', views.tournament_create, name='tournament-create'), @@ -103,9 +98,13 @@ #! Debug path('tournaments', views.tournament_list, name='tournament-list'), path('debug/games/', views.get_game, name='debug-get-game'), + path('debug/games/goals', views.game_goals_all, name='game-goals-all'), + path('debug/games//stats', views.game_stats, name='debug-game-stats'), + path('debug/games/stats', views.game_stats_all, name='debug-game-stats-all'), ] websocket_urlpatterns = [ path('ws/tournaments/', consumers.TournamentConsumer.as_asgi()), + path('ws/games/remote/queue', consumers.RemoteGameQueueConsumer.as_asgi()), ] \ No newline at end of file diff --git a/backend/pong/views.py b/backend/pong/views.py index 9c8efaf..5cd7db5 100644 --- a/backend/pong/views.py +++ b/backend/pong/views.py @@ -24,6 +24,7 @@ from icecream import ic from .models import Users import pprint +import socket # Since we want to create an API endpoint for reading, creating, and updating # Company objects, we can use Django Rest Framework mixins for such actions. from rest_framework import status @@ -89,8 +90,8 @@ def user_create(request): myuser.save() UserStats.objects.create( - user_id=myuser - ) + user_id=myuser + ) user = authenticate(username=username, password=password1) @@ -197,12 +198,16 @@ def search_suggestions(request): @csrf_exempt def search_users(request): + user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) term = request.GET.get('searched', '') userss = Users.objects.filter(username__icontains=term) return render(request, 'pages/search_users.html', { 'searched': term, 'userss': userss, 'numbers': userss.count(), + 'friends': friends, + 'user_id': user_id }) #! --------------------------------------- Friends --------------------------------------- @@ -333,24 +338,24 @@ def user_stats(request, user_id): return JsonResponse({'message': 'Method not allowed'}, status=405) def leaderboard(request): - if request.method == 'GET': - top_users = UserStats.objects.all().order_by('-nb_tournaments_won')[:3] + if request.method == 'GET': + top_users = UserStats.objects.all().order_by('-nb_tournaments_won')[:3] - enriched_top_users = [] - for top_user in top_users: - user = Users.objects.get(pk=top_user.user_id.id) + enriched_top_users = [] + for top_user in top_users: + user = Users.objects.get(pk=top_user.user_id.id) - user_stats_serializer = UserStatsSerializer(top_user) - user_serializer = UsersSerializer(user) + user_stats_serializer = UserStatsSerializer(top_user) + user_serializer = UsersSerializer(user) - enriched_data = user_stats_serializer.data - enriched_data['user'] = user_serializer.data + enriched_data = user_stats_serializer.data + enriched_data['user'] = user_serializer.data - enriched_top_users.append(enriched_data) + enriched_top_users.append(enriched_data) - return JsonResponse(enriched_top_users, safe=False, status=200) + return JsonResponse(enriched_top_users, safe=False, status=200) - return JsonResponse({'message': 'Method not allowed'}, status=405) + return JsonResponse({'message': 'Method not allowed'}, status=405) def current_place(request, user_id): @@ -371,15 +376,15 @@ def user_stats_update(user_id, game_id, data): game = Games.objects.get(pk=game_id) if game.user1_id.id == user_id: - stats.nb_goals_scored = data['nb_goals_user1'] - stats.nb_goals_suffered = data['nb_goals_user2'] + stats.nb_goals_scored += data['nb_goals_user1'] + stats.nb_goals_suffered += data['nb_goals_user2'] if data['user1_stats']['scored_first']: stats.num_first_goals += 1 if data['nb_goals_user1'] > data['nb_goals_user2']: stats.nb_games_won += 1 else: - stats.nb_goals_scored = data['nb_goals_user2'] - stats.nb_goals_suffered = data['nb_goals_user1'] + stats.nb_goals_scored += data['nb_goals_user2'] + stats.nb_goals_suffered += data['nb_goals_user1'] if data['user2_stats']['scored_first']: stats.num_first_goals += 1 if data['nb_goals_user2'] > data['nb_goals_user1']: @@ -395,12 +400,16 @@ def user_stats_update(user_id, game_id, data): if data['game_stats']['max_ball_speed'] > stats.max_ball_speed: stats.max_ball_speed = data['game_stats']['max_ball_speed'] + stats.date_max_ball_speed = game.created_at if data['game_stats']['longer_rally'] > stats.max_rally_length: stats.max_rally_length = data['game_stats']['longer_rally'] + stats.date_max_rally_length = game.created_at if data['duration'] < stats.quickest_game : stats.quickest_game = data['duration'] + stats.date_quickest_game = game.created_at if data['duration'] > stats.longest_game: stats.longest_game = data['duration'] + stats.date_longest_game = game.created_at stats.save() data_stats = UserStatsSerializer(stats) return JsonResponse({'message': 'User stats updated successfully', 'data': data_stats.data}, status=200) @@ -409,15 +418,15 @@ def win_rate_nb_games_day(request, user_id): today = timezone.now() seven_day_before = today - timedelta(days=7) games = Games.objects.filter((Q(user1_id = user_id) | Q(user2_id = user_id)) & Q(created_at__gte=seven_day_before) - & ~Q(type='Tournament')).order_by('-created_at') + & ~Q(type='Tournament')).order_by('-created_at') stats = games.annotate(day=TruncDay('created_at')).values('day').annotate( - total_games=Count('id'), ).annotate( - win_rate=Case( - When(total_games=0, then=0), - default=(100 * Count(Case(When(winner_id=user_id, then=1))) / Count('id')), - output_field=IntegerField() - ) + total_games=Count('id'), ).annotate( + win_rate=Case( + When(total_games=0, then=0), + default=(100 * Count(Case(When(winner_id=user_id, then=1))) / Count('id')), + output_field=IntegerField() + ) ).order_by('-day') return JsonResponse(list(stats), safe=False) @@ -462,7 +471,7 @@ def game_stats(request, game_id): stats = GamesStats.objects.get(game=game_id) serializer = GamesStatsSerializer(stats) data = serializer.data - return JsonResponse({'message': 'Game Stats', 'data': data}, status=200) + return JsonResponse(data, safe=False, status=200) except GamesStats.DoesNotExist: return JsonResponse({'message': 'GamesStats not found.'}, status=404) @@ -502,7 +511,7 @@ def game_goals(request, game_id): stats = Goals.objects.filter(game=game_id) serializer = GoalsSerializer(stats, many=True) data = serializer.data - return JsonResponse({'message': 'Game Stats', 'data': data}, status=200) + return JsonResponse(data, safe=False, status=200) except Goals.DoesNotExist: return JsonResponse({'message': 'Goals not found.'}, status=404) @@ -517,36 +526,31 @@ def game_goals_all(request): #! --------------------------------------- Games --------------------------------------- -@csrf_exempt -def game_create(request): - try: - data = json.loads(request.body.decode('utf-8')) - serializer = GamesSerializer(data=data) - data['start_date'] = datetime.now().isoformat() - if serializer.is_valid(): - serializer.save() - user1_id = data.get('user1_id') - user2_id = data.get('user2_id') - - if user1_id: - user1 = Users.objects.get(id=user1_id) - user1.status = "Playing" - user1.save() - - if user2_id: - user2 = Users.objects.get(id=user2_id) - user2.status = "Playing" - user2.save() - - return JsonResponse(serializer.data, status=201) +def game_create_helper(data: dict): + serializer = GamesSerializer(data=data) + data['start_date'] = datetime.now().isoformat() + + if not serializer.is_valid(): return JsonResponse(serializer.errors, status=400) - except json.JSONDecodeError: - return JsonResponse({'message': 'Invalid JSON', 'data': {}}, status=400) - except KeyError as e: - return JsonResponse({'message': f'Missing key: {str(e)}', 'data': {}}, status=400) - + + serializer.save() + user1_id = data.get('user1_id') + user2_id = data.get('user2_id') + + if user1_id: + user1 = Users.objects.get(id=user1_id) + user1.status = "Playing" + user1.save() + + if user2_id: + user2 = Users.objects.get(id=user2_id) + user2.status = "Playing" + user2.save() + + return JsonResponse(serializer.data, status=201) + @csrf_exempt -def game_update(request, game_id): +def game_create(request=None): if request.method != 'POST': return JsonResponse({'message': 'Method not allowed', 'method': request.method, 'data': {}}, status=405) if request.content_type != 'application/json': @@ -561,6 +565,10 @@ def game_update(request, game_id): except KeyError as e: return JsonResponse({'message': f'Missing key: {str(e)}', 'data': {}}, status=400) + return game_create_helper(data) + + +def game_update_helper(data, game_id): game = Games.objects.get(pk=game_id) game.duration = data['duration'] game.nb_goals_user1 = data['nb_goals_user1'] @@ -575,33 +583,54 @@ def game_update(request, game_id): player2 = Users.objects.get(pk=game.user2_id.id) player2.status = "Online" player2.save() - + + # elif data['nb_goals_user1'] < data['nb_goals_user2'] and game.type != "Local": if data['nb_goals_user1'] > data['nb_goals_user2']: game.winner_id = player1 - elif data['nb_goals_user1'] > data['nb_goals_user2'] and game.type != "Local": + else: game.winner_id = player2 game.save() - user_stats_update(player1.id, game_id, data) - user_stats_update(player2.id, game_id, data) - game_stats_create(game_id, data) - game_goals_create(game_id, data) + if game.type != "Local": + user_stats_update(player2.id, game_id, data) + game_stats_create(game_id, data) + game_goals_create(game_id, data) data = GamesSerializer(game).data return JsonResponse(data, status=200) + +@csrf_exempt +def game_update(request, game_id): + if request.method != 'POST': + return JsonResponse({'message': 'Method not allowed', 'method': request.method, 'data': {}}, status=405) + if request.content_type != 'application/json': + return JsonResponse({'message': 'Only JSON allowed', 'data': {}}, status=406) + + data = {} + + try: + data = json.loads(request.body.decode('utf-8'))['data'] + except json.JSONDecodeError: + return JsonResponse({'message': 'Invalid JSON', 'data': {}}, status=400) + except KeyError as e: + return JsonResponse({'message': f'Missing key: {str(e)}', 'data': {}}, status=400) + + return game_update_helper(data, game_id) @csrf_exempt def get_game(request, game_id): if request.method !='GET': return JsonResponse({'message': 'Method not allowed'}, status=405) - if request.method == 'GET': - try: - game = Games.objects.get(id=game_id) - serializer = GamesSerializer(game) - return JsonResponse({'message': 'Game Info', 'data': serializer.data}, status=200) - except Games.DoesNotExist: - return JsonResponse({'message': 'Game not found.'}, status=404) + try: + game = Games.objects.get(id=game_id) + serializer = GamesSerializer(game) + data = serializer.data + data['user1_id'] = UsersSerializer(game.user1_id).data + data['user2_id'] = UsersSerializer(game.user2_id).data + return JsonResponse({'message': 'Game Info', 'data': data}, status=200) + except Games.DoesNotExist: + return JsonResponse({'message': 'Game not found.'}, status=404) #! --------------------------------------- Tournaments --------------------------------------- @@ -877,16 +906,18 @@ def tournament_list_user(request, user_id): if request.method != 'GET': return JsonResponse({'message': 'Method not allowed', 'method': request.method, 'data': {}}, status=405) - all_tour=TournamentsUsers.objects.filter(user_id=user_id) - serializer = TournamentsUsersSerializer(all_tour, many=True) - all_tourusers_list = serializer.data + all_users = TournamentsUsers.objects.filter(user_id=user_id) + serializer = TournamentsUsersSerializer(all_users, many=True) + all_user_tours = serializer.data - for touruser in all_tourusers_list: + for touruser in all_user_tours: tournament = Tournaments.objects.get(pk=touruser['tournament_id']) serializer = TournamentsSerializer(tournament) touruser['tournament'] = serializer.data - return JsonResponse(all_tourusers_list, safe=False) + all_user_tours.sort(reverse=True, key=lambda t: t['created_at']) + + return JsonResponse(all_user_tours, safe=False) @csrf_exempt @@ -918,6 +949,8 @@ def tournament_update_game(request, tournament_id, game_id): user_id=tour_game.game_id.user2_id.id, tournament_id=tournament_id ) + user1 = Users.objects.get(pk=tour_game.game_id.user1_id.id) + user2 = Users.objects.get(pk=tour_game.game_id.user2_id.id) player1.score += data['nb_goals_user1'] * 100 player1.save() @@ -940,6 +973,11 @@ def tournament_update_game(request, tournament_id, game_id): for match in curr_phase_matches: if match.game_id.winner_id is not None: finished_matches += 1 + + user_stats_update(user2.id, game_id, data) + user_stats_update(user1.id, game_id, data) + game_stats_create(game_id, data) + game_goals_create(game_id, data) if finished_matches == total_phase_matches[curr_phase] and curr_phase != 'Final': return advance_tournament_phase(curr_phase, tournament_id) @@ -959,44 +997,45 @@ def tournament_update_game(request, tournament_id, game_id): @csrf_exempt -def get_access_token(code): - response = requests.post(settings.TOKEN_URL_A, data={ - 'grant_type': 'authorization_code', - 'client_id': settings.CLIENT_ID_A, - 'client_secret': settings.CLIENT_SECRET_A, - 'redirect_uri': settings.REDIRECT_URI_A, - 'code': code, - }) - if response.status_code == 200: - token_data = response.json() - access_token = token_data.get('access_token') - ic(access_token) - return access_token - else: - return None +def get_access_token(host, code): + response = requests.post(settings.TOKEN_URL_A, data={ + 'grant_type': 'authorization_code', + 'client_id': settings.CLIENT_ID_A, + 'client_secret': settings.CLIENT_SECRET_A, + 'redirect_uri': f'http://{host}/home42/', + 'code': code, + }) + ic(response.text) + if response.status_code == 200: + token_data = response.json() + access_token = token_data.get('access_token') + ic(access_token) + return access_token + else: + return None def get_user_info(token): - headers = { - "Authorization": f"Bearer {token}" - } - user_info_response = requests.get(settings.USER_INFO_URL_A, headers=headers) + headers = { + "Authorization": f"Bearer {token}" + } + user_info_response = requests.get(settings.USER_INFO_URL_A, headers=headers) - if user_info_response.status_code == 200: - return user_info_response.json() - else: - return None + if user_info_response.status_code == 200: + return user_info_response.json() + else: + return None def signin42(request): try: client_id = settings.CLIENT_ID_A - - authorization_url = f'https://api.intra.42.fr/oauth/authorize?client_id={client_id}&response_type=code&redirect_uri={settings.REDIRECT_URI_A}' + uri = f'http://{request.get_host()}/home42/' + authorization_url = f'https://api.intra.42.fr/oauth/authorize?client_id={client_id}&response_type=code&redirect_uri={uri}' ic(authorization_url) return HttpResponseRedirect(authorization_url) except Exception as e: - return HttpResponseRedirect(settings.REDIRECT_URI_A or '/') + return HttpResponseRedirect('/') def login42(request): authorization_code = request.GET.get('code') @@ -1004,7 +1043,7 @@ def login42(request): if authorization_code is None: return JsonResponse({'error': 'Authorization code missing', 'data': {}}, status=400) - access_token = get_access_token(authorization_code) + access_token = get_access_token(request.get_host(), authorization_code) if access_token is None: return JsonResponse({'error': 'Failed to fetch access token', 'data': {}}, status=400) @@ -1116,17 +1155,23 @@ def home(request): @login_required def gamelocal(request): + user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) user_id = request.user.id context = { + 'friends': friends, 'user_id': user_id, } return render(request,'pages/gamelocal.html', context) @login_required def gameonline(request): + user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) user_id = request.user.id context = { 'user_id': user_id, + 'friends': friends, } return render(request,'pages/gameonline.html', context) @@ -1166,31 +1211,54 @@ def tournaments(request): @login_required def ongoingtournaments(request, tournament_id): user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) + tournament = Tournaments.objects.get(pk=tournament_id) context = { 'user_id': user_id, - 'tournament_id': tournament_id + 'friends': friends, + 'tournament_id': tournament_id, + 'tournament_size': tournament.capacity, + 'tournament_name': tournament.name } return render(request,'pages/ongoing-tourn.html', context) @login_required def tournamentstats(request, tournament_id): + user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) + tournament = Tournaments.objects.get(pk=tournament_id) + tour_users = TournamentsUsers.objects.filter(tournament_id=tournament_id).order_by('placement') + tour_games = TournamentsGames.objects.filter(tournament_id=tournament_id).order_by('pk') context = { - 'tournament_id': tournament_id + 'friends': friends, + 'tournament_id': tournament_id, + 'tournament_size': tournament.capacity, + 'tournament_name': tournament.name, + 'tour_users': tour_users, + 'tour_games': tour_games, + 'user_id': user_id } - return render(request,'pages/tournament_stats.html', context) + ic(context) + return render(request,'pages/tournament_overview.html', context) @login_required def gamestats(request, game_id): + user_id = request.user.id + friends = Friends.objects.filter(Q(user1_id=user_id) | Q(user2_id=user_id)) stats = game_stats(request, game_id) data_stats = json.loads(stats.content) goals = game_goals(request, game_id) data_goals = json.loads(goals.content) - ic(data_goals) + game = json.loads(get_game(request, game_id).content)['data'] context = { + 'friends': friends, + 'game': game, 'game_id': game_id, 'stats': data_stats, - 'goals': data_goals + 'goals': data_goals, + 'user_id': user_id } + ic(context) return render(request,'pages/game_stats.html', context) @login_required @@ -1217,18 +1285,26 @@ def profile(request, id): friendship_status = None user = get_object_or_404(Users, id=id) - games = Games.objects.filter(Q(Q(user1_id=user_profile.id) | Q(user2_id=user_profile.id)) - ).exclude(type="Tournament").order_by('-created_at') tournament_response = tournament_list_user(request, user_profile.id) user_tournaments = json.loads(tournament_response.content) stats_response = user_stats(request, user_profile.id) stats = json.loads(stats_response.content) if stats['nb_goals_suffered'] != 0: - goals_sored_suffered_ratio = stats['nb_goals_scored'] / stats['nb_goals_suffered'] + goals_scored_suffered_ratio = round(stats['nb_goals_scored'] / stats['nb_goals_suffered'], 2) else: - goals_sored_suffered_ratio = 0 + goals_scored_suffered_ratio = 0 graph = win_rate_nb_games_day(request, user_profile.id) graph_send = json.loads(graph.content) + + games = Games.objects.filter(Q(user1_id=user_profile.id) | Q(user2_id=user_profile.id), + ).exclude(type="Tournament").order_by('-created_at') + + if games.count() != 0: + last_game_date = games.first().created_at + today = datetime.today() + monday = today - timedelta(days=today.weekday()) + monday = monday.astimezone(last_game_date.tzinfo) + context = { 'friends': friends, 'user_id': user_id, @@ -1242,11 +1318,12 @@ def profile(request, id): 'games': games, 'tours': user_tournaments, 'stats': stats, - 'goals_scored_suffered_ratio': goals_sored_suffered_ratio, + 'no_week_games': last_game_date < monday if games.count() != 0 else True, + 'goals_scored_suffered_ratio': goals_scored_suffered_ratio, 'graph': graph_send, 'page': 'profile' if is_own_profile else 'else' } - ic(context) + return render(request, 'pages/view_profile.html', context) diff --git a/backend/static/assets/icons/Collapse-Arrow.png b/backend/static/assets/icons/Collapse-Arrow.png index 2777a51..37072a8 100644 Binary files a/backend/static/assets/icons/Collapse-Arrow.png and b/backend/static/assets/icons/Collapse-Arrow.png differ diff --git a/backend/static/assets/icons/ball-speed.png b/backend/static/assets/icons/ball-speed.png new file mode 100644 index 0000000..e1350b2 Binary files /dev/null and b/backend/static/assets/icons/ball-speed.png differ diff --git a/backend/static/assets/icons/error.png b/backend/static/assets/icons/error.png new file mode 100644 index 0000000..1b7911d Binary files /dev/null and b/backend/static/assets/icons/error.png differ diff --git a/backend/static/assets/icons/error2.png b/backend/static/assets/icons/error2.png new file mode 100644 index 0000000..e062ba7 Binary files /dev/null and b/backend/static/assets/icons/error2.png differ diff --git a/backend/static/assets/icons/fast.png b/backend/static/assets/icons/fast.png new file mode 100644 index 0000000..dbd85bf Binary files /dev/null and b/backend/static/assets/icons/fast.png differ diff --git a/backend/static/assets/icons/logo2.png b/backend/static/assets/icons/logo2.png index cf8e6dc..99d9e28 100644 Binary files a/backend/static/assets/icons/logo2.png and b/backend/static/assets/icons/logo2.png differ diff --git a/backend/static/assets/icons/longest.png b/backend/static/assets/icons/longest.png new file mode 100644 index 0000000..8b98b94 Binary files /dev/null and b/backend/static/assets/icons/longest.png differ diff --git a/backend/static/assets/icons/pie.png b/backend/static/assets/icons/pie.png new file mode 100644 index 0000000..add3f69 Binary files /dev/null and b/backend/static/assets/icons/pie.png differ diff --git a/backend/static/assets/icons/pong2.png b/backend/static/assets/icons/pong2.png new file mode 100644 index 0000000..fe1179e Binary files /dev/null and b/backend/static/assets/icons/pong2.png differ diff --git a/backend/static/assets/icons/prize.png b/backend/static/assets/icons/prize.png new file mode 100644 index 0000000..8c6d029 Binary files /dev/null and b/backend/static/assets/icons/prize.png differ diff --git a/backend/static/assets/icons/quickest.png b/backend/static/assets/icons/quickest.png new file mode 100644 index 0000000..bf165ce Binary files /dev/null and b/backend/static/assets/icons/quickest.png differ diff --git a/backend/static/assets/icons/rally-len.png b/backend/static/assets/icons/rally-len.png new file mode 100644 index 0000000..7045052 Binary files /dev/null and b/backend/static/assets/icons/rally-len.png differ diff --git a/backend/static/assets/icons/rally.png b/backend/static/assets/icons/rally.png new file mode 100644 index 0000000..952365e Binary files /dev/null and b/backend/static/assets/icons/rally.png differ diff --git a/backend/static/assets/icons/return.png b/backend/static/assets/icons/return.png new file mode 100644 index 0000000..2777a51 Binary files /dev/null and b/backend/static/assets/icons/return.png differ diff --git a/backend/static/assets/icons/speed.png b/backend/static/assets/icons/speed.png new file mode 100644 index 0000000..06a6360 Binary files /dev/null and b/backend/static/assets/icons/speed.png differ diff --git a/backend/static/assets/images/no-data-image.png b/backend/static/assets/images/no-data-image.png new file mode 100644 index 0000000..43f7e60 Binary files /dev/null and b/backend/static/assets/images/no-data-image.png differ diff --git a/backend/static/css/game-stats.css b/backend/static/css/game-stats.css new file mode 100644 index 0000000..d28ed42 --- /dev/null +++ b/backend/static/css/game-stats.css @@ -0,0 +1,233 @@ +#main-wrapper { + display: flex; + width: 100%; + gap: 20px; +} + +.scoreboard { + font-size: x-large; + font-weight: bold; + width: 130px; +} + +.profile-head { + color: #FFFFFF; + height: 20%; +} + +.stats2 { + flex-direction: column; + width: 100%; + height: 100%; + +} + +.percentages { + margin-bottom: 20px; +} + + +.profile-foot { + width: 100%; + height: 60%; + margin-top: 10px; + min-width: 1624px; + min-height: 500px; +} + +.bar-graphs { + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + width: 56%; + height: 100%; + margin: 5px; +} + +.records { + margin: 5px; + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + height: 100%; + padding: 20px; + +} + +.record-info { + width: 100%; + padding: 20px; + padding: 0; + padding-top: 20px; + padding-bottom: 5px; +} + +.record-info-tit { + height: 35px; + text-align: left; + padding-left: 5px; + +} + +.img-tit { + flex-direction: row; +} + +.img-tit img { + width: 40px; + height: 40px; +} + +.record-title { + color: #FFFFFF; + font-size: large; + margin-bottom: 0; +} + +.record-sub { + color: #FFFFFF; + font-size: x-small; + margin-bottom: 0; +} + + +.record-date { + color: #ffffff83; + font-size: x-small; + +} + +.stats-title { + color: #c3c3c3bb; + font-size: x-small; +} + +.stat .winr { + font-size: larger !important; +} +.icon-stat { + height: 30px; + width: 30px; +} + +.graph-title { + font-size: x-large; + color: #fff; +} + +.winr, .graph-value { + font-weight: bold; + color: #F8D082; +} + +.numbers-stat{ + flex-direction: column; +} + +.numbers-stat h4 { + margin: 0; +} + +.graph-img { + max-width: 50%; + max-height: 50%; +} + +.averages { + width: 28%; + height: 100%; + min-width: 320px; +} + +.performance { + width: 16%; + height: 100%; + text-align: left; + white-space: nowrap; +} + +.records-title { + font-size: large; + color: #fff; + margin-bottom: 16px; +} + +.rally-len, .ball-speed { + /*background: url('../assets/images/rally-len.png') no-repeat;*/ + background: linear-gradient(to right, #ffffff00, #f8d18218); + background-size: cover; /* Make the image cover the entire div */ + width: 90%; + border-radius: 8px; + border: 1px solid #ffffff2a; +} + +.grey-line { + width: 2px; + height: 50px; + background-color: #ffffff5d; + border-radius: 8px; +} + +.rally-len div, .ball-speed div { + width: 88px; + color: #FFFFFF; +} + +.rally-len div h6, .ball-speed div h6 { + margin-bottom: 0; + font-size: x-large; +} + +.rally-len div span, .ball-speed div span { + font-size: smaller; +} + +.squares { + color: #FFFFFF; + width: 80%; + min-width: 270px; + padding-bottom: 8px; +} + +.square-length, .square-speed { + width: 35px; + height: 28px; + background-color: #FFFFFF; + border-radius: 3px; + color: #836938; + font-weight: bold; + font-size: 14px; + margin-bottom: 3px; + display: flex; + align-items: center; + justify-content: center; +} + +.squares span { + font-size: smaller; +} + +.record-info .name { + padding-left: 10px; + color: #FFFFFF; + margin-bottom: 0; + font-size: large; +} + +.record-info .winr { + font-size: xx-large; + margin-bottom: 0; +} + + +.records .record-title { + padding-bottom: 10px; + +} + + +#chart { + width: 70%; /* Set the width of the chart */ + height: 70%; /* Set the height of the chart */ + margin: 0 auto; /* Center the chart */ +} \ No newline at end of file diff --git a/backend/static/css/game/game.css b/backend/static/css/game/game.css index fda47fb..51a2d84 100644 --- a/backend/static/css/game/game.css +++ b/backend/static/css/game/game.css @@ -24,4 +24,32 @@ position: absolute; top: 0px; right: 100px; +} + +#scoreboard { + color: white; + height: 50px; + width: 100%; + position: absolute; + top: 36px; +} + +#p1 { + font-size: 24px; + text-align: end; +} + +#p2 { + font-size: 24px; + text-align: start; +} + +#score { + font-size: 32px; + font-weight: bold; +} + +#p1, #p2, #score { + width: 300px; + /* background-color: red; */ } \ No newline at end of file diff --git a/backend/static/css/match-block.css b/backend/static/css/match-block.css index 9b1370f..42a4ed2 100644 --- a/backend/static/css/match-block.css +++ b/backend/static/css/match-block.css @@ -1,26 +1,45 @@ + .match-block { width: 98%; margin: 10px; border-radius: 8px; background-color: #1A3141; + margin-bottom: 0; } -.match-block.tournament.third-place .details .result { - color: #E65C19; +.header { + padding-left: 40px; + width: calc(98% - 55px); + float: right; + margin-right: 1.5%; + } -.match-block.tournament.below-third-place .details .result { - color: grey; +.header .title { + color: #f2f2f269; + width: 20%; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + height: 30px; + position: relative; + display: flex; + align-items: center; + overflow: hidden; } -.match-block.pingpong.victory .details .result { - color: green; +.date-second { + color: #ffffffc0; + font-size: small; } -.match-block.pingpong.defeat .details .result { - color: red; +.placement { + font-weight: bold; } +.victory { + color: #F8D082; +} .match-block .icon { padding: 10px 10px 10px 5px; border-top-left-radius: 10px; @@ -43,17 +62,95 @@ flex: 1; height: 64px; z-index: 100; - background-color: #101d27; + background-color: #28313a; + color: #FFF; + padding-left: 40px; +} + +.details .content { + width: 20%; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + height: 30px; + position: relative; + display: flex; + align-items: center; + overflow: hidden; +} + +.details .last { + height: 40px; + display: flex; + align-items: center; + justify-content: space-between; + padding-right: 40px; } .match-block .details .type { font-weight: bold; } -.match-block .details .result { - margin-top: 5px; +a:hover { + cursor: pointer; +} + +.toggle-details { + border: none; + background: none; + padding: 0; + margin: 0; +} + +.last a { + text-decoration: none; + color: inherit; +} + +.last img { + margin-right: 10px; +} + +.match-container{ + border-radius: 8px; + flex: 1; + height: 64px; + z-index: 100; + background-color: #28313a; + color: #FFF; + padding-left: 40px; +} + +.match-block2 { + background-color: #1A3141; + margin-bottom: 1px; +} + +.header2 { + margin-bottom: 1px; + +} + +.header2 .title { + color: #f2f2f269; + background-color: #28313a; + width: 20%; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + height: 64px; + position: relative; + display: flex; + align-items: center; + overflow: hidden; +} + +.enlarged { + background: none !important; } -.match-block .opponent { - margin-left: 10px; +.enlarged2 { + border-bottom-left-radius: 0 ; + border-bottom-right-radius: 0 ; + margin-bottom: 1px; } \ No newline at end of file diff --git a/backend/static/css/ongoing-tourn.css b/backend/static/css/ongoing-tourn.css index aa51aa4..6e13bb3 100644 --- a/backend/static/css/ongoing-tourn.css +++ b/backend/static/css/ongoing-tourn.css @@ -1,7 +1,9 @@ .tourn-header { - color: #f2f2f2; - padding-top: 5vh; - min-width: 830px; + color: #f2f2f2; + padding-top: 5vh; + min-width: 1750px; + height: 100px; + } .tourn-info { @@ -20,7 +22,7 @@ top: 50%; position: absolute; transform: translate(0,-50%); - width: 90%; + width: 70%; } .round { @@ -33,8 +35,15 @@ flex-direction: column; margin-bottom: 20px; position: relative; + height: inherit; } +.matchup8 { + flex-direction: column; + margin-bottom: 20px; + position: relative; + height: 428px; +} .group { color: white; font-size: large; @@ -48,6 +57,7 @@ transition: all 0.3s ease; width: 250px; height: 50px; + margin-bottom: 50px; } .main-core { @@ -78,19 +88,68 @@ } .score { - font-size: 14px; - color: #666; + font-size: 20px; + font-weight: bold; + color: #ccc; } -.line { +.line-h { + position: relative; + width: 100%; + height: 4px; + background-color: #ccc; + left: 40%; + /* transform: translateX(-50%); */ + z-index: -1; + transition: background-color 0.3s ease; + top: -77px; +} + +.line-v-d { + position: absolute; + width: 4px; + height: 58px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 57px; +} + +.u { + position: absolute; + width: 4px; + height: 58px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 57px; +} + +.d { position: absolute; - width: 2px; - height: 100%; + width: 4px; + height: 62px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 113px; +} +.line-h2 { + position: relative; + width: 50%; + height: 4px; background-color: #ccc; - left: 50%; - transform: translateX(-50%); + left: 140%; + /* transform: translateX(-50%); */ z-index: -1; transition: background-color 0.3s ease; + top: -29px; } .round-1 .matchup .line { @@ -104,4 +163,5 @@ .round-3 .matchup .line { top: 50%; height: 50%; + } diff --git a/backend/static/css/profile.css b/backend/static/css/profile.css index 9083c93..419bda6 100644 --- a/backend/static/css/profile.css +++ b/backend/static/css/profile.css @@ -1,95 +1,98 @@ #main-wrapper { - display: flex; - width: 100%; - gap: 20px; + width: 100%; + gap: 20px; + /*height: inherit;*/ + /*overflow: visible;*/ } #profile-main { - width: 30%; - flex-shrink: 0; + /*width: 30%; */ + flex-shrink: 0; + width:initial; + padding-top: 80px; } .side-profile { - flex-direction: column; - width: 15vw; /* Adjust width as needed */ - padding: 20px; - border-radius: 8px; - color: #FFFFFF; + flex-direction: column; + width: 15vw; /* Adjust width as needed */ + padding: 20px; + border-radius: 8px; + color: #FFFFFF; min-width: 360px; } .side-profile img.profile-big { - width: 150px; - height: 150px; - border-radius: 50%; + width: 150px; + height: 150px; + border-radius: 50%; object-fit: cover; } #change-info2 { - width: inherit; - font-size: small; - color: #f2f2f2; + width: inherit; + font-size: small; + color: #f2f2f2; min-width: inherit; } .side-profile .description { - text-align: center; - margin-top: 10px; + text-align: center; + margin-top: 10px; } #description-form{ - width: inherit; - display: none; + width: inherit; + display: none; } textarea, #username-input, #email-input { - width: 100%; - border-radius: 5px; - background-color: #101d27; - border: none; - color: white; + width: 100%; + border-radius: 5px; + background-color: #101d27; + border: none; + color: white; } .description{ - width: inherit; + width: inherit; } .user-description { - display: inline; - vertical-align: middle; - margin-block: 10px; + display: inline; + vertical-align: middle; + margin-block: 10px; overflow-y: scroll; text-overflow: ellipsis; max-height: 200px; - text-align: justify; - overflow-x: hidden; - + text-align: center; + overflow-x: hidden; + width: 100%; } .text-block{ - font-size: small; - margin: 0; + font-size: small; + margin: 0; } .email-joined-edit { - padding-top: 5px; - padding-bottom: 5px; + padding-top: 5px; + padding-bottom: 5px; } .edit-change, .save-cancel { - width: 360px; + width: 360px; height: 52.3px; } .side-profile button { - padding: 10px 20px; - width: 170px; + padding: 10px 20px; + width: 170px; } .edit, #save-profile-button { - background-color: #1A3141; + background-color: #1A3141; } .edit:hover, #save-profile-button:hover { @@ -109,7 +112,7 @@ textarea, #username-input, #email-input { } #open-change-password-modal, #cancel-edit-button, .friend-status { - background-color: #1D2731; + background-color: #1D2731; } #decline-friend-button { @@ -125,7 +128,7 @@ textarea, #username-input, #email-input { } #cancel-change-password-button { - background-color: #1D2731; + background-color: #1D2731; border: none; } @@ -135,129 +138,250 @@ textarea, #username-input, #email-input { } #change-password-form { - flex-direction: column; - color: #f2f2f2; - width: 100%; + flex-direction: column; + color: #f2f2f2; + width: 100%; padding-top: 10px; padding-bottom: 16px; - + } #change-password-form label { - padding-top: 6px; + padding-top: 6px; } .pass-input { - background-color: #101d27; - width: 99%; - border-radius: 5px; - border: none; + background-color: #101d27; + width: 99%; + border-radius: 5px; + border: none; color: #c3c3c3bb; - flex-shrink: 0; + flex-shrink: 0; } - - #tabs { - flex-grow: 1; - width: 70%; - padding-top: 30px; + width: 80%; + padding-top: 30px; + min-width: 900px; +} + +.my-nav-tabs { + padding-left: calc(50% - 125px); } -#tabs .nav-tabs { - margin-bottom: 20px; +#tabs .my-nav-tabs { + margin-bottom: 20px; + border-bottom: none; } #tabs .nav-item .nav-link { - cursor: pointer; + display: inline-block; + cursor: pointer; + color: #c3c3c3bb; } +.nav-item { + padding: 10px; +} #tabs .nav-item .nav-link.active { - background-color: #f8f9fa; - border-color: #dee2e6 #dee2e6 #fff; + color: #f8f9fa; + font-weight: bold; + background: none; + border: none; + border-bottom: 2px solid #F8D082; } - - .stats-history { - flex-direction: column; - flex: 1; - padding: 20px; + flex-direction: column; + flex: 1; + padding: 20px; } .stats { - flex-direction: column; - margin-bottom: 20px; - color: #FFFFFF; - padding: 20px; + flex-direction: column; + margin-bottom: 20px; + color: #FFFFFF; + padding: 20px; } .topic { - margin-bottom: 20px; + margin-bottom: 20px; } .topic .icon { - width: 24px; - height: 24px; - margin-right: 10px; + width: 24px; + height: 24px; + margin-right: 10px; } .stats2 { - flex-direction: column; - + flex-direction: column; + width: 100%; + height: 100%; + +} + +.profile-head { + height: 40%; + min-height: 266px; } .percentages { - margin-bottom: 20px; + margin-bottom: 20px; } .stats-head { - width: 100%; + flex-direction: column; + width: 50%; + height: 100%; + margin: 5px; + } +.two-joined { + height: 50%; +} .stat { - flex-direction: column; - text-align: center; - padding: 10px; + text-align: center; + padding: 16px; + width: 49%; + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + +} + +.donut { + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + flex-direction: column; + width: 50%; + height: 100%; + margin: 5px; + + +} + +.profile-foot { + width: 100%; + height: 60%; + margin-top: 10px; +} + +.bar-graphs { + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + width: 65%; + height: 100%; + margin: 5px; +} + +.records { + margin: 5px; + border-radius: 8px; + background-color: #ffffff0C; + border: 1px solid #ffffff2a; + width: 35%; + height: 100%; + +} + +.record-info { + width: 100%; + padding: 20px; + padding-bottom: 5px; +} + +.record-info-tit { + height: 35px; + text-align: left; + padding-left: 5px; + +} + +.img-tit { + flex-direction: row; +} + +.img-tit img { + width: 34px; + height: 34px; + margin-right: 8px; +} + +.record-tit { + color: #FFFFFF; + font-size: 15px; + margin-bottom: 0; +} + +.record-date { + color: #ffffff83; + font-size: x-small; + margin-bottom: 0px; } .graph1, .graph2, .graph3, .graph4 { - width: 15vw; - height: 100px; - margin-left: 10px; - background-color: #101d27; /* Light gray background for graphs */ - border-radius: 8px; + width: 15vw; + height: 100px; + margin-left: 10px; + background-color: #101d27; /* Light gray background for graphs */ + border-radius: 8px; } .stats-title { - color: #c3c3c3bb; + color: #c3c3c3bb; + font-size: small; +} + +.icon-stat { + height: 30px; + width: 30px; } .graph-title { - font-size: smaller; - color: #c3c3c3bb; + align-self: flex-start; + padding: 20px; + font-size: x-large; + padding-bottom: 5px; +} + +.tour-game-winner { + color: #F8D082; + font-weight: bold; } .winr, .graph-value { - font-weight: bold; - color: #F8D082; + font-weight: bold; + color: #F8D082; + font-size: x-large; } .numbers-stat{ - flex-direction: column; + flex-direction: column; } .numbers-stat h4 { - margin: 0; + margin: 0; } .graph-img { - max-width: 50%; - max-height: 50%; + max-width: 50%; + max-height: 50%; } #games, #tournaments{ - max-height: 80%; - overflow-y: auto; + height: 750px; + overflow-y: auto; + overflow-x: hidden; +} + +.match-history-list { + + overflow-y: auto; + width: 100%; + } @@ -267,10 +391,121 @@ textarea, #username-input, #email-input { #togglePassword, #togglePassword2, #togglePassword3 { position: relative; - right: 30px; - top: -4%; - background: none; - border: none; - cursor: pointer; - padding: 0; + right: 30px; + top: -4%; + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.tournament-details { + width: 98%; + margin-left: 10px; + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; + height: 252px; + overflow-y: scroll; +} + +.apexcharts-legend-text { + font-family: inherit; + padding-left: 25px; +} + +#chart1 { + display: flex; + align-items: center; +} + +.apexcharts-datalabel-label, +.apexcharts-datalabel-value { + fill: #fff !important; +} + +#chart2 { + width: 100%; } + + +/* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: 43px; + height: 20px; + +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid #cccccc4d; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 1px; + bottom: 0px; + background-color: #ffffffd3; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #F8D082; +} + + +input:checked + .slider:before { + -webkit-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; + height: 20px; + width: 42px; +} + +.slider.round:before { + border-radius: 50%; +} + +.no-data { + color: white; +} + +.no-data h4 { + font-weight: bolder; + margin-bottom: 4px; + margin-top: 16px; +} + +.no-data img { + width: 140px; + height: 140px; +} + +.game-link{ + text-decoration:none; +} \ No newline at end of file diff --git a/backend/static/css/side-menu.css b/backend/static/css/side-menu.css index d07acfb..451c1a5 100755 --- a/backend/static/css/side-menu.css +++ b/backend/static/css/side-menu.css @@ -87,10 +87,9 @@ body { .left-div .icon2 { width: 50px; /* Adjust icon size as needed */ - height: 50px; margin: 5px 0; /* Vertical margin between icons */ margin-right: 15px; - + padding-top: 10px; } .left-div.expanded .icon-title { @@ -183,4 +182,11 @@ h2 { padding-top: 60px; font-weight: bold; color: #FFFFFF; -}*/ \ No newline at end of file +}*/ + +.profile-medium { + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; +} \ No newline at end of file diff --git a/backend/static/css/tourn-overview.css b/backend/static/css/tourn-overview.css new file mode 100644 index 0000000..028a8c7 --- /dev/null +++ b/backend/static/css/tourn-overview.css @@ -0,0 +1,214 @@ +.tourn-header { + color: #f2f2f2; + padding-top: 5vh; + min-width: 1750px; + height: 100px; + +} + +.leaderboard { + color: white; + /*position: relative;*/ + width: 400px; + text-align: left; + margin: 50px; + align-self: normal; + +} + +.header .title { + color: #f2f2f269; + width: 33%; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + height: 30px; + position: relative; + display: flex; + align-items: center; + overflow: hidden; + +} + +.details { + border-radius: 8px; + flex: 1; + height: 64px; + z-index: 100; + background-color: #28313a; + color: #FFF; + border: 1px solid #ffffff2a; + margin-bottom: 8px; +} + +.details .content { + width: 33%; + text-align: left; + white-space: nowrap; + text-overflow: ellipsis; + height: 40px; + position: relative; + display: flex; + align-items: center; + overflow: hidden; +} + +.tourn-info { + position: absolute; + left: 50%; + color: #f2f2f2; + transform: translate(-50%); +} + +.tourn-status { + color: #8E8E8E; + font-size: medium; +} + +.all-groups { + top: 50%; + position: absolute; + transform: translate(0,-50%); + width: 70%; +} + +.round { + flex-direction: column; + width: 260px; + position: relative; +} + +.matchup { + flex-direction: column; + margin-bottom: 20px; + position: relative; + height: inherit; +} + +.matchup8 { + flex-direction: column; + margin-bottom: 20px; + position: relative; + height: 428px; +} +.group { + color: white; + font-size: large; +} +.player { + background-color: #1A3141; + color: #8E8E8E; + border-radius: 8px; + margin: 5px; + cursor: pointer; + transition: all 0.3s ease; + width: 250px; + height: 50px; + margin-bottom: 50px; +} + +.main-core { + background-color: #13232e; + width: 225px; + height: 50px; + border-radius: 8px; +} + +.icon { + height: 40px; + margin-right: 10px; + margin-left: 10px; + border-radius: 50%; + width: 40px; + object-fit: cover; +} + +.name { + flex-grow: 1; + font-weight: bold; +} + +.score2 { + padding: 10px; + height: inherit; + border: 10px; + +} +.score { + font-size: 20px; + font-weight: bold; + color: #ccc; +} + +.line-h { + position: relative; + width: 100%; + height: 4px; + background-color: #ccc; + left: 40%; + /* transform: translateX(-50%); */ + z-index: -1; + transition: background-color 0.3s ease; + top: -77px; +} + +.line-v-d { + position: absolute; + width: 4px; + height: 58px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 57px; +} + +.u { + position: absolute; + width: 4px; + height: 58px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 57px; +} + +.d { + position: absolute; + width: 4px; + height: 62px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + /* z-index: -1; */ + /* transition: background-color 0.3s ease; */ + top: 113px; +} +.line-h2 { + position: relative; + width: 50%; + height: 4px; + background-color: #ccc; + left: 140%; + /* transform: translateX(-50%); */ + z-index: -1; + transition: background-color 0.3s ease; + top: -29px; +} + +.round-1 .matchup .line { + top: 25%; +} + +.round-2 .matchup .line { + top: 25%; +} + +.round-3 .matchup .line { + top: 50%; + height: 50%; + +} diff --git a/backend/static/js/edit-profile.js b/backend/static/js/edit-profile.js index 4222ff0..9539623 100755 --- a/backend/static/js/edit-profile.js +++ b/backend/static/js/edit-profile.js @@ -1,8 +1,7 @@ function onEditButtonClick() { - document.querySelector("div.save-cancel.align-items-center.justify-content-around").style.display = "block"; - document.querySelector("div.save-cancel.align-items-center.justify-content-around").classList.add('d-flex'); - document.querySelector("div.edit-change.d-flex.align-items-center.justify-content-between").style.display = "none"; - document.querySelector("div.edit-change.d-flex.align-items-center.justify-content-between").classList.remove("d-flex"); + document.getElementById("save-cancel").style.display = "flex"; + document.getElementById("edit-change1").style.display = "none"; + document.getElementById("edit-change1").classList.remove("d-flex"); document.getElementById("edit-profile-button").style.display = "none"; document.getElementById("save-profile-button").style.display = "inline-block"; document.getElementById("cancel-edit-button").style.display = "inline-block"; @@ -27,10 +26,9 @@ function onEditButtonClick() { } function onCancelButtonClick() { - document.querySelector("div.edit-change.align-items-center.justify-content-between").style.display = "block"; - document.querySelector("div.edit-change.align-items-center.justify-content-between").classList.add('d-flex'); - document.querySelector("div.save-cancel.d-flex.align-items-center.justify-content-around").style.display = "none"; - document.querySelector("div.save-cancel.d-flex.align-items-center.justify-content-around").classList.remove("d-flex"); + document.getElementById("edit-change1").style.display = "flex"; + document.getElementById("save-cancel").style.display = "none"; + document.getElementById("save-cancel").classList.remove("d-flex"); document.getElementById("edit-profile-button").style.display = "flex"; document.getElementById("save-profile-button").style.display = "none"; document.getElementById("cancel-edit-button").style.display = "none"; @@ -63,28 +61,4 @@ function onSaveButtonClick(event, userId) { } }) .catch(error => console.error('Error:', error)); -} - -// Get the modal -var modal2 = document.getElementById("modal2"); -// Get the button that opens the modal -var btn2 = document.getElementById("remove-friend-button"); -// Get the element that closes the modal -var goback = document.getElementById("cancel"); -// When the user clicks the button, open the modal -if (btn2) { - btn2.onclick = function() { - modal2.style.display = "block"; - } -} - -goback.onclick = function() { - modal2.style.display = "none"; -} - -// When the user clicks anywhere outside of the modal2, close it -window.onclick = function(event) { - if (event.target == modal2) { - modal2.style.display = "none"; - } -} +} \ No newline at end of file diff --git a/backend/static/js/game-stats.js b/backend/static/js/game-stats.js new file mode 100644 index 0000000..69fb527 --- /dev/null +++ b/backend/static/js/game-stats.js @@ -0,0 +1,177 @@ +const FAST_BALL_SPEED = 2; +const HIGH_RALLY_LENGTH = 15; + +function interpolateColor(startColor, endColor, factor) { + const result = startColor.slice(); // Create a copy of the start color + for (let i = 0; i < 3; i++) { + // Interpolate each color component (R, G, B) + result[i] = Math.round(result[i] + factor * (endColor[i] - startColor[i])); + } + return result; +} + +// Convert RGB array to hex color string +function rgbToHex(rgb) { + return `#${rgb.map(x => x.toString(16).padStart(2, '0')).join('')}`; +} + +// Main function to apply the background color to all divs with class 'square' +function applyGradientToHeatmap() { + const rallyLengthSquares = document.querySelectorAll('.square-length'); // Select all divs with class 'square' + const ballSpeedSquares = document.querySelectorAll('.square-speed'); // Select all divs with class 'square' + const startColor = [255, 255, 255]; // RGB for #FFFFFF (white) + const endColor = [253, 180, 39]; // RGB for #F8D082 (light orange) + + rallyLengthSquares.forEach(square => { + const value = parseInt(square.textContent); // Get the text inside the div and convert to integer + const factor = value / HIGH_RALLY_LENGTH; // Calculate the interpolation factor (0 to 1) + + const interpolatedColor = interpolateColor(startColor, endColor, factor); // Get the interpolated color + const hexColor = rgbToHex(interpolatedColor); // Convert the RGB color to hex format + + square.style.backgroundColor = hexColor; // Apply the background color to the div + }); + ballSpeedSquares.forEach(square => { + const value = parseInt(square.textContent); // Get the text inside the div and convert to integer + const factor = value / FAST_BALL_SPEED; // Calculate the interpolation factor (0 to 1) + + const interpolatedColor = interpolateColor(startColor, endColor, factor); // Get the interpolated color + const hexColor = rgbToHex(interpolatedColor); // Convert the RGB color to hex format + + square.style.backgroundColor = hexColor; // Apply the background color to the div + }); +} + +function fillHeatmap(rallyLengths, ballSpeeds){ + const rallyLengthLabels = document.querySelectorAll('.square-length'); + const ballSpeedLabels = document.querySelectorAll('.square-speed'); + + console.log(rallyLengths, ballSpeeds); + + rallyLengths.forEach((rally, i) => { + console.log(rally, i); + rallyLengthLabels[i].textContent = rally + }); + ballSpeeds.forEach((speed, i) => ballSpeedLabels[i].textContent = speed); + +} + +// Call the function to apply the background gradient on page load + + +async function loadCharts() { + const gameID = document.getElementById('game-id').getAttribute('data-game-id'); + const response = await fetch(`/games/${gameID}/goals`, { + method: "GET", + }); + const goals = await response.json(); + console.log(goals); + + const rallyLengths = goals.map((goal) => goal.rally_length); + const ballSpeeds = goals.map((goal) => goal.ball_speed); + + fillHeatmap(rallyLengths, ballSpeeds); + applyGradientToHeatmap(); + + const user1ID = document.getElementById('game-id').getAttribute('data-user-1'); + const user2ID = document.getElementById('game-id').getAttribute('data-user-2'); + const username1 = document.getElementById('game-id').getAttribute('data-user-1-name'); + const username2 = document.getElementById('game-id').getAttribute('data-user-2-name'); + + const gameState = {}; + gameState[user1ID] = {'score': 0, 'state': []}; + gameState[user2ID] = {'score': 0, 'state': []}; + + goals.forEach((goal) => { + gameState[goal.user].score++; + gameState[user1ID].state.push(gameState[user1ID].score); + gameState[user2ID].state.push(gameState[user2ID].score); + }); + + console.log(gameState); + + var options = { + chart: { + type: 'line', + toolbar: { + show: false + }, + }, + stroke: { + curve: 'straight', + }, + markers: { + size: 1, + }, + series: [ + { + name: username1, + data: gameState[user1ID].state + }, + { + name: username2, + data: gameState[user2ID].state + } + ], + xaxis: { + categories: [1, 2, 3, 4, 5, 6, 7, 8, 9], + labels: { + style: { + colors: '#c3c3c3bb', + fontSize: '14px', + fontWeight: 600 + } + }, + axisTicks: { + show: true, + color: '#333', + height: 6 + } + }, + yaxis: { + labels: { + style: { + colors: '#c3c3c3bb', + fontSize: '12px', + fontWeight: 500 + } + }, + axisTicks: { + show: true, + color: '#333', + width: 5 + } + }, + grid: { + show: true, + borderColor: '#c3c3c3bb', + xaxis: { + lines: { + show: true + } + }, + yaxis: { + lines: { + show: true + } + } + }, + legend: { + show: true, + horizontalAlign: 'center', + fontSize: '16px', + fontWeight: 600, + labels: { + colors: ['#c3c3c3bb', '#c3c3c3bb'], + }, + }, + colors: ['#F8D082', '#336181'], + }; + + var chart = new ApexCharts(document.querySelector("#chart"), options); + chart.render(); +} + +loadCharts(); + + diff --git a/backend/static/js/game/AIPlayer.js b/backend/static/js/game/AIPlayer.js index d356e7d..3b7d10e 100644 --- a/backend/static/js/game/AIPlayer.js +++ b/backend/static/js/game/AIPlayer.js @@ -1,6 +1,6 @@ -import { Player } from './Player.js'; +import { AbstractPlayer } from './AbstractPlayer.js'; -export class AIPlayer extends Player { +export class AIPlayer extends AbstractPlayer { constructor () { super(0, "AI Bot", null, null); } diff --git a/backend/static/js/game/AbstractGameController.js b/backend/static/js/game/AbstractGameController.js new file mode 100644 index 0000000..3886f14 --- /dev/null +++ b/backend/static/js/game/AbstractGameController.js @@ -0,0 +1,88 @@ +import * as THREE from 'three'; +import { Ball } from './Ball.js'; +import { Arena } from './Arena.js'; + +const CURR_PLAYER_ID = document.getElementById('game-engine').getAttribute('data-user-id'); + +export class AbstractGameController extends THREE.Group { + constructor ({ type }) { + super(); + + this.keybinds = null; + this.arena = null; + this.ball = null; + this.player1 = null; + this.player2 = null; + this.stats = null; + this.type = type; + } + + registerKeybinds() { + this.keybinds = { + 'w': false, 's': false, + 'ArrowUp': false, 'ArrowDown': false + }; + + document.addEventListener('keydown', (event) => { + if (event.key in this.keybinds) + this.keybinds[event.key] = true; + }); + document.addEventListener('keyup', (event) => { + if (event.key in this.keybinds) + this.keybinds[event.key] = false; + }); + } + + build({ ballDirection, onPaddleHit=null }) { + this.arena = new Arena({}); + this.ball = new Ball({ + direction: ballDirection, + onPaddleHit: onPaddleHit + }); + + this.add(this.arena); + this.add(this.player1.paddle); + this.add(this.player2.paddle); + this.add(this.ball); + + const p1display = document.getElementById('p1'); + const p2display = document.getElementById('p2'); + p1display.textContent = `${this.player1.id}-${this.player1.username}`; + p2display.textContent = `${this.player2.id}-${this.player2.username}`; + } + + update() { + this.player1.update(this.keybinds); + this.player2.update(this.keybinds); + + if (this.ball == null) + return ; + + const scorer = this.ball.move(this); + if (scorer != null) { + this.stats.registerGoal(scorer, this.ball); + this.ball.reset({}); + } + if (!this.stats.isGameOver()) + return ; + + this.cleanArena(); + + if ((this.type == "Remote" && this.stats.winner.id == CURR_PLAYER_ID) || + (this.type != "Remote" && this.stats.isGameOver())){ + console.log(this.type == "Remote" && this.stats.winner.id == CURR_PLAYER_ID); + console.log(this.type != "Remote" && this.stats.isGameOver()); + console.log(this.type, CURR_PLAYER_ID) + this.sendGameResults(); + } + } + + cleanArena() { + this.remove(this.ball); + this.ball.dispose(); + this.ball = null; + } + + createPlayers() {} + sendGameResults() {} +} \ No newline at end of file diff --git a/backend/static/js/game/AbstractPlayer.js b/backend/static/js/game/AbstractPlayer.js new file mode 100644 index 0000000..8281384 --- /dev/null +++ b/backend/static/js/game/AbstractPlayer.js @@ -0,0 +1,30 @@ +import * as THREE from 'three'; +import { ARENA_SEMI_LENGTH, PADDLE_SEMI_HEIGHT, PADDLE_SEMI_LENGTH, PADDLE_SPEED } from './macros.js'; + +export class AbstractPlayer { + constructor ({id, username, x, keybinds}) { + this.id = id + this.username = username; + this.keybinds = keybinds; + this.paddle = null; + + this.build(x); + } + + build(x) { + const height = 2 * PADDLE_SEMI_HEIGHT; + const length = 2 * PADDLE_SEMI_LENGTH; + const depth = length; + const color = x < 0 ? 0xCC0000 : 0x00FFFF; + + this.paddle = new THREE.Mesh( + new THREE.BoxGeometry(length, height, depth), + new THREE.MeshPhongMaterial({ + color: color + }) + ); + this.paddle.position.x = x; + } + + update () {} +} \ No newline at end of file diff --git a/backend/static/js/game/Ball.js b/backend/static/js/game/Ball.js index a4f35bd..570d6c2 100644 --- a/backend/static/js/game/Ball.js +++ b/backend/static/js/game/Ball.js @@ -1,17 +1,20 @@ import * as THREE from 'three'; -import { BALL_SPEED_FACTOR, BALL_START_SPEED, BALL_RADIUS, - PADDLE_SEMI_HEIGHT, PADDLE_SEMI_LENGTH } from './macros.js'; +import { BALL_SPEEDUP_FACTOR, BALL_START_SPEED, BALL_RADIUS, + PADDLE_SEMI_HEIGHT, PADDLE_SEMI_LENGTH, DIRECTION, + ARENA_SEMI_DEPTH } from './macros.js'; export class Ball extends THREE.Object3D { - constructor ({ radius, color, speed }) { + constructor ({ radius, speed, direction, onPaddleHit=null }) { super(); this.radius = radius || BALL_RADIUS; - this.speed = speed || {'x': BALL_START_SPEED.x, 'y': BALL_START_SPEED.y}; + this.speed = speed || { 'x': BALL_START_SPEED, 'y': BALL_START_SPEED }; + this.direction = direction || { 'x': DIRECTION.LEFT, 'y': DIRECTION.UP } this.rally = 0; this.ball = null; + this.onPaddleHit = onPaddleHit; this.build(); - this.reset(); + this.reset({}); } build() { @@ -23,35 +26,35 @@ export class Ball extends THREE.Object3D { this.add(this.ball); } - move(arcade) { - const { arena, player, enemy } = arcade; + move(controller) { + const { arena, player1, player2 } = controller; - this.position.x += this.speed.x; - this.position.y += this.speed.y; + this.position.x += this.direction.x * this.speed.x; + this.position.y += this.direction.y * this.speed.y; this.collideWithVerticalBounds(arena); - this.collideWithPaddle(player.paddle, true); - this.collideWithPaddle(enemy.paddle, false); - return this.collidedWithGoals(arena, player, enemy); + this.collideWithPaddle(player1.paddle, true); + this.collideWithPaddle(player2.paddle, false); + return this.collidedWithGoals(arena, player1, player2); } - collidedWithGoals(arena, player, enemy) { + collidedWithGoals(arena, player1, player2) { const { rightBoundary, leftBoundary } = arena; - if (this.position.x - this.radius <= leftBoundary.position.x) - return player; - else if (this.position.x + this.radius >= rightBoundary.position.x) - return enemy; + if (this.position.x - this.radius <= leftBoundary.position.x + ARENA_SEMI_DEPTH) + return player2; + else if (this.position.x + this.radius >= rightBoundary.position.x - ARENA_SEMI_DEPTH) + return player1; return null; } collideWithVerticalBounds(arena) { const { upperBoundary, lowerBoundary } = arena; - if (this.position.y + this.radius >= upperBoundary.position.y) - this.speed.y = -Math.abs(this.speed.y); - else if (this.position.y - this.radius <= lowerBoundary.position.y) - this.speed.y = Math.abs(this.speed.y); + if (this.position.y + this.radius >= upperBoundary.position.y - ARENA_SEMI_DEPTH) + this.direction.y = DIRECTION.DOWN; + else if (this.position.y - this.radius <= lowerBoundary.position.y + ARENA_SEMI_DEPTH) + this.direction.y = DIRECTION.UP; } collideWithPaddle(paddle, isPlayer){ @@ -87,23 +90,36 @@ export class Ball extends THREE.Object3D { //! - Change ball speed according to the speed of the paddle at the time if (isPlayer) { this.position.x = paddle.position.x + PADDLE_SEMI_LENGTH + this.radius; - this.speed.x = Math.abs(this.speed.x) + BALL_SPEED_FACTOR; + this.speed.x += BALL_SPEEDUP_FACTOR; + this.direction.x = DIRECTION.RIGHT; } else { this.position.x = paddle.position.x - PADDLE_SEMI_LENGTH - this.radius; - this.speed.x = -(Math.abs(this.speed.x) + BALL_SPEED_FACTOR); + this.speed.x += BALL_SPEEDUP_FACTOR; + this.direction.x = DIRECTION.LEFT; } + if (this.onPaddleHit != null) + this.onPaddleHit(); this.rally += 1; } - reset() { + reset({ direction }) { this.rally = 0; - this.speed.x = BALL_START_SPEED.x; - this.speed.y = BALL_START_SPEED.y; + this.speed.x = BALL_START_SPEED; + this.speed.y = BALL_START_SPEED; + if (direction) + this.direction = direction; this.position.set(0, 0, 0); } + sync({position, speed, direction}) { + this.position.set(...position); + this.speed.x = speed.x; + this.speed.y = speed.y; + this.direction = direction; + } + dispose() { this.ball.geometry.dispose(); this.ball.material.dispose(); diff --git a/backend/static/js/game/GameController.js b/backend/static/js/game/GameController.js deleted file mode 100644 index 6eb4058..0000000 --- a/backend/static/js/game/GameController.js +++ /dev/null @@ -1,92 +0,0 @@ -import * as THREE from 'three'; -import { Ball } from './Ball.js'; -import { Player } from './Player.js'; -import { Arena } from './Arena.js'; -import { GameStats } from './GameStats.js'; -import { LocalPlayer } from './LocalPlayer.js'; - - -export class GameController extends THREE.Group { - constructor(gameType) { - super(); - - this.gameType = gameType; - this.keybinds = null; - this.arena = null; - this.ball = null; - this.player = null; - this.enemy = null; - this.stats = null; - - this.init(); - this.build(); - } - - async init() { - this.arena = new Arena({}); - this.ball = new Ball({}); - this.player = new LocalPlayer(1, 'Nuno', [-25, 0, 0], {'up': 'w', 'down': 's'}); - if (this.gameType == "Local") - this.enemy = new LocalPlayer(2, 'Andreia', [25, 0, 0], {'up': 'ArrowUp', 'down': 'ArrowDown'}); - this.stats = new GameStats(this.player, this.enemy); - - this.keybinds = { - 'w': false, 's': false, - 'ArrowUp': false, 'ArrowDown': false - }; - document.addEventListener('keydown', (event) => { - if (event.key in this.keybinds) - this.keybinds[event.key] = true; - }); - document.addEventListener('keyup', (event) => { - if (event.key in this.keybinds) - this.keybinds[event.key] = false; - }); - - const formData = { - "user1_id": document.getElementById('game-engine').getAttribute('data-user-id'), - "user2_id": null, - "type": document.getElementById('game-engine').getAttribute('game-type') - } - - const response = await fetch(`/games/create`, { - method: 'POST', - body: JSON.stringify(formData), - headers: { - 'Content-Type': 'application/json', - } - }); - - const data = await response.json(); - console.log(data); - this.stats.gameId = data.id; - console.log(this.stats.gameId); - - } - - build() { - this.add(this.arena); - this.add(this.player.paddle); - this.add(this.enemy.paddle); - this.add(this.ball); - } - - update() { - this.player.update(this.keybinds, this.arena.semiHeight); - this.enemy.update(this.keybinds, this.arena.semiHeight); - if (this.ball == null) - return ; - - const scorer = this.ball.move(this); - if (scorer != null) { - this.stats.registerGoal(scorer, this.ball); - this.ball.reset(); - } - if (this.stats.winner != null) - { - this.remove(this.ball); - this.ball.dispose(); - this.ball = null; - } - } -} \ No newline at end of file diff --git a/backend/static/js/game/GameStats.js b/backend/static/js/game/GameStats.js index 10c5c98..9fb9ebf 100644 --- a/backend/static/js/game/GameStats.js +++ b/backend/static/js/game/GameStats.js @@ -3,35 +3,37 @@ /* ::: :::::::: */ /* GameStats.js :+: :+: :+: */ /* +:+ +:+ +:+ */ -/* By: crypted +#+ +:+ +#+ */ +/* By: crypto +#+ +:+ +#+ */ /* +#+#+#+#+#+ +#+ */ /* Created: 2024/09/30 18:34:16 by ncarvalh #+# #+# */ -/* Updated: 2024/10/01 21:41:00 by crypted ### ########.fr */ +/* Updated: 2024/10/21 15:08:36 by crypto ### ########.fr */ /* */ /* ************************************************************************** */ import { MAX_GOALS, TEST_GOALS } from "./macros.js"; export class GameStats { - constructor(player, enemy) { - this.player = player; - this.enemy = enemy; + constructor(player1, player2) { + this.player1 = player1; + this.player2 = player2; this.score = {}; this.goals = []; this.loser = null; this.winner = null; - this.gameId = 0; + this.gameID = 0; this.gameStats = {}; this.scoredFirst = null; - this.startTime = null; + this.startTime = null + this.gameScore = null; this.init(); } init() { this.startTime = new Date().getTime(); - this.score[this.player.username] = 0; - this.score[this.enemy.username] = 0; - + this.score[this.player1.username] = 0; + this.score[this.player2.username] = 0; + this.gameScore = document.getElementById('score'); + //! Testing // this.goals = TEST_GOALS; // this.calculateSmallStats(); @@ -44,17 +46,15 @@ export class GameStats { 'timestamp': new Date().toISOString(), 'user': scorer.id, 'rally_length': ball.rally, - 'ball_speed': Math.abs(ball.speed.x), + 'ball_speed': parseFloat(ball.speed.x.toFixed(2)), }; this.score[scorer.username] += 1; this.goals.push(goal); + console.log(this.goals); + this.gameScore.textContent = + `${this.score[this.player1.username]} : ${this.score[this.player2.username]}`; + console.log(goal); - - if (this.gameHasEnded()){ - this.calculateSmallStats(); - this.calculateAdvancedStats(); - this.sendGameResults(); - } } calculateSmallStats() { @@ -71,7 +71,7 @@ export class GameStats { } calculateAdvancedStats() { - let stats = {}, p1 = this.player.id, p2 = this.enemy.id; + let stats = {}, p1 = this.player1.id, p2 = this.player2.id; stats[p1] = { "score": 0, "canOvercome": false, "maxOvercome": 0, "overcome": 0, "maxLead": 0, "lead": 0, "maxConsecutive": 0, "consecutive": 0 }; stats[p2] = { "score": 0, "canOvercome": false, "maxOvercome": 0, "overcome": 0, @@ -107,55 +107,46 @@ export class GameStats { stats[p1].maxConsecutive = Math.max(stats[p1].consecutive, stats[p1].maxConsecutive); stats[p2].maxConsecutive = Math.max(stats[p2].consecutive, stats[p2].maxConsecutive); stats[loser].consecutive = 0; - - console.log(stats[p1], stats[p2]); } this.gameStats["greatest_deficit_overcome"] = Math.max(stats[p1].maxOvercome, stats[p2].maxOvercome); - this.gameStats["gdo_user"] = stats[p1].maxOvercome > stats[p2].maxOvercome ? p1 : p2; + this.gameStats["gdo_user"] = stats[p1].maxOvercome >= stats[p2].maxOvercome ? p1 : p2; this.gameStats["most_consecutive_goals"] = Math.max(stats[p1].maxConsecutive, stats[p2].maxConsecutive); - this.gameStats["mcg_user"] = stats[p1].maxConsecutive > stats[p2].maxConsecutive ? p1 : p2; + this.gameStats["mcg_user"] = stats[p1].maxConsecutive >= stats[p2].maxConsecutive ? p1 : p2; this.gameStats["biggest_lead"] = Math.max(stats[p1].maxLead, stats[p2].maxLead); - this.gameStats["bg_user"] = stats[p1].maxLead > stats[p2].maxLead ? p1 : p2; + this.gameStats["bg_user"] = stats[p1].maxLead >= stats[p2].maxLead ? p1 : p2; } - async sendGameResults() { + assembleGameResults(){ + this.calculateSmallStats(); + this.calculateAdvancedStats(); + const now = new Date().getTime(); - const formData = { + const results = { + "id": this.gameID, "duration": Math.round((now - this.startTime) / 1000), - "nb_goals_user1": this.score[this.player.username], - "nb_goals_user2": this.score[this.enemy.username], + "nb_goals_user1": this.score[this.player1.username], + "nb_goals_user2": this.score[this.player2.username], "game_stats": this.gameStats, "user1_stats": { - "scored_first": this.goals[0].user == this.player.id + "scored_first": this.goals[0].user == this.player1.id }, "user2_stats": { - "scored_first": this.goals[0].user == this.enemy.id + "scored_first": this.goals[0].user == this.player2.id }, - "goals": this.goals + "goals": this.goals }; - console.log(formData); - - this.winner = this.score[this.player.username] == MAX_GOALS ? this.player : this.enemy; - this.loser = this.winner == this.player ? this.enemy : this.player; - - const response = await fetch(`/games/update/${this.gameId}`, { - method: 'POST', - body: JSON.stringify(formData), - headers: { - 'Content-Type': 'application/json', - } - }); - - const responseData = await response.json(); - console.log(responseData); + console.log('GAME REPORT', results); + return results; } - gameHasEnded() { - return (Object.values(this.score).includes(MAX_GOALS)); - } + isGameOver() { + if (!Object.values(this.score).includes(MAX_GOALS)) + return false; - debug() { + this.winner = this.score[this.player1.username] == MAX_GOALS ? this.player1 : this.player2; + this.loser = this.winner == this.player1 ? this.player2 : this.player1; + return true; } } \ No newline at end of file diff --git a/backend/static/js/game/LocalGameController.js b/backend/static/js/game/LocalGameController.js new file mode 100644 index 0000000..3cbd5ec --- /dev/null +++ b/backend/static/js/game/LocalGameController.js @@ -0,0 +1,78 @@ +import { GameStats } from './GameStats.js'; +import { LocalPlayer } from './LocalPlayer.js'; +import { AbstractGameController } from './AbstractGameController.js'; +import { PADDLE_OFFSET_X, ARENA_SEMI_LENGTH, STANDARD_KEYBINDS, ALTERNATE_KEYBINDS } from './macros.js'; + +export class LocalGameController extends AbstractGameController { + constructor({ player1Data, player2Data, ballDirection }) { + super({type: "Local"}); + + this.registerKeybinds(); + this.createPlayers(player1Data, player2Data); + this.createGameInstance(); + this.build(ballDirection); + } + + createPlayers(player1Data, player2Data) { + const { id: p1ID, username: p1Username } = player1Data; + const { id: p2ID, username: p2Username } = player2Data; + + this.player1 = new LocalPlayer({ + id: p1ID, + username: p1Username, + x: -ARENA_SEMI_LENGTH + PADDLE_OFFSET_X, + keybinds: STANDARD_KEYBINDS + }); + this.player2 = new LocalPlayer({ + id: p2ID, + username: p2Username, + x: ARENA_SEMI_LENGTH - PADDLE_OFFSET_X, + keybinds: ALTERNATE_KEYBINDS + }); + + this.stats = new GameStats(this.player1, this.player2); + } + + async createGameInstance() { + const formData = { + "user1_id": this.player1.id, + "user2_id": this.player2.id, + "type": "Local" + } + + const response = await fetch(`/games/create`, { + method: 'POST', + body: JSON.stringify(formData), + headers: { + 'Content-Type': 'application/json', + } + }); + + const gameData = await response.json(); + console.log(gameData); + this.stats.gameID = gameData.id; + } + + async sendGameResults() { + const results = this.stats.assembleGameResults(); + + await fetch(`/games/update/${this.stats.gameID}`, { + method: 'POST', + body: JSON.stringify({ + 'event': 'FINISH', + 'data': results + }), + headers: { + 'Content-Type': 'application/json', + } + }); + } + + build(ballDirection) { + const ballData = { + ballDirection: ballDirection, + }; + + super.build(ballData); + } +} \ No newline at end of file diff --git a/backend/static/js/game/LocalPlayer.js b/backend/static/js/game/LocalPlayer.js index eb3db12..4ebf4e1 100644 --- a/backend/static/js/game/LocalPlayer.js +++ b/backend/static/js/game/LocalPlayer.js @@ -1,26 +1,31 @@ -import { Player } from './Player.js'; -import { PADDLE_SEMI_HEIGHT, PADDLE_SPEED } from './macros.js'; +import { AbstractPlayer } from './AbstractPlayer.js'; +import { PADDLE_SEMI_HEIGHT, PADDLE_SPEED, ARENA_SEMI_HEIGHT } from './macros.js'; -export class LocalPlayer extends Player { - constructor (id, username, position, controls) { - super(id, username, position, controls); +export class LocalPlayer extends AbstractPlayer { + constructor ({id=null, username='Local Player', x, keybinds}) { + super({ + id: id, + username: username, + x: x, + keybinds: keybinds + }); } - update(pressedKeys, arenaSemiHeight) { + update(pressedKeys) { const targetPos = this.paddle.position.clone(); - const { up: upKey, down: downKey } = this.controls; + const { up: upKey, down: downKey } = this.keybinds; if (pressedKeys[upKey]) { this.paddle.position.y = Math.min( this.paddle.position.y + PADDLE_SPEED, - arenaSemiHeight - PADDLE_SEMI_HEIGHT + ARENA_SEMI_HEIGHT - PADDLE_SEMI_HEIGHT ); } if (pressedKeys[downKey]){ this.paddle.position.y = Math.max( this.paddle.position.y - PADDLE_SPEED, - -(arenaSemiHeight - PADDLE_SEMI_HEIGHT) + -(ARENA_SEMI_HEIGHT - PADDLE_SEMI_HEIGHT) ); } this.paddle.position.lerp(targetPos, 0.5); diff --git a/backend/static/js/game/MyApp.js b/backend/static/js/game/MyApp.js index 876d556..3fa141d 100644 --- a/backend/static/js/game/MyApp.js +++ b/backend/static/js/game/MyApp.js @@ -4,8 +4,13 @@ import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; import Stats from 'three/addons/libs/stats.module.js' import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { Axis } from './Axis.js'; -import { GameController } from './GameController.js'; +import { LocalGameController } from './LocalGameController.js'; +import { REFRESH_RATE } from './macros.js'; +import { RemoteGameController } from './RemoteGameController.js'; + +var frameID; +var timeoutID; /** * This class contains the application object */ @@ -15,62 +20,73 @@ export class MyApp { */ constructor() { this.scene = null; - - // camera related attributes - this.activeCamera = null; - this.activeCameraName = null; - this.lastCameraName = null; this.cameras = []; // other attributes this.renderer = null; this.controls = null; this.gui = null; - this.arcade = null; + this.gameController = null; + this.activateControls = false; this.canvas = document.querySelector('#canvas-container'); - this.gameType = document.getElementById('game-engine').getAttribute('game-type'); } + /** * initializes the application */ - init() { + init({player1Data, player2Data, socket=null, gameType, gameID=null, ballDirection}) { this.scene = new THREE.Scene(); this.scene.background = new THREE.Color( 0x101010 ); - this.scene.add(new Axis(this)); - - this.arcade = new GameController(this.gameType); - this.scene.add(this.arcade); - - this.light = new THREE.PointLight('#FFFFFF', 100); + // this.scene.add(new Axis(this)); + + if (gameType == "Remote"){ + this.gameController = new RemoteGameController({ + player1Data: player1Data, + player2Data: player2Data, + socket: socket, + gameID: gameID, + ballDirection: ballDirection + }); + } else { + this.gameController = new LocalGameController({ + player1Data: player1Data, + player2Data: player2Data, + ballDirection: ballDirection + }); + } + this.scene.add(this.gameController); + + this.light = new THREE.PointLight('#FFFFFF', 1000); this.light.position.set(0, 0, 5); this.scene.add(this.light); this.pointLightHelper = new THREE.PointLightHelper(this.light); - this.scene.add(this.pointLightHelper); + // this.scene.add(this.pointLightHelper); this.gui = new GUI({ autoPlace: false }); this.gui.domElement.id = 'gui'; document.getElementById('main-content').appendChild(this.gui.domElement); this.stats = new Stats(); - this.stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom + this.stats.showPanel(0); document.body.appendChild(this.stats.dom); - const lightFolder = this.gui.addFolder('Light') - lightFolder.add(this.light, 'intensity', 0, 100).name("Intensity") - lightFolder.add(this.light.position, 'x', -30, 30).name("X") - lightFolder.add(this.light.position, 'y', -30, 30).name("Y") - lightFolder.add(this.light.position, 'z', -30, 30).name("Z") - lightFolder.open(); - + const lightFolder = this.gui.addFolder('Light'); + lightFolder.add(this.light, 'intensity', 0, 1000).name("Intensity"); + lightFolder.add(this.light.position, 'x', -30, 30).name("X"); + lightFolder.add(this.light.position, 'y', -30, 30).name("Y"); + lightFolder.add(this.light.position, 'z', -30, 30).name("Z"); + + const orbitFolder = this.gui.addFolder('Mouse Controls'); + orbitFolder.add(this, 'activateControls', false).name("Active") + .onChange((value) => this.setActivateControls(value)); + this.renderer = new THREE.WebGLRenderer({antialias:true}); this.renderer.setPixelRatio( this.canvas.clientWidth / this.canvas.clientHeight ); this.renderer.setClearColor("#000000"); this.renderer.setSize( this.canvas.clientWidth, this.canvas.clientHeight ); - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // search for other alternatives this.initCameras(); this.setActiveCamera('Perspective') @@ -86,7 +102,7 @@ export class MyApp { initCameras() { const aspect = this.canvas.clientWidth / this.canvas.clientHeight; - const perspective1 = new THREE.PerspectiveCamera( 75, aspect, 0.1, 1000 ) + const perspective1 = new THREE.PerspectiveCamera( 75, aspect, 0.1, 50 ) perspective1.position.set(0, 0, 35); this.cameras['Perspective'] = perspective1; } @@ -100,30 +116,27 @@ export class MyApp { this.activeCamera = this.cameras[this.activeCameraName] } - /** - * updates the active camera if required - * this function is called in the render loop - * when the active camera name changes - * it updates the active camera and the controls - */ - updateCameraIfRequired() { - - if (this.lastCameraName !== this.activeCameraName) { - this.lastCameraName = this.activeCameraName; - this.activeCamera = this.cameras[this.activeCameraName] - document.getElementById("camera").innerHTML = this.activeCameraName - - this.onResize() - - if (this.controls === null) { - this.controls = new OrbitControls( this.activeCamera, this.renderer.domElement ); - this.controls.enableZoom = true; - this.controls.update(); - } - else { - this.controls.object = this.activeCamera - } - } + setActivateControls(value) { + this.activateControls = value; + + if (this.activateControls) + this.controls = new OrbitControls( this.activeCamera, this.renderer.domElement ); + else + this.controls = null; + } + + updateOrbitControls() { + if (!this.activateControls) + return ; + + if (this.controls === null) { + this.controls = new OrbitControls( this.activeCamera, this.renderer.domElement ); + this.controls.enableZoom = true; + this.controls.update(); + } + else { + this.controls.object = this.activeCamera + } } /** @@ -141,16 +154,26 @@ export class MyApp { * the main render function. Called in a requestAnimationFrame loop */ render () { - this.stats.begin(); - this.updateCameraIfRequired() - - this.controls.update(); - this.arcade.update(); - this.renderer.render(this.scene, this.activeCamera); - - requestAnimationFrame( this.render.bind(this) ); - - this.lastCameraName = this.activeCameraName - this.stats.end(); + const updateCallback = (() => { + this.stats.begin(); + this.updateOrbitControls(); + + if (this.controls != null) + this.controls.update(); + this.gameController.update(); + this.renderer.render(this.scene, this.activeCamera); + + frameID = requestAnimationFrame( this.render.bind(this) ); + + this.lastCameraName = this.activeCameraName; + this.stats.end(); + }).bind(this); + + timeoutID = setTimeout(updateCallback, REFRESH_RATE); } -} \ No newline at end of file +} + +window.addEventListener('popstate', function(event) { + if (confirm("You're about to leave the game! Are you sure?!")) + this.window.cancelAnimationFrame(frameID); +}); \ No newline at end of file diff --git a/backend/static/js/game/Player.js b/backend/static/js/game/Player.js deleted file mode 100644 index cdc9ba2..0000000 --- a/backend/static/js/game/Player.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as THREE from 'three'; -import { PADDLE_SEMI_HEIGHT, PADDLE_SEMI_LENGTH, PADDLE_SPEED } from './macros.js'; -export class Player { - constructor (id, username, position, controls) { - this.id = id - this.username = username; - this.controls = controls; - this.paddle = null; - - this.build(); - this.paddle.position.set(...position); - } - - build() { - const height = 2 * PADDLE_SEMI_HEIGHT; - const length = 2 * PADDLE_SEMI_LENGTH; - const depth = length; - - this.paddle = new THREE.Mesh( - new THREE.BoxGeometry(length, height, depth), - new THREE.MeshNormalMaterial() - ); - } - - update(pressedKeys, arenaSemiHeight) {} -} \ No newline at end of file diff --git a/backend/static/js/game/RemoteGameController.js b/backend/static/js/game/RemoteGameController.js new file mode 100644 index 0000000..06f5f44 --- /dev/null +++ b/backend/static/js/game/RemoteGameController.js @@ -0,0 +1,111 @@ +import { GameStats } from './GameStats.js'; +import { RemotePlayer } from './RemotePlayer.js'; +import { ARENA_SEMI_LENGTH, PADDLE_OFFSET_X, STANDARD_KEYBINDS } from './macros.js'; +import { AbstractGameController } from './AbstractGameController.js'; + + + +export class RemoteGameController extends AbstractGameController { + constructor({ player1Data, player2Data, gameID, socket, ballDirection }) { + super({type: "Remote"}); + + this.players = {}; + this.socket = socket; + + this.registerKeybinds(); + this.registerSocketEvents(); + this.createPlayers(player1Data, player2Data, gameID); + this.build(ballDirection); + } + + createPlayers(player1Data, player2Data, gameID) { + const { id: p1ID, username: p1Username } = player1Data; + const { id: p2ID, username: p2Username } = player2Data; + const currPlayerID = document.getElementById('game-engine').getAttribute('data-user-id'); + const onUpdate = (id, username, targetY) => { + this.socket.send(JSON.stringify({ + 'event': 'UPDATE', + 'data': { + 'id': id, + 'username': username, + 'y': targetY, + 'ball': { + 'position': [...this.ball.position] + } + } + })); + } + + this.player1 = new RemotePlayer({ + id: p1ID, + username: p1Username, + onUpdate: p1ID == currPlayerID ? onUpdate : null, + isEnemy: p1ID != currPlayerID, + keybinds: p1ID == currPlayerID ? STANDARD_KEYBINDS : null, + x: -ARENA_SEMI_LENGTH + PADDLE_OFFSET_X + }); + this.player2 = new RemotePlayer({ + id: p2ID, + username: p2Username, + onUpdate: p2ID == currPlayerID ? onUpdate : null, + isEnemy: p2ID != currPlayerID, + keybinds: p2ID == currPlayerID ? STANDARD_KEYBINDS : null, + x: ARENA_SEMI_LENGTH - PADDLE_OFFSET_X + }); + this.players[this.player1.id] = this.player1; + this.players[this.player2.id] = this.player2; + this.stats = new GameStats(this.player1, this.player2); + this.stats.gameID = gameID; + console.log(this.stats.gameID); + } + + registerSocketEvents(){ + this.socket.onmessage = (ev) => { + const { event, data } = JSON.parse(ev.data); + + if (event == 'UPDATE') + this.players[data.id].move(data.y); + else if (event == 'SYNC') + this.ball.sync(data.ball); + else if (event == 'FINISH'){ + this.socket.close(); + } + } + + this.socket.onerror = (ev) => { + console.error(ev); + } + } + + build(ballDirection) { + const onPaddleHit = () => { + this.socket.send(JSON.stringify({ + 'event': 'SYNC', + 'data': { + 'ball': { + 'position': [...this.ball.position], + 'direction': this.ball.direction, + 'speed': this.ball.speed + } + } + })); + }; + const ballData = { + ballDirection: ballDirection, + onPaddleHit: onPaddleHit + }; + + super.build(ballData); + } + + sendGameResults() { + const results = this.stats.assembleGameResults(); + console.log(`WINNER:`, this.stats.winner, 'SCORE:', this.stats.score); + console.log('SENDING DATA TO SERVER...'); + + this.socket.send(JSON.stringify({ + 'event': 'FINISH', + 'data': results + })); + } +} \ No newline at end of file diff --git a/backend/static/js/game/RemotePlayer.js b/backend/static/js/game/RemotePlayer.js index 44c0128..2de10c9 100644 --- a/backend/static/js/game/RemotePlayer.js +++ b/backend/static/js/game/RemotePlayer.js @@ -1,11 +1,48 @@ -import { Player } from './Player.js'; +import * as THREE from 'three'; +import { AbstractPlayer } from './AbstractPlayer.js'; +import { PADDLE_SEMI_HEIGHT, ARENA_SEMI_HEIGHT, PADDLE_SPEED } from './macros.js'; -export class RemotePlayer extends Player { - constructor (id, username, position, controls) { - super(id, username, position, controls); +export class RemotePlayer extends AbstractPlayer { + constructor ({ id, username, x, keybinds=null, onUpdate }) { + super({ + id: id, + username: username, + keybinds:keybinds, + x: x + }); + this.onUpdate = onUpdate; } - update(pressedKeys, arenaSemiHeight) { + update(pressedKeys) { + //! REPOR COM KEYBINDS == NULL + if (this.keybinds == null) + return ; + const targetPos = this.paddle.position.clone(); + const { up: upKey, down: downKey } = this.keybinds; + + if (pressedKeys[upKey]) { + targetPos.y = Math.min( + this.paddle.position.y + PADDLE_SPEED, + ARENA_SEMI_HEIGHT - PADDLE_SEMI_HEIGHT + ); + } + + if (pressedKeys[downKey]){ + targetPos.y = Math.max( + this.paddle.position.y - PADDLE_SPEED, + -(ARENA_SEMI_HEIGHT - PADDLE_SEMI_HEIGHT) + ); + } + + if (pressedKeys[upKey] || pressedKeys[downKey]) + this.onUpdate(this.id, this.username, targetPos.y); + } + + move(targetY) { + const target = new THREE.Vector3(...this.paddle.position); + + target.y = targetY; + this.paddle.position.lerp(target, 0.5); } } \ No newline at end of file diff --git a/backend/static/js/game/index.html b/backend/static/js/game/index.html deleted file mode 100644 index 72e73ae..0000000 --- a/backend/static/js/game/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - -
-
-
-
- - - diff --git a/backend/static/js/game/macros.js b/backend/static/js/game/macros.js index efff222..714e506 100644 --- a/backend/static/js/game/macros.js +++ b/backend/static/js/game/macros.js @@ -1,11 +1,18 @@ -export const HELLO = "Hello"; -export const BALL_START_SPEED = {'x': -0.3, 'y': 0.3}; -export const BALL_SPEED_FACTOR = 0.02; +export const BALL_START_SPEED = 0.6; +export const BALL_SPEEDUP_FACTOR = 0.06; export const BALL_RADIUS = 1.25; +export const DIRECTION = Object.freeze({ + UP: 1, + DOWN: -1, + LEFT: -1, + RIGHT: 1, +}); + export const PADDLE_SEMI_LENGTH = 0.25; export const PADDLE_SEMI_HEIGHT = 3.75; -export const PADDLE_SPEED = 2; +export const PADDLE_SPEED = 3.5; +export const PADDLE_OFFSET_X = 7; export const ARENA_SEMI_HEIGHT = 20; export const ARENA_SEMI_LENGTH = 30; @@ -13,6 +20,20 @@ export const ARENA_SEMI_DEPTH = 0.25; export const MAX_GOALS = 5; +export const FPS = 50; +export const REFRESH_RATE = 1000 / FPS; + +export const STANDARD_KEYBINDS = { + 'up': 'w', + 'down': 's' +}; + +export const ALTERNATE_KEYBINDS = { + 'up': 'ArrowUp', + 'down': 'ArrowDown' +}; + + //! DEPENDENCY INJECTION TESTING export var TEST_GOALS = [ @@ -70,4 +91,37 @@ export var TEST_GOALS = [ 'rally_length': Math.round(Math.random() * 30), 'ball_speed': Math.random() * 10, }, +]; + +export const TEST_STATS = [ + { + day: "2024-10-18T00:00:00Z", + total_games: 5, + win_rate: 20 + }, + { + day: "2024-10-19T00:00:00Z", + total_games: 6, + win_rate: 33 + }, + { + day: "2024-10-20T00:00:00Z", + total_games: 7, + win_rate: 42 + }, + { + day: "2024-10-21T00:00:00Z", + total_games: 8, + win_rate: 50 + }, + { + day: "2024-10-22T00:00:00Z", + total_games: 16, + win_rate: 58 + }, + { + day: "2024-10-24T00:00:00Z", + total_games: 7, + win_rate: 100 + } ]; \ No newline at end of file diff --git a/backend/static/js/game/main.js b/backend/static/js/game/main.js index e56b7dd..037e7e7 100644 --- a/backend/static/js/game/main.js +++ b/backend/static/js/game/main.js @@ -1,5 +1,41 @@ import { MyApp } from './MyApp.js'; -let app = new MyApp() -app.init() -app.render() \ No newline at end of file +const gameType = document.getElementById('game-engine').getAttribute('game-type'); +const userID = document.getElementById('game-engine').getAttribute('data-user-id'); +const username = document.getElementById('game-engine').getAttribute('data-username'); + +const setupGame = (data) => { + let app = new MyApp(); + app.init(data); + app.render(); +}; + +const remoteHandler = () => { + let socket = new WebSocket(`ws://${window.location.host}/ws/games/remote/queue`); + socket.onmessage = (event) => { + const { player1, player2, ball, gameID } = JSON.parse(event.data); + setupGame({ + player1Data: player1, + player2Data: player2, + socket: socket, + gameType: gameType, + gameID: gameID, + ballDirection: ball.direction, + }); + }; +} + +const localHandler = () => { + setupGame({ + player1Data: {'id': userID, 'username': username}, + player2Data: {'id': '', 'username': 'Anonymous'}, + gameType: gameType + }); +} + +const handlers = { + 'Local': localHandler, + 'Remote': remoteHandler +}; + +handlers[gameType](); \ No newline at end of file diff --git a/backend/static/js/ongoing-tourn.js b/backend/static/js/ongoing-tourn.js index 9a60098..fb8b85e 100755 --- a/backend/static/js/ongoing-tourn.js +++ b/backend/static/js/ongoing-tourn.js @@ -17,11 +17,6 @@ socket.onmessage = (event) => { console.log('WebSocket message received:', players); - playerSlots.forEach(slot => { - slot.querySelector("span.name").textContent = "Waiting for player..."; - slot.querySelector("img").src = "../../media/default.jpg"; - }); - players.forEach((player, i) => { playerSlots[i].querySelector("span.name").textContent = player.alias playerSlots[i].querySelector("img").src = player.user.picture @@ -42,53 +37,6 @@ socket.onclose = (event) => { // ================================================================== -const players = [ - { id: 1, name: 'Player 1', score: 0, icon: 'icon1.png' }, - { id: 2, name: 'Player 2', score: 0, icon: 'icon2.png' }, - { id: 3, name: 'Player 3', score: 0, icon: 'icon3.png' }, - { id: 4, name: 'Player 4', score: 0, icon: 'icon4.png' }, -]; - -let semiFinals = { winner1: null, winner2: null }; -let finalWinner = null; - -function advancePlayer(playerId) { - const player = players.find(p => p.id === playerId); - if (!player) return; - - // Increase the player's score - player.score += 1; - - // Update the player's score in the HTML - document.getElementById(`player${playerId}`).querySelector('.score').innerText = player.score; - - // Check if the player should advance to the next round - if (player.score === 1) { - if (playerId === 1 || playerId === 2) { - semiFinals.winner1 = player; - updateRound2Player('semi1', player); - } else if (playerId === 3 || playerId === 4) { - semiFinals.winner2 = player; - updateRound2Player('semi2', player); - } - - // Update lines - document.querySelector(`#player${playerId} ~ .line`).style.backgroundColor = 'white'; - } else if (player.score === 2) { - if (playerId === semiFinals.winner1.id) { - finalWinner = semiFinals.winner1; - updateRound3Player('final', finalWinner); - } else if (playerId === semiFinals.winner2.id) { - finalWinner = semiFinals.winner2; - updateRound3Player('final', finalWinner); - } - - // Update lines - document.querySelector(`#semi1 ~ .line`).style.backgroundColor = 'white'; - document.querySelector(`#semi2 ~ .line`).style.backgroundColor = 'white'; - } -} - function updateRound2Player(elementId, player) { const element = document.getElementById(elementId); element.querySelector('.name').innerText = player.name; diff --git a/backend/static/js/profile.js b/backend/static/js/profile.js new file mode 100644 index 0000000..7d096ec --- /dev/null +++ b/backend/static/js/profile.js @@ -0,0 +1,248 @@ +import { TEST_STATS } from "./game/macros.js"; + +var modal2 = document.getElementById("modal2"); +var btn2 = document.getElementById("remove-friend-button"); +var goback = document.getElementById("cancel"); + +if (btn2) { + btn2.onclick = function() { + modal2.style.display = "block"; + } +} + +goback.onclick = function() { + modal2.style.display = "none"; +} + +window.onclick = function(event) { + if (event.target == modal2) { + modal2.style.display = "none"; + } +} + +function formatDate(timestamp) { + const date = new Date(timestamp); + + const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + const day = String(date.getUTCDate()).padStart(2, '0'); + const month = monthNames[date.getUTCMonth()]; + const year = date.getUTCFullYear(); + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + + const suffixes = ['st', 'nd', 'rd']; + const dayUnits = date.getUTCDate() % 10; + + if ((dayUnits >= 1 && dayUnits <= 3) && (date.getUTCDate() < 10 || date.getUTCDate() > 13)) + return `${day}${suffixes[dayUnits - 1]} ${month}. ${year}, ${hours}:${minutes}`; + return `${day}th ${month}. ${year}, ${hours}:${minutes}`; +} + +function formatRecordsTimestamp(divClass) { + + const recordTimeDivs = document.querySelectorAll(divClass); + + recordTimeDivs.forEach(div => { + div.textContent = formatDate(div.textContent); + }); +} + +async function loadDonutChart() { + const userID = document.getElementById('main-content').getAttribute('data-user-view-id'); + const response = await fetch(`/stats/${userID}`, { + method: "GET", + }); + const stats = await response.json(); + + const remoteTime = Math.round(stats.remote_time_played / 60); + const aiTime = Math.round(stats.ai_time_played / 60); + const localTime = Math.round(stats.local_time_played / 60); + const tournamentTime = Math.round(stats.tournament_time_played / 60); + + var options = { + chart: { + type: 'donut', + offsetX: -110, + offsetY: 10, + height: 200, + width: '100%', + }, + series: [remoteTime, aiTime, localTime, tournamentTime], + labels: ['Remote Games', 'AI Mode', 'Local Games', 'Tournaments'], + colors: ['#EC6158', '#46CDBD', '#66DD53', '#FFAD72'], + legend: { + position: 'right', + offsetY: 25, + offsetX: 100, + markers: { + width: 12, + height: 12 + }, + fontSize: '16px', + labels: { + colors: ['#fff','#fff','#fff','#fff'] + } + }, + plotOptions: { + pie: { + donut: { + size: '70%', + labels: { + show: true, + total: { + show: true, + label: 'Min', + color: '#fff', + }, + value: { + fontSize: "28px", + fontWeight: "bold" + } + } + }, + expandOnClick: false + }, + + }, + stroke: { + show: false, + + }, + dataLabels: { + enabled: false + } + }; + + var chart = new ApexCharts(document.querySelector("#chart1"), options); + chart.render(); +} + +async function loadBarLineChart() { + const userID = document.getElementById('main-content').getAttribute('data-user-view-id'); + const response = await fetch(`/graph/${userID}`, { + method: "GET", + }); + const dailyRawStats = await response.json(); + // const dailyRawStats = TEST_STATS; + + const rawWinRates = dailyRawStats.map((x) => x.win_rate); + const rawTotalGames = dailyRawStats.map((x) => x.total_games); + const winRates = new Array(7).fill(0); + const totalGames = new Array(7).fill(0); + + const today = new Date(); + var lastMonday = new Date(); + lastMonday.setDate(today.getDate() - (today.getDay() + 6) % 7); + + rawWinRates.forEach((winRate, i) => { + const timestamp = new Date(dailyRawStats[i].day); + if (timestamp.getDate() < lastMonday.getDate()) + return ; + + const weekday = (timestamp.getDay() + 6) % 7; + winRates[weekday] = winRate; + }); + rawTotalGames.forEach((numGames, i) => { + const timestamp = new Date(dailyRawStats[i].day); + if (timestamp.getDate() < lastMonday.getDate()) + return ; + + const weekday = (timestamp.getDay() + 6) % 7; + totalGames[weekday] = numGames; + }); + + var options = { + chart: { + type: 'line', + height: 350, + stacked: false, + toolbar: { + show: false + }, + width: '100%', + }, + followCursor: true, + plotOptions: { + bar: { + borderRadius: 10, + } + }, + grid: { + show: true, + borderColor: '#ffffff0C', + xaxis: { + lines: { + show: true + } + }, + yaxis: { + lines: { + show: true + } + }, + }, + series: [ + { + name: 'Games Played', + type: 'column', + data: totalGames + }, + { + name: 'Win Rate (%)', + type: 'line', + data: winRates, + stroke: { + width: 2, + }, + } + ], + xaxis: { + categories: ['Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat', 'Sun'], + labels: { + rotate: 0, + style: { + colors: '#c3c3c3bb', + fontSize: '14px', + fontWeight: 600 + } + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + colors: ['#605CFF', '#83E9FF'], + yaxis: { + show: false + }, + tooltip: { + y: { + formatter: function (val) { + return val; + } + }, + }, + dataLabels: { + enabled: false, + }, + legend: { + show: true, + horizontalAlign: 'center', + fontSize: '14px', + labels: { + colors: ['#FFFFFF', '#FFFFFF'], + }, + } + }; + + var chart = new ApexCharts(document.querySelector("#chart2"), options); + chart.render(); +} + +formatRecordsTimestamp(".record-date"); +loadDonutChart(); +loadBarLineChart(); + diff --git a/backend/static/js/tab-recent-matches.js b/backend/static/js/tab-recent-matches.js index b9781d2..44c044a 100644 --- a/backend/static/js/tab-recent-matches.js +++ b/backend/static/js/tab-recent-matches.js @@ -6,6 +6,7 @@ function onProfileClick() { document.getElementById("tab-profile").classList.add("active"); document.getElementById("tab-games").classList.remove("active"); } + function onGamesClick() { document.getElementById("games").style.display = "block"; document.getElementById("tournaments").style.display = "none"; @@ -24,3 +25,117 @@ function onTournamentsClick() { document.getElementById("tab-profile").classList.remove("active"); } + +function formatTimestamp_day(timestamp) { + const date = new Date(timestamp); + + // Array with the abbreviated month names + const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + // Get the day, month, year, hours, minutes, and seconds from the date object + const day = String(date.getUTCDate()).padStart(2, '0'); + const month = monthNames[date.getUTCMonth()]; + const year = date.getUTCFullYear(); + + const suffixes = ['st', 'nd', 'rd']; + const dayUnits = date.getUTCDate() % 10; + + if ((dayUnits >= 1 && dayUnits <= 3) && (date.getUTCDate() < 10 || date.getUTCDate() > 13)) + return `${day}${suffixes[dayUnits - 1]}, ${month}. ${year}`; + return `${day}th, ${month}. ${year}`; +} + +// Function to convert the content of the div +function formatDays(div_class) { + // Get the div element by its ID + const dateDivs = document.querySelectorAll(div_class); + + // Loop through each div and convert its content + dateDivs.forEach(div => { + const isoTimestamp = div.textContent; // Get the ISO date string from the div + const formattedDate = formatTimestamp_day(isoTimestamp); // Format the date + div.textContent = formattedDate; // Set the formatted date back into the div + }); +} + +function formatTimestamp_second(timestamp) { + const date = new Date(timestamp); + + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + const seconds = String(date.getUTCSeconds()).padStart(2, '0'); + + // Format the result as dd, mmm yyyy hh:mm:ss + return `${hours}:${minutes}:${seconds}`; +} + +// Function to convert the content of the div +function formatRecordsTimestamp(div_class) { + // Get the div element by its ID + const dateDivs = document.querySelectorAll(div_class); + + // Loop through each div and convert its content + dateDivs.forEach(div => { + const isoTimestamp = div.textContent; // Get the ISO date string from the div + const formattedDate = formatTimestamp_second(isoTimestamp); // Format the date + div.textContent = formattedDate; // Set the formatted date back into the div + }); +} + + + +function placement_func(placement) { + p = parseInt(placement); + if (p == 1) { + return `1st PLACE`; + } else if (p == 2) { + return `2nd PLACE`; + } else if (p == 3) { + return `3rd PLACE`; + } else { + return `${p}th PLACE`; + } +} + +// Converter colocações +function formatTournamentPlacements() { + const place = document.querySelectorAll(".placement"); + place.forEach(div => { + const placement = div.textContent; + if (parseInt(placement) == 1) { + div.classList.add("victory"); + } + const formatted = placement_func(placement); + div.textContent = formatted; + }); +} + +// Calcular a diferença de tempo +function calculateTimeDifference(duration) { + let totalSeconds = Math.floor(parseInt(duration)); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const formattedSeconds = String(seconds).padStart(2, '0'); + return `${minutes}:${formattedSeconds} min`; +} + +// Converter duração +function formatGameDurations() { + const places = document.querySelectorAll(".duration"); + places.forEach(div => { + const duration = div.textContent; + const formatted = calculateTimeDifference(duration); + div.textContent = formatted; + }); +} + +document.addEventListener('DOMContentLoaded', (event) => { + document.getElementById('tab-games').onclick = () => onGamesClick(); + document.getElementById('tab-tournaments').onclick = () => onTournamentsClick(); + document.getElementById('tab-profile').onclick = () => onProfileClick(); + + formatTournamentPlacements(); // Converte colocações + formatGameDurations(); // Converte durações + formatDays(".date-day"); // Converte datas + formatRecordsTimestamp(".date-second"); // Converte datas +}); diff --git a/backend/static/js/view-details-tournaments.js b/backend/static/js/view-details-tournaments.js index 5e78c8c..04f191b 100644 --- a/backend/static/js/view-details-tournaments.js +++ b/backend/static/js/view-details-tournaments.js @@ -15,7 +15,13 @@ document.addEventListener('DOMContentLoaded', () => { function detailTournamentGames(button) { const tournament_id = button.getAttribute('data-tournament-id'); const detailsDiv = document.getElementById('details-' + tournament_id); + const imgElement = button.querySelector('img'); + const buttonParentDiv = button.closest('div.details'); + const grandParentDiv = buttonParentDiv.closest('div.match-block'); // assuming match-block is the outer div class + // Toggle classes or add them as needed + grandParentDiv.classList.add("enlarged2"); + buttonParentDiv.classList.add("enlarged"); if (detailsDiv.style.display === 'none' || detailsDiv.style.display === '') { fetch(`/tournaments/${tournament_id}/games`, { headers: { @@ -30,11 +36,20 @@ function detailTournamentGames(button) { }) .then(data => { const gameList = detailsDiv; - gameList.innerHTML = ''; + gameList.innerHTML = ` +
+ + Phase + Duration + Player 1 + Score + Player 2 +
+ `; data.forEach(game => { const gameBlock = document.createElement('div'); - gameBlock.classList.add('match-container'); + gameBlock.classList.add('match-container2'); gameBlock.id = `game-${game.game.id}`; const user1Link = document.createElement('a'); @@ -77,28 +92,42 @@ function detailTournamentGames(button) { user2Link.appendChild(user2ProfilePic); user2Link.appendChild(user2Name); - + if ( game.game.nb_goals_user1 > game.game.nb_goals_user2) { + user1Name.classList.add('tour-game-winner'); + } else { + user2Name.classList.add('tour-game-winner'); + } + + const gameDetailLink = document.createElement('a'); + gameDetailLink.href = `/games/${game.game.id}/stats`; + gameDetailLink.classList.add('game-link'); gameBlock.innerHTML = ` -
+
- ${game.phase} - ${game.game.duration} - ${user1Link.outerHTML} - ${game.game.nb_goals_user1} - ${game.game.nb_goals_user2} - ${user2Link.outerHTML} + ${game.phase} + ${game.game.duration} + ${user1Link.outerHTML} + ${game.game.nb_goals_user1} - ${game.game.nb_goals_user2} + ${user2Link.outerHTML}
`; - gameList.appendChild(gameBlock); + gameDetailLink.appendChild(gameBlock); + + gameList.appendChild(gameDetailLink); }); - detailsDiv.style.display = 'block'; + detailsDiv.style.display = 'flex'; + imgElement.src = "/static/assets/icons/return.png"; }) .catch(error => { console.error('Erro ao buscar jogos:', error); }); } else { - detailsDiv.style.display = 'none'; + detailsDiv.style.display = 'none'; + imgElement.src = "/static/assets/icons/Collapse-Arrow.png"; + grandParentDiv.classList.toggle("enlarged2"); + buttonParentDiv.classList.toggle("enlarged"); } } diff --git a/docker-compose.yml b/docker-compose.yml index 4b03a59..cf0f823 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,13 @@ services: networks: - Transcendence depends_on: - - postgres - - redis + postgres: + condition: service_healthy + redis: + condition: service_started env_file: - .env + restart: on-failure postgres: container_name: postgres-container @@ -28,9 +31,14 @@ services: ports: - "5432:5432" networks: - - Transcendence + - Transcendence env_file: - .env + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 5s + retries: 5 pgadmin4: container_name: pgadmin4-container