Skip to content

Commit

Permalink
feat/game-invite (#137)
Browse files Browse the repository at this point in the history
* feat: activate loggers on game

* feat: component to invite user to a game

* feat: game invite prototype

* ref: inviteError event name change

* Feature: Implement chat DMs between friends (#128)

* feat: implement chat DMs between friends

- Remove finished todos
- Add some alt text to components
- Private chats don't show in the main chat lobby anymore
- Only private chats created by the user are temporarily displayed in the chat lobby
- If a user resets its chat component, all of the DM chats are removed
- All chat DMs are database persisted

* lint: remove unused imports

* fix: filter private chat to no appear on list chats

* style: resize name and hide users popover on dm

* style: fixed chat width

* style: add cursor pointer into dm icon

---------

Co-authored-by: Italo A <[email protected]>

* feat/127-add-alt (#132)

* feat: add action hover tip on each button

* fix: use a single alt attribute

---------

Co-authored-by: Victor Wildner <[email protected]>

* Fix/125-chat-reload (#133)

* style: cards justify

* fix: chat reloading when access history page

* feat/game-actions (#134)

* feat: better game actions and titles

* feat: show player name on play screen

* feat: unmute user automatically after 5s (#136)

* feat/owner-chat-actions (#135)

* feat: allow owner update and remove password from chat

* fix: wrong html tree

* feat: update chat listing on render list

* feat: on chat open goes to the bottom

* refactor: remove console log

* feat: update lock icon

* fix: avoid read undefined

* fix: missing provider and duplicated toaster

---------

Co-authored-by: Victor Wildner <[email protected]>

* Feat/profile popover (#138)

* refactor: remove not user isFriend property

* feat: add buttons and remove bio description

---------

Co-authored-by: Victor Wildner <[email protected]>

* Feat/descriptive top buttons (#139)

* feat: add missing back home button

* feat: more descriptive menu actions

* Fixes 2fa (#130)

* Splits auth-only updates from user update service
- Removes mfaEnabled from user field interceptor
- Removes mfaEnabled from PatchUserDto
- Removes secrets from the frontend user model

* fix: Removes duplicate Toaster
- chore: cleanup in user dto

* Fixes CORS on 2fa code submission
- Fixes redirect not pushing to auth/2fa

* Feat/front block user (#144)

* feat: add block button

* feat: replicate button pattern

* fix: avatar wrong initial letter

* feat: implement block user (#148)

* Fix: Cleaning abandoned lobby rooms (#142)

* feat: activate loggers on game

* feat: component to invite user to a game

* feat: make game a global context

* refactor: game invite to game

* style: lint file

* feat: implement invite player to game

* fix: returning game copy instead game reference

* ref: cleaning invite lobby in disconnect

* fix:game invite

* feat: modal to handle invite actions

* refactor: remove not used imports

* feat: add guestRejected emit socket

* refactor: update match history create to update with login

* refactor: remove not used player user id

* feat: add invite rejected actions and alerts

* fix: inviting offline player

* ref: class attributes

* feat: stopGame event handler

* fix: event call stopGame on gameFinished handler

* feat: modal in game not show

* fix: breaking line names

* refactor: remove not used template

* style: responsive elements

* fix: client not leaving socket room

* fix: abandoned game still running

* feat: player that abandone game looses

* ref: removing useless code

* ref: removing useless code

---------

Co-authored-by: Guilherme Martinelli <[email protected]>
Co-authored-by: Victor Wildner <[email protected]>
Co-authored-by: Juliano Choi <[email protected]>
Co-authored-by: Guilherme Ferreira Martinelli <[email protected]>
  • Loading branch information
5 people authored Dec 5, 2023
1 parent e3c012c commit 3a1257f
Show file tree
Hide file tree
Showing 25 changed files with 652 additions and 96 deletions.
3 changes: 3 additions & 0 deletions backend/src/friends/friends.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ export class FriendsService {
}

async blockUser(userId: string, createFriendDto: CreateFriendDto) {

const blocked = await this.prisma.user.findUnique({
where: { id: userId },
select: { blocked: { where: { id: createFriendDto.friend_id } } },
});


if (blocked.blocked.length > 0) {
throw new NotAcceptableException('User already blocked');
}
Expand All @@ -116,6 +118,7 @@ export class FriendsService {
where: { id: createFriendDto.friend_id },
data: { blocked: { connect: [{ id: userId }] } },
});

// check if user is friend
const friendship = await this.prisma.user.findUnique({
where: { id: userId },
Expand Down
4 changes: 4 additions & 0 deletions backend/src/game/dto/game.invite.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class GameInviteDto {
inviting: string; //Socket ID player 1
guest: string; //Login player 2
}
1 change: 0 additions & 1 deletion backend/src/game/dto/game.player.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export class Player {
login: string;
socketId: string;
userId: string;
x: number;
y: number;
width: number;
Expand Down
98 changes: 86 additions & 12 deletions backend/src/game/game.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ import { GameService } from './game.service';
import { GameDto } from './dto/game.dto';
import { GameLobbyService } from './lobby/game.lobby.service';
import { GameMoveDto } from './dto/game.move';
import { GameInviteDto } from './dto/game.invite.dto';
import { JwtService } from '@nestjs/jwt';
import { Logger } from '@nestjs/common';

@WebSocketGateway({ namespace: '/game' })
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {

@WebSocketServer()
gameServer: Server;
private PADDLE_WIDTH = 10;
private PADDLE_HEIGHT = 150;
private FRAMES_PER_SECOND = 60;
private readonly logger = new Logger(GameGateway.name);
private pool = new Map<string, Socket>();
private gamesPlaying: Map<string, GameDto> = new Map();

constructor(
private gameService: GameService,
Expand All @@ -30,26 +38,31 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
this.gameLobby.PADDLE_WIDTH = this.PADDLE_WIDTH;
}

@WebSocketServer()
gameServer: Server;
private gamesPlaying: Map<string, GameDto> = new Map();

handleConnection(client: Socket) {
// Get user from cookie coming from client
const user = client.handshake.auth.token;

// Decode user from JWT
const decodedUser = this.jwtService.decode(user).sub;

console.log(`Client ${decodedUser} connected`);
this.pool.set(decodedUser, client);

this.logger.log(`Client ${decodedUser} connected`);
}

handleDisconnect(client: Socket) {
// Get user from cookie coming from client
const user = client.handshake.auth.token;

// Decode user from JWT
const decodedUser = this.jwtService.decode(user).sub;

const gameId = this.finishGame(client);
client.leave(gameId);
this.gameLobby.abandoneLobby(client.id);
this.gameServer.to(gameId).emit('gameAbandoned', this.gamesPlaying[gameId]);
console.log(`Client ${client.id} disconnected`);
this.pool.delete(decodedUser);
this.logger.log(`Client ${client.id} disconnected`);
}

@SubscribeMessage('joinGame')
Expand All @@ -58,7 +71,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
const decodedUser = this.jwtService.decode(user).sub;

if (this.gameLobby.joinPlayer1(client, decodedUser)) {
console.log(`waiting Player 2`);
this.logger.log(`Client ${client.id} joined game`);
client.emit('waitingPlayer2', `game_${client.id}`);
} else {
const game = this.gameLobby.joinPlayer2(client, decodedUser);
Expand All @@ -69,6 +82,56 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
}
}

@SubscribeMessage('createInvite')
inviteGame(client: Socket, info: GameInviteDto) {
const user = client.handshake.auth.token;
const decodedUser = this.jwtService.decode(user).sub;

this.logger.debug(
`Client ${client.id} created a invite game for ${info.guest}`,
);

if (
this.gameService.checkGuestAvailability(
info.guest,
this.pool,
)
) {
this.gameLobby.invitePlayer1(client, decodedUser);
this.logger.log(`Client ${client.id} created a invite game`);
info.inviting = client.id;
const guest = this.pool.get(info.guest);
guest.emit('invited', info);
} else {
client.emit('guestRejected', `Convidado não disponível`);
}
}

@SubscribeMessage('inviteAccepted')
inviteAccepted(client: Socket, info: GameInviteDto) {
const user = client.handshake.auth.token;
const decodedUser = this.jwtService.decode(user).sub;

const game = this.gameLobby.invitePlayer2(client, decodedUser, info);

if (game == undefined) {
client.emit('inviteError', `Jogo não encontrado`);
return;
}

this.gamesPlaying[game.gameId] = game;
this.gameService.restartBall(this.gamesPlaying[game.gameId]);
this.gameServer.to(game.gameId).emit('gameCreated', game.gameId);
this.startGame(client, game.gameId);
}

@SubscribeMessage('inviteRejected')
inviteRejected(client: Socket, info: GameInviteDto) {
this.gameLobby.inviteRejected(info);
client.leave(`game_${info.inviting}`);
this.gameServer.to(`game_${info.inviting}`).emit('guestRejected');
}

@SubscribeMessage('startGame')
startGame(client: Socket, gameId: string) {
const game = this.gamesPlaying[gameId];
Expand All @@ -78,10 +141,11 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
this.gameService.addPoint(game);
this.gameService.restartBall(game);
}

if (this.gameService.isGameFinished(game)) {
this.gameServer.to(gameId).emit('gameFinished', game);
this.finishGame(client);
}

if (game.finished) {
return;
}
Expand All @@ -94,6 +158,11 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
}, 1000 / this.FRAMES_PER_SECOND);
}

@SubscribeMessage('stopGame')
stopGame(client: Socket) {
this.finishGame(client);
}

@SubscribeMessage('movePlayer')
movePlayer(_: Socket, info: GameMoveDto) {
this.gameService.updatePlayerPosition(this.gamesPlaying[info.gameId], info);
Expand All @@ -102,13 +171,18 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
private finishGame(client: Socket): string {
const gameId = Object.keys(this.gamesPlaying).find((gameId) => {
return (
this.gamesPlaying[gameId].player1.id === client.id ||
this.gamesPlaying[gameId].player2.id === client.id
this.gamesPlaying[gameId].player1.socketId === client.id ||
this.gamesPlaying[gameId].player2.socketId === client.id
);
});
if (gameId) {
this.gamesPlaying[gameId].finished = true;
this.gamesPlaying.delete(gameId);
if (this.gamesPlaying[gameId].finished == false)
this.gameService.setWinner(this.gamesPlaying[gameId], client.id);
this.gameServer.to(gameId).emit('gameFinished', this.gamesPlaying[gameId]);
client.leave(gameId);
setTimeout(() => {
this.gamesPlaying.delete(gameId);
}, 1000);
return gameId;
}
return null;
Expand Down
31 changes: 27 additions & 4 deletions backend/src/game/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Player } from './dto/game.player.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import { User } from '@prisma/client';
import { MatchHistoryService } from 'src/match-history/match-history.service';
import { Socket } from 'socket.io';

@Injectable()
export class GameService {
Expand All @@ -17,8 +18,8 @@ export class GameService {

public PADDLE_WIDTH: number;
public PADDLE_HEIGHT: number;
private MAX_SCORE = 2;
private BALL_SPEED = 6;
private MAX_SCORE = 5;
private BALL_SPEED = 5;
private BALL_ACCELERATION = 1.1;

updateBallPosition(gameDto: GameDto) {
Expand Down Expand Up @@ -174,10 +175,11 @@ export class GameService {
winnerPoints = gameDto.score.player2;
loserPoints = gameDto.score.player1;
}

try {
await this.matchHistoryService.create({
winnerId: winner.userId,
loserId: loser.userId,
winnerLogin: winner.login,
loserLogin: loser.login,
winnerPoints,
loserPoints,
});
Expand All @@ -186,6 +188,27 @@ export class GameService {
}
}

checkGuestAvailability(
player: string,
pool: Map<string, Socket>,
): boolean {
if (pool.get(player) == undefined)
return false;
return true;
}

setWinner(gameDto: GameDto, socketId: string) {
if (gameDto.player1.socketId == socketId) {
gameDto.score.player1 = 0;
gameDto.score.player2 = 5;
gameDto.finished = true;
} else {
gameDto.score.player2 = 0;
gameDto.score.player1 = 5;
gameDto.finished = true;
}
}

private async storeGameResult(gameDto: GameDto) {
await this.updatePlayerVictory(gameDto);

Expand Down
53 changes: 47 additions & 6 deletions backend/src/game/lobby/game.lobby.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { GameDto } from '../dto/game.dto';
import { GameInviteDto } from '../dto/game.invite.dto';

@Injectable()
export class GameLobbyService {
public PADDLE_WIDTH: number;
public PADDLE_HEIGHT: number;
private lobby: GameDto[] = [];
private invite_lobby = new Map<string, GameDto>();
private PLAYER_INITIAL_X = 0;
private CANVAS_WIDTH = 858;
private CANVAS_HEIGHT = 525;
private readonly logger = new Logger(GameLobbyService.name);

joinPlayer1(player: any, login: string): boolean {
if (this.lobby.length == 0) {
const gameDto = this.initGame(player.id);
this.lobby.push(gameDto);
gameDto.player1.login = login;
console.log('player 1 joined');
this.lobby.push(gameDto);
this.logger.log(`Client player 1 joined`);
player.join(`game_${gameDto.player1.socketId}`);
return true;
} else {
Expand All @@ -28,26 +31,60 @@ export class GameLobbyService {
gameDto.player2 = {
login,
socketId: player.id,
userId: '',
x: gameDto.canvas.width - this.PLAYER_INITIAL_X - this.PADDLE_WIDTH,
y: this.CANVAS_HEIGHT / 2 - this.PADDLE_HEIGHT / 2,
width: this.PADDLE_WIDTH,
height: this.PADDLE_HEIGHT,
};
player.join(`game_${gameDto.player1.socketId}`);
console.log('Player 2 joined');
this.logger.log(`Client player 2 joined`);
this.lobby.splice(0, 1);
return gameDto;
}

invitePlayer1(player: any, login: string) {
const gameDto = this.initGame(player.id);
gameDto.player1.login = login;
this.invite_lobby.set(`game_${player.id}`, gameDto);
this.logger.log(`Client player 1 joined invite game`);
player.join(`game_${gameDto.player1.socketId}`);
}

invitePlayer2(player: any, login: string, info: GameInviteDto) {
if (this.invite_lobby.get(`game_${info.inviting}`) == undefined) {
return;
}

const gameDto = Object.assign(
this.invite_lobby.get(`game_${info.inviting}`),
);

gameDto.player2 = {
login,
socketId: player.id,
x: gameDto.canvas.width - this.PLAYER_INITIAL_X - this.PADDLE_WIDTH,
y: this.CANVAS_HEIGHT / 2 - this.PADDLE_HEIGHT / 2,
width: this.PADDLE_WIDTH,
height: this.PADDLE_HEIGHT,
};
player.join(`game_${info.inviting}`);
this.logger.log(`Client player 2 joined invited game`);
this.invite_lobby.delete(`game_${info.inviting}`);
return gameDto;
}

inviteRejected(info: GameInviteDto) {
this.logger.log(`Client player 2 rejected invited game`);
this.invite_lobby.delete(`game_${info.inviting}`);
}

initGame(player1Id: string): GameDto {
const gameDto: GameDto = {
gameId: `game_${player1Id}`,
finished: false,
player1: {
login: '',
socketId: player1Id,
userId: '',
x: this.PLAYER_INITIAL_X,
y: this.CANVAS_HEIGHT / 2 - this.PADDLE_HEIGHT / 2,
width: this.PADDLE_WIDTH,
Expand Down Expand Up @@ -78,5 +115,9 @@ export class GameLobbyService {
(item) => item.gameId == `game_${playerId}`,
);
if (index >= 0) this.lobby.splice(index);

if (this.invite_lobby.get(`game_${playerId}`) != undefined)
this.invite_lobby.delete(`game_${playerId}`);

}
}
4 changes: 2 additions & 2 deletions backend/src/match-history/dto/create-match-history.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { IsInt, IsNotEmpty, IsString } from 'class-validator';
export class CreateMatchHistoryDto {
@IsNotEmpty()
@IsString()
winnerId: string;
winnerLogin: string;

@IsNotEmpty()
@IsString()
loserId: string;
loserLogin: string;

@IsNotEmpty()
@IsInt()
Expand Down
Loading

0 comments on commit 3a1257f

Please sign in to comment.