merge master

This commit is contained in:
simplonco
2022-12-28 20:29:43 +01:00
134 changed files with 8629 additions and 2776 deletions

View File

@@ -1,40 +1,24 @@
DOCKERCOMPOSEPATH=./srcs/docker-compose.yml
#dev allow hot reload.
dev:
up:
@bash ./make_env.sh
docker compose -f ${DOCKERCOMPOSEPATH} up -d --build
@make start
@docker ps
#prod only the needed files ares presents inside the container
prod:
docker compose -f ${DOCKERCOMPOSEPATH} up -d --build prod
@make start_prod
@docker ps
start:
docker compose -f ${DOCKERCOMPOSEPATH} start
docker logs --follow srcs-backend_dev-1
docker logs --follow nestjs
start_dev:
docker compose -f ${DOCKERCOMPOSEPATH} start dev
docker logs --follow srcs-backend_dev-1
all : up
start_prod:
docker compose -f ${DOCKERCOMPOSEPATH} start prod
restart:stop
@make up
re: down up
down:
docker compose -f ${DOCKERCOMPOSEPATH} -v down
destroy:
# rm -rf ./srcs/requirements/nestjs/api_back/node_modules/
# rm -rf ./srcs/requirements/nestjs/api_back/dist
# rm -rf ./srcs/requirements/svelte/api_front/node_modules/
# rm -rf ./srcs/requirements/svelte/api_front/public/build
docker compose -f ${DOCKERCOMPOSEPATH} down -v --rmi all --remove-orphans
docker ps -aq | xargs --no-run-if-empty docker rm -f
docker images -aq | xargs --no-run-if-empty docker rmi -f

View File

@@ -15,14 +15,14 @@
- [x] Utilisateur : faire la base pour un utilisateur
- [x] Utilisateur : faire le système de requêtes amis
- [ ] Utilisateur : mettre en place le système de session (voire de statut ?)
- [ ] Utilisateur : mettre en place le système d'avatar
- [ ] Utilisateur : mettre en place la double authentification
- [ ] Utilisateur : mettre en place le système d'Oauth
- [ ] Utilisateur : mettre en place la hashage de mot de passe (avec Oauth)
- [ ] Utilisateur : mettre en place le système de statut
- [ ] Utilisateur : mettre en place le système de stats
- [ ] Utilisateur : mettre en place l'historique des matches
- [x] Utilisateur : mettre en place le système de session (voire de statut ?)
- [x] Utilisateur : mettre en place le système d'avatar
- [x] Utilisateur : mettre en place la double authentification
- [x] Utilisateur : mettre en place le système d'Oauth
- [x] Utilisateur : mettre en place la hashage de mot de passe (avec Oauth)
- [x] Utilisateur : mettre en place le système de statut
- [x] Utilisateur : mettre en place le système de stats
- [x] Utilisateur : mettre en place l'historique des matches
### TODO List : Docker édition.

86
make_env.sh Normal file
View File

@@ -0,0 +1,86 @@
#! /usr/bin/env bash
# This script is used to create a new environment for the project.
# Create a new environment for docker
ENV_FILE_DOCKER=./srcs/.env
ENV_FILE_NESTJS=./srcs/requirements/nestjs/api_back/.env
# Create a new environment for docker
if [ -f "$ENV_FILE_DOCKER" ] && [ -f "$ENV_FILE_NESTJS" ]; then
echo "The file $ENV_FILE_DOCKER and $ENV_FILE_NESTJS already exists. Do you want to overwrite them ? (y/n)"
read -p "Enter your choice : " OVERWRITE
if [ "$OVERWRITE" = "y" ]; then
rm "$ENV_FILE_DOCKER" && rm "$ENV_FILE_NESTJS"
else
echo "The file $ENV_FILE_DOCKER and $ENV_FILE_NESTJS will not be overwritten. The script will exit."
exit 0
fi
fi
echo "Creating a new environment for docker"
read -p "Enter the env configuration for nestjs : \"1\" for development OR \"2\" for production : " NODE_ENV
if [ "$NODE_ENV" = "1" ]; then
echo "NODE_ENV=development" > "$ENV_FILE_DOCKER"
elif [ "$NODE_ENV" = "2" ]; then
echo "NODE_ENV=production" > "$ENV_FILE_DOCKER"
else
echo "You entered a wrong value. The default value will be used (development)."
echo "NODE_ENV=development" > "$ENV_FILE_DOCKER"
fi
read -p "Enter the name of the host like \"localhost\" : " PROJECT_HOST
echo "WEBSITE_HOST=$PROJECT_HOST" >> "$ENV_FILE_DOCKER"
echo "WEBSITE_PORT=8080" >> "$ENV_FILE_DOCKER"
echo "POSTGRES_USER=postgres" >> "$ENV_FILE_DOCKER"
echo "POSTGRES_PASSWORD=$(openssl rand -base64 32)" >> "$ENV_FILE_DOCKER"
echo "POSTGRES_DB=transcendance_db" >> "$ENV_FILE_DOCKER"
echo "POSTGRES_HOST=postgresql" >> "$ENV_FILE_DOCKER"
echo "POSTGRES_PORT=5432" >> "$ENV_FILE_DOCKER"
echo "REDIS_HOST=redis" >> "$ENV_FILE_DOCKER"
echo "REDIS_PORT=6379" >> "$ENV_FILE_DOCKER"
echo "REDIS_PASSWORD=$(openssl rand -base64 32)" >> "$ENV_FILE_DOCKER"
# Create a new environment for nestjs
echo "Creating a new environment for nestjs"
if [ "$NODE_ENV" = "1" ]; then
echo "NODE_ENV=development" > "$ENV_FILE_NESTJS"
elif [ "$NODE_ENV" = "2" ]; then
echo "NODE_ENV=production" > "$ENV_FILE_NESTJS"
else
echo "NODE_ENV=development" > "$ENV_FILE_NESTJS"
fi
echo "WEBSITE_HOST=$PROJECT_HOST" >> "$ENV_FILE_NESTJS"
echo "WEBSITE_PORT=8080" >> "$ENV_FILE_NESTJS"
echo "POSTGRES_USER=postgres" >> "$ENV_FILE_NESTJS"
echo "POSTGRES_PASSWORD=$POSTGRES_PASSSWORD" >> "$ENV_FILE_NESTJS"
echo "POSTGRES_DB=transcendance_db" >> "$ENV_FILE_NESTJS"
echo "POSTGRES_HOST=postgresql" >> "$ENV_FILE_NESTJS"
echo "POSTGRES_PORT=5432" >> "$ENV_FILE_NESTJS"
echo "In the next steps, we'll need to enter the client secret and client id of the 42 api"
read -p "Enter the client id of the 42 api : " CLIENT_ID
echo "FORTYTWO_CLIENT_ID=$CLIENT_ID" >> "$ENV_FILE_NESTJS"
read -p "Enter the client secret of the 42 api : " CLIENT_SECRET
echo "FORTYTWO_CLIENT_SECRET=$CLIENT_SECRET" >> "$ENV_FILE_NESTJS"
echo "FORTYTWO_CALLBACK_URL=http://$PROJECT_HOST:8080/api/v2/auth/redirect" >> "$ENV_FILE_NESTJS"
echo "COOKIE_SECRET=$(openssl rand -base64 32)" >> "$ENV_FILE_NESTJS"
echo "PORT=3000" >> "$ENV_FILE_NESTJS"
echo "REDIS_HOST=redis" >> "$ENV_FILE_DOCKER"
echo "REDIS_PORT=6379" >> "$ENV_FILE_DOCKER"
echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> "$ENV_FILE_DOCKER"
echo "TWO_FACTOR_AUTHENTICATION_APP_NAME=Transcendance" >> "$ENV_FILE_NESTJS"
echo "TICKET_FOR_PLAYING_GAME_SECRET=$(openssl rand -base64 32)" >> "$ENV_FILE_NESTJS"
echo "The environment has been created successfully. You can now wait for the docker to build the project."

View File

@@ -1,9 +1,13 @@
NODE_ENV=development
WEBSITE_HOST=transcendance
POSTGRES_USER=postgres
POSTGRES_PASSWORD=9pKpKEgiamxwk5P7Ggsz
POSTGRES_PASSWORD=unPCFsMxZwwCguGFMTmKgCySt6o5uX76QsKyabKS89I=
POSTGRES_DB=transcendance_db
POSTGRES_HOST=postgresql
POSTGRES_PORT=5432
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=1a5e04138b91b3d683c708e4689454c2
REDIS_PASSWORD=yiCFcBSrFv7DXVBmydtwL9unzNA2MjbB70XspflHHPc=
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=

View File

@@ -1,5 +1,6 @@
services:
backend_dev:
container_name: nestjs
build:
context: ./requirements/nestjs
target: development
@@ -7,6 +8,7 @@ services:
volumes:
- ./requirements/nestjs/api_back/src:/usr/app/src
- ./requirements/nestjs/api_back/test:/usr/app/test/
- nestjs_photos_volume:/usr/app/src/uploads/avatars
env_file:
- .env
environment:
@@ -16,7 +18,21 @@ services:
- postgresql
- redis
game_server:
container_name: game_server
build:
context: ./requirements/game_server
dockerfile: Dockerfile
environment:
NODE_ENV: "${NODE_ENV}"
restart: unless-stopped
ports:
- "8042:8042"
depends_on:
- backend_dev
frontend_dev:
container_name: svelte
build:
context: ./requirements/svelte
target: development
@@ -38,6 +54,7 @@ services:
# t'embete pas a gerer ton propre container nginx
nginx:
container_name: nginx
image: nginx:alpine
restart: unless-stopped
volumes:
@@ -53,7 +70,7 @@ services:
- redis
postgresql:
container_name: nestjs_postgresql
container_name: postgresql
image: postgres
volumes:
- data_nest_postgresql:/var/lib/postgresql/data
@@ -67,10 +84,8 @@ services:
# Je connais pas redis, mais si t'en a besoin que a l'interieur de tes containers, je pense pas que t'as besoin d'un expose.
redis:
container_name: nestjs_redis
container_name: redis
image: redis:alpine
expose:
- "6379"
restart: unless-stopped
environment:
REDIS_HOST: "${REDIS_HOST}"
@@ -78,3 +93,4 @@ services:
volumes:
data_nest_postgresql:
nestjs_photos_volume:

View File

@@ -0,0 +1,15 @@
FROM node:alpine AS build
WORKDIR /usr/app
COPY ./game_back ./
RUN npm install typescript
RUN npx tsc
WORKDIR /usr/app/src/server
EXPOSE 8042
CMD [ "node", "wsServer.js"]

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@@ -0,0 +1,121 @@
{
"name": "game_back",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
},
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.9.4"
}
},
"node_modules/@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"requires": {}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"type": "module",
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.9.4"
},
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
}
}

View File

@@ -0,0 +1,36 @@
import { WebSocket } from "../wsServer.js";
import { Racket } from "../../shared_js/class/Rectangle.js";
import { GameSession } from "./GameSession.js";
import * as ev from "../../shared_js/class/Event.js"
import * as en from "../../shared_js/enums.js"
export class Client {
socket: WebSocket;
id: string; // same as "socket.id"
isAlive: boolean = true;
gameSession: GameSession = null;
constructor(socket: WebSocket, id: string) {
this.socket = socket;
this.id = id;
}
}
export class ClientPlayer extends Client {
token: string;
username: string;
matchOptions: en.MatchOptions = 0;
inputBuffer: ev.EventInput = new ev.EventInput();
lastInputId: number = 0;
racket: Racket;
constructor(socket: WebSocket, id: string, racket: Racket) {
super(socket, id);
this.racket = racket;
}
}
export class ClientSpectator extends Client {
constructor(socket: WebSocket, id: string) {
super(socket, id);
}
}

View File

@@ -0,0 +1,12 @@
import * as en from "../../shared_js/enums.js"
import { GameComponents } from "../../shared_js/class/GameComponents.js";
export class GameComponentsServer extends GameComponents {
scoreLeft: number = 0;
scoreRight: number = 0;
constructor(options: en.MatchOptions)
{
super(options);
}
}

View File

@@ -0,0 +1,267 @@
import * as en from "../../shared_js/enums.js"
import * as ev from "../../shared_js/class/Event.js"
import * as c from "../constants.js"
import { ClientPlayer, ClientSpectator } from "./Client";
import { GameComponentsServer } from "./GameComponentsServer.js";
import { clientInputListener, clientTerminate } from "../wsServer.js";
import { random } from "../utils.js";
import { Ball } from "../../shared_js/class/Rectangle.js";
import { wallsMovements } from "../../shared_js/wallsMovement.js";
/*
multiples methods of GameSession have parameter "s: GameSession".
its used with calls to setTimeout(),
because "this" is not equal to the GameSession but to "this: Timeout"
*/
export class GameSession {
id: string; // url ?
playersMap: Map<string, ClientPlayer> = new Map();
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
spectatorsMap: Map<string, ClientSpectator> = new Map();
gameLoopInterval: NodeJS.Timer | number = 0;
playersUpdateInterval: NodeJS.Timer | number = 0;
spectatorsUpdateInterval: NodeJS.Timer | number = 0;
components: GameComponentsServer;
matchOptions: en.MatchOptions;
isPrivateMatch: boolean; // WIP: could be used to separate leaderboards for example.
matchEnded: boolean = false;
lastStateSnapshot: ev.EventGameUpdate;
actual_time: number;
last_time: number;
delta_time: number;
constructor(id: string, matchOptions: en.MatchOptions, isPrivateMatch: boolean = false) {
this.id = id;
this.matchOptions = matchOptions;
this.isPrivateMatch = isPrivateMatch;
this.components = new GameComponentsServer(this.matchOptions);
}
start() {
const gc = this.components;
setTimeout(this.resume, c.matchStartDelay, this);
let timeout = c.matchStartDelay + c.newRoundDelay;
gc.ballsArr.forEach((ball) => {
setTimeout(this._newRound, timeout, this, ball);
timeout += c.newRoundDelay*0.5;
});
}
resume(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.on("message", clientInputListener);
});
s.actual_time = Date.now();
s.lastStateSnapshot = s._gameStateSnapshot();
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
s.playersUpdateInterval = setInterval(s._playersUpdate, c.playersUpdateIntervalMS, s);
s.spectatorsUpdateInterval = setInterval(s._spectatorsUpdate, c.spectatorsUpdateIntervalMS, s);
}
pause(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.off("message", clientInputListener);
});
clearInterval(s.gameLoopInterval);
clearInterval(s.playersUpdateInterval);
clearInterval(s.spectatorsUpdateInterval);
}
instantInputDebug(client: ClientPlayer) {
this._handleInput(c.fixedDeltaTime, client);
}
private _handleInput(delta: number, client: ClientPlayer) {
// if (client.inputBuffer === null) {return;}
const gc = this.components;
const input = client.inputBuffer.input;
if (input === en.InputEnum.up) {
client.racket.dir.y = -1;
}
else if (input === en.InputEnum.down) {
client.racket.dir.y = 1;
}
if (input !== en.InputEnum.noInput) {
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
}
client.lastInputId = client.inputBuffer.id;
// client.inputBuffer = null;
}
private _gameLoop(s: GameSession) {
/* s.last_time = s.actual_time;
s.actual_time = Date.now();
s.delta_time = (s.actual_time - s.last_time) / 1000; */
s.delta_time = c.fixedDeltaTime;
// WIP, replaced by instantInputDebug() to prevent desynchro
/* s.playersMap.forEach( (client) => {
s._handleInput(s.delta_time, client);
}); */
const gc = s.components;
gc.ballsArr.forEach((ball) => {
s._ballMovement(s.delta_time, ball);
});
if (s.matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(s.delta_time, gc);
}
}
private _ballMovement(delta: number, ball: Ball) {
const gc = this.components;
if (ball.ballInPlay)
{
ball.moveAndBounce(delta, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
if (ball.pos.x > c.w
|| ball.pos.x < 0 - ball.width)
{
ball.ballInPlay = false;
if (this.matchEnded) {
return;
}
this._scoreUpdate(ball);
setTimeout(this._newRound, c.newRoundDelay, this, ball);
}
}
}
private _scoreUpdate(ball: Ball) {
const gc = this.components;
if (ball.pos.x > c.w) {
++gc.scoreLeft;
}
else if (ball.pos.x < 0 - ball.width) {
++gc.scoreRight;
}
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
}
private _playersUpdate(s: GameSession) {
s.lastStateSnapshot = s._gameStateSnapshot();
s.playersMap.forEach( (client) => {
s.lastStateSnapshot.lastInputId = client.lastInputId;
client.socket.send(JSON.stringify(s.lastStateSnapshot));
});
}
private _spectatorsUpdate(s: GameSession) {
s.lastStateSnapshot.lastInputId = 0;
s.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(s.lastStateSnapshot));
});
}
private _gameStateSnapshot() : ev.EventGameUpdate {
const gc = this.components;
const snapshot = new ev.EventGameUpdate();
snapshot.playerLeft.y = gc.playerLeft.pos.y;
snapshot.playerRight.y = gc.playerRight.pos.y;
gc.ballsArr.forEach((ball) => {
snapshot.ballsArr.push({
x: ball.pos.x,
y: ball.pos.y,
dirX: ball.dir.x,
dirY: ball.dir.y,
speed: ball.speed
});
});
if (this.matchOptions & en.MatchOptions.movingWalls) {
snapshot.wallTop.y = gc.wallTop.pos.y;
snapshot.wallBottom.y = gc.wallBottom.pos.y;
}
return (snapshot);
}
private _newRound(s: GameSession, ball: Ball) {
if (s._checkDisconnexions()) {
return;
}
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
const gc = s.components;
const minScore = 11;// can be changed for testing
if (gc.scoreLeft >= minScore || gc.scoreRight >= minScore)
{
if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2)
{
if (gc.scoreLeft > gc.scoreRight) {
s._matchEnd(en.PlayerSide.left);
}
else {
s._matchEnd(en.PlayerSide.right);
}
return;
}
}
ball.pos.x = c.w_mid;
ball.pos.y = random(c.h*0.3, c.h*0.7);
ball.speed = ball.baseSpeed;
ball.ballInPlay = true;
}
private _checkDisconnexions() {
if (this.playersMap.size !== 2)
{
this.matchEnded = true;
if (this.playersMap.size != 0)
{
console.log("Forfeit Ending");
const gc = this.components;
const luckyWinner: ClientPlayer = this.playersMap.values().next().value;
if (luckyWinner.racket === gc.playerLeft) {
this._matchEnd(en.PlayerSide.left, true);
}
else {
this._matchEnd(en.PlayerSide.right, true);
}
}
return true;
}
return false;
}
private async _matchEnd(winner: en.PlayerSide, forfeit_flag: boolean = false)
{
this.matchEnded = true;
let eventEnd: ev.EventMatchEnd;
eventEnd = new ev.EventMatchEnd(winner, forfeit_flag);
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
const gc = this.components;
await fetch(c.addressBackEnd + "/game/gameserver/updategame",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
gameServerIdOfTheMatch: this.id,
playerOneUsernameResult: gc.scoreLeft,
playerTwoUsernameResult: gc.scoreRight,
})
});
const gameSession = this;
setTimeout(function kickRemainingClients() {
gameSession.spectatorsMap.forEach((client) => {
clientTerminate(client);
});
gameSession.playersMap.forEach((client) => {
clientTerminate(client);
});
}, 15000);
// logs
if (winner === en.PlayerSide.left) {
console.log("Player Left WIN");
}
else {
console.log("Player Right WIN");
}
}
}

View File

@@ -0,0 +1,12 @@
export * from "../shared_js/constants.js"
// 15ms == 1000/66.666
export const serverGameLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
// 33.333ms == 1000/30
export const playersUpdateIntervalMS = 1000/30; // millisecond
export const spectatorsUpdateIntervalMS = 1000/30; // millisecond
export const addressBackEnd = "http://backend_dev:3000/api/v2";

View File

@@ -0,0 +1,6 @@
export * from "../shared_js/utils.js"
export function shortId(id: string): string {
return id.substring(0, id.indexOf("-"));
}

View File

@@ -0,0 +1,393 @@
import { WebSocketServer, WebSocket as BaseLibWebSocket } from "ws";
export class WebSocket extends BaseLibWebSocket {
id?: string;
}
import { IncomingMessage } from "http";
import { v4 as uuidv4 } from 'uuid';
import * as en from "../shared_js/enums.js"
import * as ev from "../shared_js/class/Event.js"
import * as c from "./constants.js"
import { Client, ClientPlayer, ClientSpectator } from "./class/Client.js"
import { GameSession } from "./class/GameSession.js"
import { shortId } from "./utils.js";
import { gameSessionIdPLACEHOLDER } from "./constants.js";
const wsPort = 8042;
export const wsServer = new WebSocketServer<WebSocket>({host: "0.0.0.0", port: wsPort, path: "/pong"});
const clientsMap: Map<string, Client> = new Map; // socket.id/Client
const matchmakingMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const privateMatchmakingMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession
wsServer.on("connection", connectionListener);
wsServer.on("error", errorListener);
wsServer.on("close", closeListener);
function connectionListener(socket: WebSocket, request: IncomingMessage)
{
const id = uuidv4();
const client = new Client(socket, id);
clientsMap.set(id, client);
socket.id = id;
socket.on("pong", function heartbeat() {
client.isAlive = true;
console.log(`client ${shortId(client.id)} is alive`);
});
socket.on("message", function log(data: string) {
try {
const event: ev.ClientEvent = JSON.parse(data);
if (event.type === en.EventTypes.clientInput) {
return;
}
}
catch (e) {}
console.log("data: " + data);
});
socket.once("message", clientAnnounceListener);
}
async function clientAnnounceListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientAnnounce = JSON.parse(data);
if (msg.type === en.EventTypes.clientAnnounce)
{
// TODO: reconnection with msg.clientId ?
// "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that)
if (msg.role === en.ClientRole.player)
{
const announce: ev.ClientAnnouncePlayer = <ev.ClientAnnouncePlayer>msg;
// WIP nest, fetch token validation
const body = {
playerOneUsername: announce.username,
playerTwoUsername: "",
gameOptions: announce.matchOptions,
isGameIsWithInvitation: announce.privateMatch,
token: announce.token,
};
if (announce.privateMatch) {
body.playerTwoUsername = announce.playerTwoUsername;
}
const response = await fetch(c.addressBackEnd + "/game/gameserver/validate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body)
});
if (!response.ok)
{
this.send(JSON.stringify( new ev.EventError((await response.json()).message)));
clientTerminate(clientsMap.get(this.id));
return;
}
const player = clientsMap.get(this.id) as ClientPlayer;
player.matchOptions = announce.matchOptions;
player.token = announce.token;
announce.isInvitedPerson ? player.username = announce.playerTwoUsername : player.username = announce.username;
this.send(JSON.stringify( new ev.EventAssignId(this.id) )); // unused
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
if (announce.privateMatch) {
privateMatchmaking(player);
}
else {
publicMatchmaking(player);
}
}
else if (msg.role === en.ClientRole.spectator)
{
const announce: ev.ClientAnnounceSpectator = <ev.ClientAnnounceSpectator>msg;
const gameSession = gameSessionsMap.get(announce.gameSessionId);
if (!gameSession) {
this.send(JSON.stringify( new ev.EventError("invalid gameSessionId")));
clientTerminate(clientsMap.get(this.id));
return;
}
const spectator = clientsMap.get(this.id) as ClientSpectator;
spectator.gameSession = gameSession;
gameSession.spectatorsMap.set(spectator.id, spectator);
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
}
}
else {
console.log("Invalid ClientAnnounce");
}
return;
}
catch (e) {
console.log("Invalid JSON (clientAnnounceListener)");
}
this.once("message", clientAnnounceListener);
}
function publicMatchmaking(player: ClientPlayer)
{
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
matchmakingMap.set(player.id, player);
const matchOptions = player.matchOptions;
const compatiblePlayers: ClientPlayer[] = [];
for (const [id, client] of matchmakingMap)
{
if (client.matchOptions === matchOptions)
{
compatiblePlayers.push(client);
if (compatiblePlayers.length === maxPlayersNumber) {
break;
}
}
}
if (compatiblePlayers.length >= minPlayersNumber) {
compatiblePlayers.forEach((client) => {
matchmakingMap.delete(client.id);
});
createGameSession(compatiblePlayers, matchOptions);
}
}
function privateMatchmaking(player: ClientPlayer)
{
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
privateMatchmakingMap.set(player.id, player);
const matchOptions = player.matchOptions;
const token = player.token;
const compatiblePlayers: ClientPlayer[] = [];
for (const [id, client] of privateMatchmakingMap)
{
if (client.token === token)
{
compatiblePlayers.push(client);
if (compatiblePlayers.length === maxPlayersNumber) {
break;
}
}
}
if (compatiblePlayers.length >= minPlayersNumber) {
compatiblePlayers.forEach((client) => {
privateMatchmakingMap.delete(client.id);
});
createGameSession(compatiblePlayers, matchOptions);
}
else
{
setTimeout(async function abortMatch() {
if (!player.gameSession)
{
if (player.socket.OPEN) {
player.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
}
const response = await fetch(c.addressBackEnd + "/game/gameserver/destroysession",{
method: "POST",
headers : {"Content-Type": "application/json"},
body : JSON.stringify({
token : player.token
})
})
.then(x => x.json())
.catch(error => console.log("ERROR : " + error));
clientTerminate(player);
}
}, 60000);
}
}
function createGameSession(playersArr: ClientPlayer[], matchOptions: en.MatchOptions)
{
// const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR
const id = uuidv4();
const gameSession = new GameSession(id, matchOptions);
gameSessionsMap.set(id, gameSession);
playersArr.forEach((client) => {
client.gameSession = gameSession;
gameSession.playersMap.set(client.id, client);
gameSession.unreadyPlayersMap.set(client.id, client);
client.socket.once("message", playerReadyConfirmationListener);
});
// REFACTORING: Not pretty, hardcoded two players.
// Could be done in gameSession maybe ?
const gameSessionPlayersIterator = gameSession.playersMap.values();
let player: ClientPlayer;
player = (<ClientPlayer>gameSessionPlayersIterator.next().value);
player.racket = gameSession.components.playerLeft;
player.socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) ));
player = (<ClientPlayer>gameSessionPlayersIterator.next().value);
player.racket = gameSession.components.playerRight;
player.socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) ));
// REFACTORING
setTimeout(function abortMatch() {
if (gameSession.unreadyPlayersMap.size !== 0)
{
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
client.gameSession = null;
clientTerminate(client);
});
}
}, 5000);
}
async function playerReadyConfirmationListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientEvent = JSON.parse(data);
if (msg.type === en.EventTypes.clientPlayerReady)
{
const client = clientsMap.get(this.id);
const gameSession = client.gameSession;
gameSession.unreadyPlayersMap.delete(this.id);
if (gameSession.unreadyPlayersMap.size === 0)
{
// WIP nest , send gameSession.id
const gameSessionPlayersIterator = gameSession.playersMap.values();
const body = {
gameServerIdOfTheMatch : gameSession.id,
gameOptions: gameSession.matchOptions,
playerOneUsername: (<ClientPlayer>gameSessionPlayersIterator.next().value).username,
playerTwoUsername: (<ClientPlayer>gameSessionPlayersIterator.next().value).username,
playerOneUsernameResult : 0,
playerTwoUsernameResult : 0
};
const response = await fetch(c.addressBackEnd + "/game/gameserver/creategame",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body)
});
if (!response.ok)
{
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify( new ev.EventMatchAbort() ));
client.gameSession = null;
clientTerminate(client);
});
return;
}
gameSession.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
});
gameSession.start();
}
}
else {
console.log("Invalid playerReadyConfirmation");
}
return;
}
catch (e) {
console.log("Invalid JSON (playerReadyConfirmationListener)");
}
this.once("message", playerReadyConfirmationListener);
}
export function clientInputListener(this: WebSocket, data: string)
{
try {
// const input: ev.ClientEvent = JSON.parse(data);
const input: ev.EventInput = JSON.parse(data);
if (input.type === en.EventTypes.clientInput)
{
const client = clientsMap.get(this.id) as ClientPlayer;
client.inputBuffer = input;
client.gameSession.instantInputDebug(client); // wip
}
else {
console.log("Invalid clientInput");
}
}
catch (e) {
console.log("Invalid JSON (clientInputListener)");
}
}
////////////
////////////
const pingInterval = setInterval( () => {
let deleteLog = "";
clientsMap.forEach( (client) => {
if (!client.isAlive) {
clientTerminate(client);
deleteLog += ` ${shortId(client.id)} |`;
}
else {
client.isAlive = false;
client.socket.ping();
}
});
if (deleteLog) {
console.log(`Disconnected:${deleteLog}`);
}
console.log("gameSessionMap size: " + gameSessionsMap.size);
console.log("clientsMap size: " + clientsMap.size);
console.log("matchmakingMap size: " + matchmakingMap.size);
console.log("privateMatchmakingMap size: " + privateMatchmakingMap.size);
console.log("");
}, 4200);
export function clientTerminate(client: Client)
{
client.socket.terminate();
if (client.gameSession)
{
client.gameSession.playersMap.delete(client.id);
if (client.gameSession.playersMap.size === 0)
{
clearInterval(client.gameSession.playersUpdateInterval);
clearInterval(client.gameSession.spectatorsUpdateInterval);
clearInterval(client.gameSession.gameLoopInterval);
gameSessionsMap.delete(client.gameSession.id);
}
}
clientsMap.delete(client.id);
if (matchmakingMap.has(client.id)) {
matchmakingMap.delete(client.id);
}
else if (privateMatchmakingMap.has(client.id)) {
privateMatchmakingMap.delete(client.id);
}
}
function closeListener()
{
clearInterval(pingInterval);
}
function errorListener(error: Error)
{
console.log("Error: " + JSON.stringify(error));
}

View File

@@ -0,0 +1,144 @@
import * as en from "../enums.js"
/* From Server */
export class ServerEvent {
type: en.EventTypes;
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
export class EventAssignId extends ServerEvent {
id: string;
constructor(id: string) {
super(en.EventTypes.assignId);
this.id = id;
}
}
export class EventMatchmakingComplete extends ServerEvent {
side: en.PlayerSide;
constructor(side: en.PlayerSide) {
super(en.EventTypes.matchmakingComplete);
this.side = side;
}
}
export class EventGameUpdate extends ServerEvent {
playerLeft = {
y: 0
};
playerRight = {
y: 0
};
ballsArr: {
x: number,
y: number,
dirX: number,
dirY: number,
speed: number
}[] = [];
wallTop? = {
y: 0
};
wallBottom? = {
y: 0
};
lastInputId = 0;
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
super(en.EventTypes.gameUpdate);
}
}
export class EventScoreUpdate extends ServerEvent {
scoreLeft: number;
scoreRight: number;
constructor(scoreLeft: number, scoreRight: number) {
super(en.EventTypes.scoreUpdate);
this.scoreLeft = scoreLeft;
this.scoreRight = scoreRight;
}
}
export class EventMatchEnd extends ServerEvent {
winner: en.PlayerSide;
forfeit: boolean;
constructor(winner: en.PlayerSide, forfeit = false) {
super(en.EventTypes.matchEnd);
this.winner = winner;
this.forfeit = forfeit;
}
}
export class EventMatchAbort extends ServerEvent {
constructor() {
super(en.EventTypes.matchAbort);
}
}
export class EventError extends ServerEvent {
message: string;
constructor(message: string) {
super(en.EventTypes.error);
this.message = message;
}
}
/* From Client */
export class ClientEvent {
type: en.EventTypes; // readonly ?
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
export class ClientAnnounce extends ClientEvent {
role: en.ClientRole;
constructor(role: en.ClientRole) {
super(en.EventTypes.clientAnnounce);
this.role = role;
}
}
export class ClientAnnouncePlayer extends ClientAnnounce {
clientId: string; // unused
matchOptions: en.MatchOptions;
token: string;
username: string;
privateMatch: boolean;
playerTwoUsername?: string;
isInvitedPerson? : boolean;
constructor(matchOptions: en.MatchOptions, token: string, username: string, privateMatch: boolean = false, playerTwoUsername?: string, isInvitedPerson? : boolean) {
super(en.ClientRole.player);
this.matchOptions = matchOptions;
this.token = token;
this.username = username;
this.privateMatch = privateMatch;
if (isInvitedPerson) {
this.isInvitedPerson = isInvitedPerson;
}
if (playerTwoUsername) {
this.playerTwoUsername = playerTwoUsername;
}
}
}
export class ClientAnnounceSpectator extends ClientAnnounce {
gameSessionId: string;
constructor(gameSessionId: string) {
super(en.ClientRole.spectator);
this.gameSessionId = gameSessionId;
}
}
export class EventInput extends ClientEvent {
input: en.InputEnum;
id: number;
constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) {
super(en.EventTypes.clientInput);
this.input = input;
this.id = id;
}
}

View File

@@ -5,7 +5,7 @@ import { VectorInteger } from "./Vector.js";
import { Rectangle, MovingRectangle, Racket, Ball } from "./Rectangle.js";
import { random } from "../utils.js";
class GameComponents {
export class GameComponents {
wallTop: Rectangle | MovingRectangle;
wallBottom: Rectangle | MovingRectangle;
playerLeft: Racket;
@@ -61,5 +61,3 @@ class GameComponents {
}
}
}
export {GameComponents}

View File

@@ -1,9 +1,9 @@
import { Vector, VectorInteger } from "./Vector.js";
import { Component, Moving } from "./interface.js";
import type { Component, Moving } from "./interface.js";
import * as c from "../constants.js"
class Rectangle implements Component {
export class Rectangle implements Component {
pos: VectorInteger;
width: number;
height: number;
@@ -33,7 +33,7 @@ class Rectangle implements Component {
}
}
class MovingRectangle extends Rectangle implements Moving {
export class MovingRectangle extends Rectangle implements Moving {
dir: Vector = new Vector(0,0);
speed: number;
readonly baseSpeed: number;
@@ -61,7 +61,7 @@ class MovingRectangle extends Rectangle implements Moving {
}
}
class Racket extends MovingRectangle {
export class Racket extends MovingRectangle {
constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(pos, width, height, baseSpeed);
}
@@ -72,13 +72,22 @@ class Racket extends MovingRectangle {
}
}
class Ball extends MovingRectangle {
export class Ball extends MovingRectangle {
readonly speedIncrease: number;
ballInPlay: boolean = false;
constructor(pos: VectorInteger, size: number, baseSpeed: number, speedIncrease: number) {
super(pos, size, size, baseSpeed);
this.speedIncrease = speedIncrease;
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
bounce(collider?: Rectangle) {
this._bounceAlgo(collider);
}
@@ -92,15 +101,6 @@ class Ball extends MovingRectangle {
this._bounceWall();
}
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
protected _bounceWall() { // Should be enough for Wall
this.dir.y = this.dir.y * -1;
}
@@ -140,5 +140,3 @@ class Ball extends MovingRectangle {
// console.log(`x: ${this.dir.x}, y: ${this.dir.y}`);
}
}
export {Rectangle, MovingRectangle, Racket, Ball}

View File

@@ -1,5 +1,5 @@
class Vector {
export class Vector {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
@@ -16,13 +16,13 @@ class Vector {
}
}
class VectorInteger extends Vector {
export class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
class VectorInteger {
export class VectorInteger {
// private _x: number = 0;
// private _y: number = 0;
// constructor(x: number = 0, y: number = 0) {
@@ -45,5 +45,3 @@ class VectorInteger {
// }
}
*/
export {Vector, VectorInteger}

View File

@@ -1,21 +1,19 @@
import { Vector, VectorInteger } from "./Vector.js";
import type { Vector, VectorInteger } from "./Vector.js";
interface Component {
export interface Component {
pos: VectorInteger;
}
interface GraphicComponent extends Component {
export interface GraphicComponent extends Component {
ctx: CanvasRenderingContext2D;
color: string;
update: () => void;
clear: (pos?: VectorInteger) => void;
}
interface Moving {
export interface Moving {
dir: Vector;
speed: number; // pixel per second
move(delta: number): void;
}
export {Component, GraphicComponent, Moving}

View File

@@ -11,8 +11,8 @@ export const pw = Math.floor(w*0.017);
export const ph = pw*6;
export const ballSize = pw;
export const wallSize = Math.floor(w*0.01);
export const racketSpeed = Math.floor(w*0.66); // pixel per second
export const ballSpeed = Math.floor(w*0.66); // pixel per second
export const racketSpeed = Math.floor(w*0.60); // pixel per second
export const ballSpeed = Math.floor(w*0.55); // pixel per second
export const ballSpeedIncrease = Math.floor(ballSpeed*0.05); // pixel per second
export const normalizedSpeed = false; // for consistency in speed independent of direction
@@ -24,3 +24,7 @@ export const newRoundDelay = 1500; // millisecond
export const multiBallsCount = 3;
export const movingWallPosMax = Math.floor(w*0.12);
export const movingWallSpeed = Math.floor(w*0.08);
export const gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER
// for testing, force gameSession.id in wsServer.ts->matchmaking()

View File

@@ -1,15 +1,17 @@
enum EventTypes {
export enum EventTypes {
// Class Implemented
gameUpdate = 1,
scoreUpdate,
matchEnd,
assignId,
matchmakingComplete,
error,
// Generic
matchmakingInProgress,
matchStart,
matchAbort,
matchNewRound, // unused
matchPause, // unused
matchResume, // unused
@@ -21,27 +23,25 @@ enum EventTypes {
}
enum InputEnum {
export enum InputEnum {
noInput = 0,
up = 1,
down,
}
enum PlayerSide {
export enum PlayerSide {
left = 1,
right
}
enum ClientRole {
export enum ClientRole {
player = 1,
spectator
}
enum MatchOptions {
export enum MatchOptions {
// binary flags, can be mixed
noOption = 0b0,
multiBalls = 1 << 0,
movingWalls = 1 << 1
}
export {EventTypes, InputEnum, PlayerSide, ClientRole, MatchOptions}

View File

@@ -0,0 +1,25 @@
import type { MovingRectangle } from "./class/Rectangle.js";
export function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
export function sleep (ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function clamp(n: number, min: number, max: number) : number
{
if (n < min)
n = min;
else if (n > max)
n = max;
return (n);
}
// Typescript hack, unused
export function assertMovingRectangle(value: unknown): asserts value is MovingRectangle {
// if (value !== MovingRectangle) throw new Error("Not a MovingRectangle");
return;
}

View File

@@ -1,9 +1,9 @@
import * as c from "./constants.js";
import { MovingRectangle } from "../shared_js/class/Rectangle.js";
import { GameComponents } from "./class/GameComponents.js";
import type { MovingRectangle } from "../shared_js/class/Rectangle.js";
import type { GameComponents } from "./class/GameComponents.js";
function wallsMovements(delta: number, gc: GameComponents)
export function wallsMovements(delta: number, gc: GameComponents)
{
const wallTop = <MovingRectangle>gc.wallTop;
const wallBottom = <MovingRectangle>gc.wallBottom;
@@ -16,5 +16,3 @@ function wallsMovements(delta: number, gc: GameComponents)
wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
}
export {wallsMovements}

View File

@@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ES6", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@@ -5,7 +5,7 @@ WORKDIR /usr/app
COPY ./api_back ./
COPY ./api_back/.env ./.env
COPY ./api_back/src/uploads/avatars/default.png ./uploads/avatars/default.png
RUN npm install
RUN npm ci

View File

@@ -1,22 +1,13 @@
NODE_ENV=development
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=transcendance_db
POSTGRES_HOST=postgresql
POSTGRES_PORT=5432
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=9pKpKEgiamxwk5P7Ggsz
POSTGRES_DATABASE=transcendance_db
# OAUTH2 42 API
FORTYTWO_CLIENT_ID=u-s4t2ud-49dc7b539bcfe1acb48b928b2b281671c99fc5bfab1faca57a536ab7e0075500
FORTYTWO_CLIENT_SECRET=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
FORTYTWO_CALLBACK_URL=http://transcendance:8080/api/v2/auth/redirect
COOKIE_SECRET=248cdc831110eec8796d7c1edbf79835
# JWT
JWT_SECRET=442d774798979fcc14a4a2b6b7535902
# Misc
COOKIE_SECRET=JsqrZopdOb3zuAkZd+8xDkPHOhEMmbz4eAlJ+liEo0U=
PORT=3000
#Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=1a5e04138b91b3d683c708e4689454c2
#2fa
TWO_FACTOR_AUTHENTICATION_APP_NAME=Transcendance
TICKET_FOR_PLAYING_GAME_SECRET=5MkACVi80PE+7XGrG3Tij3+BE3RJk0h0v7NI0uFJswg=

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,6 @@
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mapped-types": "^1.2.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
@@ -32,7 +31,6 @@
"@nestjs/typeorm": "^9.0.1",
"@nestjs/websockets": "^9.2.1",
"@types/express-session": "^1.17.5",
"@types/redis": "^4.0.11",
"@types/validator": "^13.7.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
@@ -43,7 +41,6 @@
"otplib": "^12.0.1",
"passport": "^0.6.0",
"passport-42": "^1.2.6",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"pg": "^8.8.0",
"qrcode": "^1.5.1",
@@ -63,7 +60,6 @@
"@types/jest": "28.1.8",
"@types/multer": "^1.4.7",
"@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.7",
"@types/passport-local": "^1.0.34",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",

View File

@@ -7,7 +7,7 @@ import { ConfigModule } from '@nestjs/config';
import { FriendshipsModule } from './friendship/friendships.module';
import { AuthenticationModule } from './auth/42/authentication.module';
import { PassportModule } from '@nestjs/passport';
// import { GameModule } from './game/game/game.module';
import { GameModule } from './game/game.module';
import { ChatGateway } from './chat/chat.gateway';
@Module({
@@ -16,6 +16,7 @@ import { ChatGateway } from './chat/chat.gateway';
AuthenticationModule,
PassportModule.register({ session: true }),
FriendshipsModule,
GameModule,
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'postgres',

View File

@@ -18,7 +18,7 @@ import { CreateUsersDto } from "src/users/dto/create-users.dto";
async validate(accessToken: string, refreshToken: string, profile: Profile, callbackURL: string) {
console.log("Validate inside strategy.ts");
console.log(profile.id, profile.username, profile.phoneNumbers[0].value, profile.emails[0].value, profile.photos[0].value);
const userDTO: CreateUsersDto = { fortyTwoId: profile.id, username: profile.username, email: profile.emails[0].value, image_url: 'default.png', isEnabledTwoFactorAuth: false , status: "connected" };
const userDTO: CreateUsersDto = { fortyTwoId: profile.id, username: profile.username, email: profile.emails[0].value, image_url: 'default.png', isEnabledTwoFactorAuth: false , status: "Connected" };
const user = await this.authenticationService.validateUser(userDTO);
if (!user)
throw new UnauthorizedException();

View File

@@ -8,6 +8,12 @@ const MIME_TYPES = {
'image/png': 'png'
};
export enum STATUS {
CONNECTED = 'Connected',
DISCONNECTED = 'Disconnected',
IN_GAME = 'In Game',
IN_POOL = 'In Pool',
}
export const storageForAvatar = {
storage: diskStorage({

View File

@@ -1,16 +0,0 @@
import { IsBoolean, IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class CreateUsersDto {
@IsString()
@IsNotEmpty()
readonly username: string;
readonly fortyTwoId: string;
@IsEmail()
readonly email: string;
@IsString()
readonly image_url: string;
@IsString()
readonly status: string;
@IsBoolean()
readonly isEnabledTwoFactorAuth: boolean;
}

View File

@@ -0,0 +1,19 @@
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from "class-validator";
export class CreateGameDto {
@IsString()
@IsNotEmpty()
gameServerIdOfTheMatch : string
@IsNumber()
@IsNotEmpty()
gameOptions: number
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
playerTwoUsernameResult : number
@IsNumber()
playerOneUsernameResult : number
}

View File

@@ -0,0 +1,14 @@
import { IsBoolean, IsEmpty, IsInt, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { IsNull } from "typeorm";
export class GrantTicketDto {
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
gameOptions : number
@IsBoolean()
isGameIsWithInvitation : boolean
}

View File

@@ -0,0 +1,5 @@
import { OmitType } from "@nestjs/mapped-types";
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from "class-validator";
import { CreateGameDto } from "./createGame.dto";
export class UpdateGameDto extends OmitType(CreateGameDto, ['playerOneUsername', 'playerTwoUsername', 'gameOptions'] as const){}

View File

@@ -0,0 +1,16 @@
import { IsBase64, IsBoolean, IsEmpty, IsNotEmpty, IsNumber, IsString } from "class-validator";
export class ValidateTicketDto {
@IsString()
@IsNotEmpty()
playerOneUsername : string
@IsString()
playerTwoUsername : string
@IsNumber()
gameOptions : number
@IsBoolean()
isGameIsWithInvitation : boolean
@IsBase64()
@IsNotEmpty()
token : string
}

View File

@@ -0,0 +1,25 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('game')
export class Game {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOneUsername: string
@Column()
playerTwoUsername: string
@Column({default : 0, nullable : true})
playerOneUsernameResult : number
@Column({default : 0, nullable : true})
playerTwoUsernameResult : number
@Column({unique : true})
gameServerIdOfTheMatch: string
@Column({default: false, nullable : true}) //éric pourra trouver un meilleur mot : ongoing ?
isMatchIsFinished: boolean
}

View File

@@ -0,0 +1,22 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('tokenGame')
export class TokenGame {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOneUsername : string
@Column({nullable: true})
playerTwoUsername : string
@Column()
gameOptions : number
@Column()
isGameIsWithInvitation : boolean
@Column({default: 0, nullable: true})
numberOfRegisteredUser : number
@Column({default : false})
isSecondUserAcceptedRequest : boolean
@Column()
token : string
}

View File

@@ -1,22 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('gameParty')
export class gameParty {
@PrimaryGeneratedColumn()
id: number;
@Column()
playerOne: string
@Column()
playerTwo: string
@Column()
resultOfTheMatch: string
@Column()
gameServerIdOfTheMatch: string
}

View File

@@ -1,4 +1,96 @@
import { Controller } from '@nestjs/common';
import { Body, Controller, Get, HttpException, HttpStatus, Post, Req, Res, UseGuards } from '@nestjs/common';
import { AuthenticateGuard, TwoFactorGuard } from 'src/auth/42/guards/42guards';
import { User } from 'src/users/entities/user.entity';
import { Response } from 'express';
import { CreateGameDto } from './dto/createGame.dto';
import { GrantTicketDto } from './dto/grantTicket.dto';
import { UpdateGameDto } from './dto/updateGame.dto';
import { ValidateTicketDto } from './dto/validateTicket.dto';
import { GameService } from './game.service';
@Controller('game')
export class GameController {}
export class GameController {
constructor (private readonly gameService : GameService) { }
@Get('ranking')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async getRankingForAllUsers(@Req() req)
{
const currentUser : User = req.user
return this.gameService.getRankingForAllUsers(currentUser);
}
@Post('ticket')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async grantTicket(@Req() req, @Body() grantTicketDto : GrantTicketDto, @Res() res : Response)
{
const user : User = req.user
if (grantTicketDto.playerOneUsername != user.username)
return res.status(HttpStatus.BAD_REQUEST).json({message : 'You can\'t grant a ticket to another user'});
return this.gameService.generateToken(user, grantTicketDto, res);
}
@Post('decline')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async declineInvitation(@Body('token') token, @Req() req, @Res() res : Response)
{
const user : User = req.user;
return this.gameService.declineInvitation(user, token, res);
}
@Post('accept')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async acceptInvitation(@Body('token') token, @Req() req, @Res() res : Response)
{
const user : User = req.user;
return this.gameService.acceptInvitation(user, token, res);
}
@Get('invitations')
@UseGuards(AuthenticateGuard)
@UseGuards(TwoFactorGuard)
async findInvitations(@Req() request, @Res() res : Response)
{
const user : User = request.user;
return this.gameService.findInvitations(user, res);
}
//
//N'est valable que pour le game-serveur.
@Post('gameserver/validate')
async validateTicket(@Body() validateTicketDto : ValidateTicketDto, @Req() request)
{
if (await this.gameService.validateToken(validateTicketDto) === false)
return new HttpException("The token is not valid", HttpStatus.NOT_FOUND);
console.log("200 retourné côté nest")
return HttpStatus.OK;
}
@Post('gameserver/creategame')
async createGame(@Body() creategameDto : CreateGameDto)
{
console.log("On est dans create game")
console.log(creategameDto)
return this.gameService.createGame(creategameDto);
}
@Post('gameserver/updategame')
async updateGame(@Body() updateGameDto : UpdateGameDto)
{
console.log("On est dans update game")
console.log(updateGameDto)
return this.gameService.updateGame(updateGameDto);
}
@Post('gameserver/destroysession')
async destroySession(@Body('token') token)
{
return this.gameService.destroySession(token);
}
}

View File

@@ -1,9 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Friendship } from 'src/friendship/entities/friendship.entity';
import { FriendshipService } from 'src/friendship/friendship.service';
import { User } from 'src/users/entities/user.entity';
import { UsersService } from 'src/users/users.service';
import { Game } from './entity/game.entity';
import { TokenGame } from './entity/tokenGame.entity';
import { GameController } from './game.controller';
import { GameService } from './game.service';
@Module({
controllers: [GameController],
providers: [GameService]
imports: [TypeOrmModule.forFeature([TokenGame, User, Game, Friendship])],
controllers: [GameController],
providers: [GameService, UsersService, FriendshipService]
})
export class GameModule {}

View File

@@ -1,18 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GameService } from './game.service';
// import { Test, TestingModule } from '@nestjs/testing';
// // import { GameService } from './game.service';
describe('GameService', () => {
let service: GameService;
// describe('GameService', () => {
// let service: GameService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GameService],
}).compile();
// beforeEach(async () => {
// const module: TestingModule = await Test.createTestingModule({
// providers: [GameService],
// }).compile();
service = module.get<GameService>(GameService);
});
// service = module.get<GameService>(GameService);
// });
it('should be defined', () => {
expect(service).toBeDefined();
});
});
// it('should be defined', () => {
// expect(service).toBeDefined();
// });
// });

View File

@@ -1,4 +1,282 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable, Res } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createCipheriv, randomBytes, scrypt } from 'crypto';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
import { promisify } from 'util';
import { Response } from 'express';
import { GrantTicketDto } from './dto/grantTicket.dto';
import { Game } from './entity/game.entity';
import { ValidateTicketDto } from './dto/validateTicket.dto';
import { TokenGame } from './entity/tokenGame.entity';
import { UsersService } from 'src/users/users.service';
import { CreateGameDto } from './dto/createGame.dto';
import { UpdateGameDto } from './dto/updateGame.dto';
import { FriendshipService } from 'src/friendship/friendship.service';
import { STATUS } from 'src/common/constants/constants';
@Injectable()
export class GameService {}
export class GameService {
constructor (
@InjectRepository(Game)
private readonly gameRepository : Repository<Game>,
@InjectRepository(User)
private readonly userRepository : Repository<User>,
@InjectRepository(TokenGame)
private readonly tokenGameRepository : Repository<TokenGame>,
private readonly userService : UsersService,
private readonly friendShipService : FriendshipService
) { }
async getRankingForAllUsers(currentUser : User) {
const users = await this.userRepository.createQueryBuilder("user")
.leftJoinAndSelect("user.stats", "stats")
.orderBy('stats.winGame', "DESC")
.getMany();
const partialUser : Partial<User>[] = []
for (const user of users)
{
if (await this.friendShipService.findIfUserIsBlockedOrHasBlocked(currentUser.id.toString(), user.id.toString()) === false)
partialUser.push({username : user.username, stats : user.stats })
}
console.log(...partialUser)
return partialUser;
}
async encryptToken(toEncrypt : string) : Promise<string> {
const iv = randomBytes(16);
const password = process.env.TICKET_FOR_PLAYING_GAME_SECRET + new Date();
const key = (await promisify(scrypt)(password, 'salt', 32)) as Buffer;
const cipher = createCipheriv('aes-256-ctr', key, iv);
const encryptedText = Buffer.concat([
cipher.update(toEncrypt),
cipher.final(),
]);
const encryptedTextToReturn = encryptedText.toString('base64');
return encryptedTextToReturn
}
async deleteToken(user : User){
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.orWhere('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : user.username})
.getMany();
if (tokenGame)
return this.tokenGameRepository.remove(tokenGame);
}
async generateToken(user : User, grantTicketDto : GrantTicketDto, @Res() res : Response)
{
console.log(user.status);
if (user.status === STATUS.IN_POOL || user.status === STATUS.IN_GAME)
{
await this.deleteToken(user);
user.status = STATUS.CONNECTED;
this.userRepository.save(user);
}
if (grantTicketDto.isGameIsWithInvitation === true)
{
const secondUser : Partial<User> = await this.userService.findOneByUsername(user.id.toString(), grantTicketDto.playerTwoUsername)
if (!secondUser || secondUser.username === user.username)
return res.status(HttpStatus.NOT_FOUND).json({message : "User not found OR you want to play with yourself."});
const encryptedTextToReturn = await this.encryptToken(user.username + '_' + secondUser.username + '_'
+ grantTicketDto.gameOptions + '_' + grantTicketDto.isGameIsWithInvitation + '_' + new Date())
const tok = this.tokenGameRepository.create(grantTicketDto);
tok.isSecondUserAcceptedRequest = false;
tok.numberOfRegisteredUser = 0;
tok.token = encryptedTextToReturn;
this.tokenGameRepository.save(tok);
this.userService.updateStatus(user.id, "In Pool")
return res.status(HttpStatus.OK).json({ token : encryptedTextToReturn });
}
else if (grantTicketDto.isGameIsWithInvitation === false) {
const encryptedTextToReturn = await this.encryptToken(user.username + '_'
+ grantTicketDto.gameOptions + '_' + grantTicketDto.isGameIsWithInvitation + '_' + new Date())
const tok = this.tokenGameRepository.create(grantTicketDto);
tok.numberOfRegisteredUser = 0;
tok.token = encryptedTextToReturn;
this.tokenGameRepository.save(tok);
this.userService.updateStatus(user.id, "In Pool")
return res.status(HttpStatus.OK).json({ token : encryptedTextToReturn });
}
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({message : "Internal Server Error"});
}
async validateToken(validateTicketDto : ValidateTicketDto) {
if (validateTicketDto.isGameIsWithInvitation === true)
{
const tokenGame : TokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : validateTicketDto.playerTwoUsername})
.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: true})
.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : true})
.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
.getOne();
if (tokenGame)
{
tokenGame.numberOfRegisteredUser++;
if (tokenGame.numberOfRegisteredUser === 2)
{
this.tokenGameRepository.remove(tokenGame)
const userOne : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerOneUsername})
.getOne();
this.userService.updateStatus(userOne.id, "In Game")
const userTwo : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerTwoUsername})
.getOne();
this.deleteToken(userOne)
this.deleteToken(userTwo)
this.userService.updateStatus(userTwo.id, "In Game")
}
return true;
}
}
else if (validateTicketDto.isGameIsWithInvitation === false)
{
const tokenGame : TokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerOneUsername = :playerOneUsername', {playerOneUsername : validateTicketDto.playerOneUsername})
.andWhere('tokengame.gameOptions = :gameOption', {gameOption : validateTicketDto.gameOptions})
.andWhere('tokengame.isGameIsWithInvitation = :isGameIsWithInvitation', {isGameIsWithInvitation: false})
.andWhere('tokengame.token = :token', {token : validateTicketDto.token})
.getOne();
if (tokenGame)
{
this.tokenGameRepository.remove(tokenGame)
console.log("USERNAME : " + tokenGame.playerOneUsername)
const user : User = await this.userRepository.createQueryBuilder('user')
.where("user.username = :username", {username : tokenGame.playerOneUsername})
.getOne();
this.userService.updateStatus(user.id, "In Game")
this.deleteToken(user)
return true;
}
}
return false;
}
async findInvitations(user : User, @Res() res : Response) {
const game = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokengame.isGameIsWithInvitation = :invit', {invit : true})
.andWhere('tokengame.isSecondUserAcceptedRequest = :choice', {choice : false})
.getMany();
if (!game)
return res.status(HttpStatus.NOT_FOUND).send({message : "No invitation found"});
let partialGame : Partial<TokenGame>[] = [];
for (const gameToken of game) {
partialGame.push({
playerOneUsername : gameToken.playerOneUsername,
playerTwoUsername : gameToken.playerTwoUsername,
gameOptions : gameToken.gameOptions,
token : gameToken.token,
});
}
return res.status(HttpStatus.OK).json(partialGame);
}
async declineInvitation(user : User, token : string, @Res() res : Response)
{
if (user.status !== "Connected")
return res.status(HttpStatus.FORBIDDEN).json({message : "You must not be in game to decline an invitation"});
console.log("On décline l'invitation")
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.andWhere('tokengame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokengame.token = :token', {token : token})
.getOne();
if (tokenGame)
{
this.tokenGameRepository.remove(tokenGame);
return res.status(HttpStatus.OK).json({message : "Invitation declined."});
}
return res.status(HttpStatus.NOT_FOUND).json({message : "No invitation found !"});
}
async destroySession(token : string)
{
console.log("On détruit le token et la session qui va avec")
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokengame')
.where('tokengame.token = :token', {token : token})
.getOne();
if (tokenGame)
{
const playerOne = await this.userRepository.findOneBy({username : tokenGame.playerOneUsername})
const playerTwo = await this.userRepository.findOneBy({username : tokenGame.playerTwoUsername})
if (playerOne.status !== "Disconnected")
this.userService.updateStatus(playerOne.id, "Connected")
if (playerTwo.status !== "Disconnected")
this.userService.updateStatus(playerTwo.id, "Connected")
return this.tokenGameRepository.remove(tokenGame);
}
return new HttpException("Token not found !", HttpStatus.NOT_FOUND)
}
async acceptInvitation(user : User, token : string, @Res() res : Response)
{
if (user.status !== "Connected")
return res.status(HttpStatus.FORBIDDEN).send("")
const tokenGame = await this.tokenGameRepository.createQueryBuilder('tokenGame')
.andWhere('tokenGame.playerTwoUsername = :playerTwoUsername', {playerTwoUsername : user.username})
.andWhere('tokenGame.token = :token', {token : token})
.getOne();
if (tokenGame)
{
tokenGame.isSecondUserAcceptedRequest = true;
this.tokenGameRepository.save(tokenGame)
return res.status(HttpStatus.OK).json({message : "Invitation accepted."});
}
return res.status(HttpStatus.NOT_FOUND).json({message : "No invitation found !"});
}
async createGame(creategameDto : CreateGameDto)
{
if (creategameDto.playerOneUsername === "" || creategameDto.playerTwoUsername === ""
|| creategameDto.playerOneUsername === creategameDto.playerTwoUsername)
return HttpStatus.INTERNAL_SERVER_ERROR
const game = this.gameRepository.create(creategameDto)
game.isMatchIsFinished = false;
this.gameRepository.save(game);
if (!game)
return HttpStatus.INTERNAL_SERVER_ERROR
console.log("200 retourné pour la création de partie")
return HttpStatus.OK
}
async updateGame(updateGameDto : UpdateGameDto) {
console.log("Updata game" + updateGameDto)
const game = await this.gameRepository.createQueryBuilder('game')
.where("game.gameServerIdOfTheMatch = :gameServerIdOfTheMatch", {gameServerIdOfTheMatch : updateGameDto.gameServerIdOfTheMatch})
.getOne();
if (!game)
throw new HttpException(`The game could not be updated.`,HttpStatus.NOT_FOUND);
game.isMatchIsFinished = true;
game.playerOneUsernameResult = updateGameDto.playerOneUsernameResult
game.playerTwoUsernameResult = updateGameDto.playerTwoUsernameResult
this.gameRepository.save(game);
const playerOne = await this.userRepository.findOneBy({username : game.playerOneUsername})
const playerTwo = await this.userRepository.findOneBy({username : game.playerTwoUsername})
if (!playerOne || !playerTwo)
return new HttpException("Internal Server Error. Impossible to update the database", HttpStatus.INTERNAL_SERVER_ERROR);
if (game.playerOneUsernameResult === game.playerTwoUsernameResult)
{
this.userService.incrementDraws(playerOne.id)
this.userService.incrementDraws(playerTwo.id)
}
else if (game.playerOneUsernameResult < game.playerTwoUsernameResult)
{
this.userService.incrementDefeats(playerOne.id)
this.userService.incrementVictories(playerTwo.id)
}
else
{
this.userService.incrementVictories(playerOne.id)
this.userService.incrementDefeats(playerTwo.id)
}
this.userService.updateStatus(playerOne.id, "Connected")
this.userService.updateStatus(playerTwo.id, "Connected")
return HttpStatus.OK
}
}

View File

@@ -2,7 +2,7 @@
// et de les mettre comme optionnelles. De plus on peut hériter
// des décorateurs de la classe parente (par exemple @IsString()).
import { OmitType, PartialType } from "@nestjs/mapped-types";
import { OmitType } from "@nestjs/mapped-types";
import { CreateUsersDto } from "./create-users.dto";
export class UpdateUsersDto extends OmitType(CreateUsersDto, ['fortyTwoId', 'email', 'image_url', 'status'] as const){}

View File

@@ -28,7 +28,7 @@ export class User {
@Column({ nullable: true })
phone: string;
@Column({ default: 'disconnected' })
@Column({ default: 'Disconnected' })
status: string;
// @Column()

View File

@@ -49,11 +49,9 @@ export class UsersController {
@Get()
findOne(@Query('username') username: string, @Req() req) {
if (username === undefined) {
console.log("Backend Getting current user");
return this.usersService.findOne(req.user.id);
} else {
const user : User = req.user;
console.log('we have a query: ' + username)
return this.usersService.findOneByUsername(user.id.toString(),username);
}
}
@@ -64,7 +62,6 @@ export class UsersController {
@Get('search')
findOneByUsername(@Query('username') username: string, @Req() req) {
const user : User = req.user;
console.log('searching for user' + user.username);
return this.usersService.findOneByUsername(user.id.toString(),username);
}
@@ -81,12 +78,9 @@ export class UsersController {
@UseGuards(TwoFactorGuard)
@Patch()
async update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto, @Res() response : Response) {
console.log("DANS PATCH USERS");
const user = await this.usersService.update(req.user.id, usersUpdateDto);
// const user : User = req.user;
if (user.isEnabledTwoFactorAuth === false && user.isTwoFactorAuthenticated === true)
this.usersService.setIsTwoFactorAuthenticatedWhenLogout(user.id);
console.log ("Enbale 2FA " + user.isEnabledTwoFactorAuth + " Is authenticated " + user.isTwoFactorAuthenticated);
if (user.isEnabledTwoFactorAuth === true && user.isTwoFactorAuthenticated === false)
{
response.status(201).send('2FA redirect')

View File

@@ -4,12 +4,9 @@ import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
import { CreateUsersDto } from './dto/create-users.dto';
import { UpdateUsersDto } from './dto/update-users.dto';
import { Friendship } from '../friendship/entities/friendship.entity';
import { PaginationQueryDto } from 'src/common/dto/pagination-query.dto';
import { UserStats } from './entities/userStat.entities';
import { FriendshipService } from 'src/friendship/friendship.service';
import { stringify } from 'querystring';
// On va devoir sûrement trouver un moyen plus simple pour passer l'id, sûrement via des pipes
// ou des interceptors, mais pour l'instant on va faire comme ça.
@Injectable()

View File

@@ -11,6 +11,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://backend_dev:3000;
}
location /chat {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -19,6 +20,19 @@ server {
proxy_set_header Connection "upgrade";
proxy_pass http://backend_dev:5000/chat;
}
location /api/v2/game/gameserver {
deny all;
}
location /pong {
proxy_pass http://game_server:8042/pong;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -1,14 +1,14 @@
header.svelte-7t4byu.svelte-7t4byu{overflow-y:hidden}.grid-container.svelte-7t4byu.svelte-7t4byu{position:absolute;left:0;top:0;box-sizing:border-box;width:100%;height:100%;white-space:nowrap;margin-bottom:0px;overflow:hidden;padding:20px 40px;margin:0px;display:grid;grid-template-columns:repeat(12, 1fr);grid-template-rows:1fr 1fr 1fr 1fr 1fr;align-items:center}header.svelte-7t4byu h1.svelte-7t4byu{grid-column:1 / 7;grid-row:1;padding:20px;border:1px solid bisque}header.svelte-7t4byu nav.svelte-7t4byu{grid-column:7 / 13;grid-row:1;justify-self:end;padding:20px;border:1px solid bisque}header.svelte-7t4byu h2.svelte-7t4byu{grid-row:3;grid-column:5 / span 4;justify-self:center;border:1px solid black;z-index:3}header.svelte-7t4byu h2 div.svelte-7t4byu{font-size:2em}nav.svelte-7t4byu div.svelte-7t4byu{display:inline;color:bisque;font-weight:bold}nav.svelte-7t4byu>div.svelte-7t4byu{padding-left:1em}nav.svelte-7t4byu div.svelte-7t4byu:hover{text-decoration:underline;cursor:pointer}main.svelte-1cznfcz.svelte-1cznfcz{text-align:center;padding-top:40px;padding-bottom:40px}form.svelte-1cznfcz.svelte-1cznfcz{padding-top:15px}form.svelte-1cznfcz input.svelte-1cznfcz{max-width:330px}.error.svelte-1cznfcz.svelte-1cznfcz{font-weight:bold;font-size:0.8em;color:red}div.wrapper.svelte-1q8uute{display:flexbox;align-items:center}div.wrapper.svelte-1q8uute{display:flexbox;align-items:center}canvas.svelte-1bstsd0{width:100%;height:100%;background-color:#666}@font-face{font-family:'Bondi';src:url('/fonts/Bondi.ttf.woff') format('woff'),
url('/fonts/Bondi.ttf.svg#Bondi') format('svg'),
url('/fonts/Bondi.ttf.eot'),
url('/fonts/Bondi.ttf.eot?#iefix') format('embedded-opentype');font-weight:normal;font-style:normal}header.svelte-1gjmpio.svelte-1gjmpio{background:#618174;margin:0}header.svelte-1gjmpio.svelte-1gjmpio{position:sticky;display:grid;grid-template-columns:1fr 1fr 1fr}h1.svelte-1gjmpio.svelte-1gjmpio{font-family:'Bondi'}h1.svelte-1gjmpio.svelte-1gjmpio{margin:0;text-align:left;display:flex;justify-self:center;align-self:center}img.svelte-1gjmpio.svelte-1gjmpio{cursor:pointer;max-width:40px;padding:7px 20px;justify-self:left}nav.svelte-1gjmpio.svelte-1gjmpio{display:flex;justify-content:right}nav.svelte-1gjmpio button.svelte-1gjmpio{margin:7px 20px;border-radius:4px}div.outer.svelte-16aefqu{max-width:960px;margin:40px auto}:root{--purple:rgb(123, 31, 162);--violet:rgb(103, 58, 183);--pink:rgb(244, 143, 177)}@keyframes svelte-16aefqu-background-pan{from{background-position:0% center}to{background-position:-200% center}}@keyframes svelte-16aefqu-scale{from,to{transform:scale(0)}50%{transform:scale(1)}}@keyframes svelte-16aefqu-rotate{from{transform:rotate(0deg)}to{transform:rotate(180deg)}}main.svelte-qtbld7{text-align:center}div.cards.svelte-qtbld7{display:grid;grid-template-columns:1fr 1fr;grid-gap:20px}img.svelte-qtbld7{width:60px}form.svelte-qtbld7{text-align:center}.form-field.svelte-qtbld7{padding:10px}.label.svelte-qtbld7{font-weight:bold}.inline-check.svelte-qtbld7{display:inline}.error.svelte-qtbld7{font-size:0.8em;font-weight:bold;color:red}.success.svelte-qtbld7{font-size:0.8em;font-weight:bold;color:green}div.top-grid.svelte-55f7si{display:grid;grid-template-columns:repeat(12, 1fr);height:85vh}div.all-users-sidebar.svelte-55f7si{grid-column:1 / span 2;background:white}div.a-user.svelte-55f7si{display:inline-block}div.status.svelte-55f7si{font-size:0.6em;font-weight:bold}div[class^="a-user"].svelte-55f7si:hover{text-decoration:underline;font-weight:bold;cursor:pointer}div.main-display.svelte-55f7si{grid-column:3 / span 10}.error.svelte-55f7si{font-size:0.8em;font-weight:bold;color:red}div.outer.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:960px;margin:40px auto}main.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:960px;margin:40px auto;text-align:center}.avatar.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:150px}section.main-stats.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:600px;margin:40px auto;text-align:center;display:grid;grid-template-columns:repeat(3, 1fr);grid-template-rows:repeat(3, 1fr)}section.main-stats.svelte-16aefqu h4.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{grid-column:1 / span 3}div.username.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{font-size:1.5em;font-weight:bold;padding-bottom:5px}div.rank.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{font-size:1.2em;font-weight:bold}:root{--purple:rgb(123, 31, 162);--violet:rgb(103, 58, 183);--pink:rgb(244, 143, 177)}@keyframes svelte-16aefqu-background-pan{from{background-position:0% center}to{background-position:-200% center}}@keyframes svelte-16aefqu-scale{from,to{transform:scale(0)}50%{transform:scale(1)}}@keyframes svelte-16aefqu-rotate{from{transform:rotate(0deg)}to{transform:rotate(180deg)}}div.svelte-16aefqu>.glitter.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{display:inline-block;position:relative}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{--size:clamp(20px, 1.5vw, 30px);animation:svelte-16aefqu-scale 700ms ease forwards;display:block;height:var(--size);left:var(--star-left);position:absolute;top:var(--star-top);width:var(--size)}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu>svg.svelte-16aefqu.svelte-16aefqu{animation:svelte-16aefqu-rotate 1000ms linear infinite;display:block;opacity:0.7}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu>svg.svelte-16aefqu>path.svelte-16aefqu{fill:var(--violet)}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-text.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{animation:svelte-16aefqu-background-pan 3s linear infinite;background:linear-gradient(
url('/fonts/Bondi.ttf.eot?#iefix') format('embedded-opentype');font-weight:normal;font-style:normal}header.svelte-1aisfio.svelte-1aisfio{background:#618174;margin:0}header.svelte-1aisfio.svelte-1aisfio{position:sticky;display:grid;grid-template-columns:1fr 1fr 1fr}h1.svelte-1aisfio.svelte-1aisfio{font-family:'Bondi'}h1.svelte-1aisfio.svelte-1aisfio{margin:0;text-align:left;display:flex;justify-self:center;align-self:center}img.svelte-1aisfio.svelte-1aisfio{cursor:pointer;max-width:40px;padding:7px 20px;justify-self:left}nav.svelte-1aisfio.svelte-1aisfio{display:flex;justify-content:right}nav.svelte-1aisfio button.svelte-1aisfio{margin:7px 20px;border-radius:4px}div.outer.svelte-1tyjf3q{max-width:960px;margin:40px auto}:root{--purple:rgb(123, 31, 162);--violet:rgb(103, 58, 183);--pink:rgb(244, 143, 177)}@keyframes svelte-1tyjf3q-background-pan{from{background-position:0% center}to{background-position:-200% center}}@keyframes svelte-1tyjf3q-scale{from,to{transform:scale(0)}50%{transform:scale(1)}}@keyframes svelte-1tyjf3q-rotate{from{transform:rotate(0deg)}to{transform:rotate(180deg)}}main.svelte-qtbld7{text-align:center}div.cards.svelte-qtbld7{display:grid;grid-template-columns:1fr 1fr;grid-gap:20px}img.svelte-qtbld7{width:60px}form.svelte-qtbld7{text-align:center}.form-field.svelte-qtbld7{padding:10px}.label.svelte-qtbld7{font-weight:bold}.inline-check.svelte-qtbld7{display:inline}.error.svelte-qtbld7{font-size:0.8em;font-weight:bold;color:red}.success.svelte-qtbld7{font-size:0.8em;font-weight:bold;color:green}div.top-grid.svelte-55f7si{display:grid;grid-template-columns:repeat(12, 1fr);height:85vh}div.all-users-sidebar.svelte-55f7si{grid-column:1 / span 2;background:white}div.a-user.svelte-55f7si{display:inline-block}div.status.svelte-55f7si{font-size:0.6em;font-weight:bold}div[class^="a-user"].svelte-55f7si:hover{text-decoration:underline;font-weight:bold;cursor:pointer}div.main-display.svelte-55f7si{grid-column:3 / span 10}.error.svelte-55f7si{font-size:0.8em;font-weight:bold;color:red}div.outer.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:960px;margin:40px auto}main.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:960px;margin:40px auto;text-align:center}.avatar.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:150px}section.main-stats.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{max-width:600px;margin:40px auto;text-align:center;display:grid;grid-template-columns:repeat(3, 1fr);grid-template-rows:repeat(3, 1fr)}section.main-stats.svelte-16aefqu h4.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{grid-column:1 / span 3}div.username.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{font-size:1.5em;font-weight:bold;padding-bottom:5px}div.rank.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{font-size:1.2em;font-weight:bold}:root{--purple:rgb(123, 31, 162);--violet:rgb(103, 58, 183);--pink:rgb(244, 143, 177)}@keyframes svelte-16aefqu-background-pan{from{background-position:0% center}to{background-position:-200% center}}@keyframes svelte-16aefqu-scale{from,to{transform:scale(0)}50%{transform:scale(1)}}@keyframes svelte-16aefqu-rotate{from{transform:rotate(0deg)}to{transform:rotate(180deg)}}div.svelte-16aefqu>.glitter.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{display:inline-block;position:relative}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{--size:clamp(20px, 1.5vw, 30px);animation:svelte-16aefqu-scale 700ms ease forwards;display:block;height:var(--size);left:var(--star-left);position:absolute;top:var(--star-top);width:var(--size)}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu>svg.svelte-16aefqu.svelte-16aefqu{animation:svelte-16aefqu-rotate 1000ms linear infinite;display:block;opacity:0.7}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-star.svelte-16aefqu>svg.svelte-16aefqu>path.svelte-16aefqu{fill:var(--violet)}div.svelte-16aefqu>.glitter.svelte-16aefqu>.glitter-text.svelte-16aefqu.svelte-16aefqu.svelte-16aefqu{animation:svelte-16aefqu-background-pan 3s linear infinite;background:linear-gradient(
to right,
var(--purple),
var(--violet),
var(--pink),
var(--purple)
);background-size:200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;color:transparent;white-space:nowrap}.card.svelte-8smyff{background:white;padding:20px;border-radius:6px;box-shadow:0px 2px 4px rgba(0,0,0,0.1)}button.svelte-1u0z9cq{border:0;cursor:pointer;border-radius:6px;padding:8px 12px;font-weight:bold;box-shadow:1px 2px 3px rgba(0,0,0,0.2)}.primary.svelte-1u0z9cq{background:#d91b42;color:white}.secondary.svelte-1u0z9cq{background:#45c496;color:white}.flat.svelte-1u0z9cq{box-shadow:none}.primary.inverse.svelte-1u0z9cq{color:#d91b42;background:white;border:2px solid #d91b42}.secondary.inverse.svelte-1u0z9cq{color:#45c496;background:white;border:2px solid #45c496}.chat_box.svelte-zxenra.svelte-zxenra{display:flex;position:fixed;bottom:20px;right:20px;padding:5px;width:300px;height:400px;border:1px solid black}.chat.svelte-zxenra.svelte-zxenra{grid-area:chat}.chat_box.close.svelte-zxenra .grid_box.svelte-zxenra{gap:0px;grid:' chat ' auto
/ auto }.chat_box.close.svelte-zxenra.svelte-zxenra{padding:0px;width:auto;height:auto}.chat_box.svelte-zxenra *{-ms-overflow-style:none;scrollbar-width:none}.chat_box.svelte-zxenra *::-webkit-scrollbar{display:none}.chat_box.svelte-zxenra .grid_box{display:grid;margin:0px;gap:5px;width:100%;height:100%}.chat_box.svelte-zxenra .grid_box *{display:flex;flex-direction:column;position:relative;box-sizing:border-box}.chat_box.svelte-zxenra .grid_box p{padding:10px;font-size:15px}.chat_box.svelte-zxenra .panel{overflow-y:scroll}.chat_box.svelte-zxenra .panel > *{margin-top:10px;margin-bottom:10px}.chat_box.svelte-zxenra .__show_if_only_child{display:none}.chat_box.svelte-zxenra .__show_if_only_child:only-child{display:flex;color:rgb(100, 100, 100)}.chat_box.svelte-zxenra .__center{margin-left:auto;margin-right:auto}.chat_box.svelte-zxenra .__border_top{border-top:1px solid black}.chat_box.svelte-zxenra .__check_change_next:checked ~ .__to_show{display:flex}.chat_box.svelte-zxenra .__check_change_next:checked ~ .__to_block,.chat_box.svelte-zxenra .__check_change_next:checked ~ .__to_block *{pointer-events:none;color:rgb(100, 100, 100)}.chat_box.svelte-zxenra .__to_show{display:none}button.svelte-j5z24r.svelte-j5z24r{display:flex;padding:0px;margin:auto;width:100%;cursor:pointer;outline:none;border:none;background-color:rgb(220, 220, 220)}button.svelte-j5z24r p.svelte-j5z24r{width:100%;margin:auto;text-align:center}button.svelte-j5z24r.svelte-j5z24r:hover{background-color:rgb(200, 200, 200)}button.svelte-j5z24r.svelte-j5z24r:active{background-color:rgb(190, 190, 190)}.list.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:rgb(240, 240, 240)}.list.svelte-j5z24r p.svelte-j5z24r{text-align:left}.transparent.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:transparent}.deactivate.svelte-j5z24r.svelte-j5z24r{background-color:transparent;pointer-events:none}.icon.svelte-j5z24r p.svelte-j5z24r{display:none}.icon.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:transparent}.icon.svelte-j5z24r.svelte-j5z24r{width:30px;height:100%;padding:0px}.dots.svelte-j5z24r.svelte-j5z24r::after{content:'\2807';font-size:20px;position:absolute;top:50%;left:0px;width:100%;height:auto;text-align:center;transform:translateY(-50%);cursor:pointer}.close.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 1px);left:5px;width:20px;height:2px;background-color:black}.back.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 6px - 1px);left:6px;width:14px;height:14px;border-left:1px solid black;border-bottom:1px solid black;transform:rotate(45deg)}.blocked.svelte-j5z24r.svelte-j5z24r{padding-left:30px}.blocked.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 2px);left:10px;cursor:pointer;width:13px;height:10px;border-radius:2px;background-color:rgb(110, 110, 110)}.blocked.svelte-j5z24r.svelte-j5z24r::after{content:"";position:absolute;top:calc(50% - 9px);left:12px;cursor:pointer;width:9px;height:13px;border-radius:5px;box-sizing:border-box;border:3px solid rgb(110, 110, 110)}.grid_box.svelte-1jygwt2 .settings {grid-area:settings}.grid_box.svelte-1jygwt2 .close {grid-area:close}.grid_box.svelte-1jygwt2 .new {grid-area:new}.grid_box.svelte-1jygwt2 .panel_home{grid-area:panel_home}.grid_box.svelte-1jygwt2.svelte-1jygwt2{grid:' settings new close ' auto
);background-size:200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;color:transparent;white-space:nowrap}.card.svelte-8smyff{background:white;padding:20px;border-radius:6px;box-shadow:0px 2px 4px rgba(0,0,0,0.1)}button.svelte-1u0z9cq{border:0;cursor:pointer;border-radius:6px;padding:8px 12px;font-weight:bold;box-shadow:1px 2px 3px rgba(0,0,0,0.2)}.primary.svelte-1u0z9cq{background:#d91b42;color:white}.secondary.svelte-1u0z9cq{background:#45c496;color:white}.flat.svelte-1u0z9cq{box-shadow:none}.primary.inverse.svelte-1u0z9cq{color:#d91b42;background:white;border:2px solid #d91b42}.secondary.inverse.svelte-1u0z9cq{color:#45c496;background:white;border:2px solid #45c496}.chat_box.svelte-auy6qm{display:flex;position:fixed;bottom:20px;right:20px;padding:0px;width:auto;height:auto;border:1px solid black}.chat_box.svelte-auy6qm *{-ms-overflow-style:none;scrollbar-width:none}.chat_box.svelte-auy6qm *::-webkit-scrollbar{display:none}.chat_box.svelte-auy6qm .grid_box{display:grid;margin:5px;gap:5px;width:300px;height:400px}.chat_box.svelte-auy6qm .grid_box *{display:flex;flex-direction:column;position:relative;box-sizing:border-box}.chat_box.svelte-auy6qm .grid_box p{padding:10px;font-size:15px}.chat_box.svelte-auy6qm .panel{overflow-y:scroll}.chat_box.svelte-auy6qm .panel > *{margin-top:10px;margin-bottom:10px}.chat_box.svelte-auy6qm .__show_if_only_child{display:none}.chat_box.svelte-auy6qm .__show_if_only_child:only-child{display:flex;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__center{margin-left:auto;margin-right:auto}.chat_box.svelte-auy6qm .__border_top{border-top:1px solid black}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_show{display:flex}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block,.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block *{pointer-events:none;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__to_show{display:none}.grid_box.svelte-fc4a40 .chat{grid-area:chat}.grid_box.svelte-fc4a40{gap:0px;grid:' chat ' auto
/ auto }.chat_box.close .grid_box.svelte-fc4a40{margin:0px;width:auto;height:auto}.grid_box.svelte-1jygwt2 .settings {grid-area:settings}.grid_box.svelte-1jygwt2 .close {grid-area:close}.grid_box.svelte-1jygwt2 .new {grid-area:new}.grid_box.svelte-1jygwt2 .panel_home{grid-area:panel_home}.grid_box.svelte-1jygwt2.svelte-1jygwt2{grid:' settings new close ' auto
' panel_home panel_home panel_home ' 1fr
/ auto 1fr auto }.panel_home.svelte-1jygwt2 p.title.svelte-1jygwt2{margin:10px auto 0px auto}.grid_box.svelte-rmdfjs .back {grid-area:back}.grid_box.svelte-rmdfjs .room_name {grid-area:room_name}.grid_box.svelte-rmdfjs .close {grid-area:close}.grid_box.svelte-rmdfjs .panel_msg {grid-area:panel_msg}.grid_box.svelte-rmdfjs .send {grid-area:send}.grid_box.svelte-rmdfjs .panel_write{grid-area:panel_write}.grid_box.svelte-rmdfjs.svelte-rmdfjs{grid:' back room_name room_name close ' auto
' panel_msg panel_msg panel_msg panel_msg ' 1fr
@@ -28,5 +28,6 @@ header.svelte-7t4byu.svelte-7t4byu{overflow-y:hidden}.grid-container.svelte-7t4b
/ auto 1fr auto }form.svelte-yo0any input[type=checkbox].svelte-yo0any.svelte-yo0any{display:none}form.svelte-yo0any label._checkbox.svelte-yo0any.svelte-yo0any{margin:0px auto 0px 10px;padding-left:10px;cursor:pointer}form.svelte-yo0any label._checkbox.svelte-yo0any.svelte-yo0any::after{content:"";position:absolute;top:calc(50% - 6px);left:0px;width:12px;height:12px;border:2px solid rgb(150, 150, 150);box-sizing:border-box;cursor:pointer}form.svelte-yo0any input[type=checkbox].svelte-yo0any:checked+label._checkbox.svelte-yo0any::after{background-color:rgb(200, 200, 200)}form.svelte-yo0any label._select.svelte-yo0any.svelte-yo0any{flex-direction:row}form.svelte-yo0any label._select p.svelte-yo0any.svelte-yo0any{margin:0px}form.svelte-yo0any select.svelte-yo0any.svelte-yo0any{margin:auto auto auto 10px;background-color:rgb(220, 220, 220);border:none;padding:5px;cursor:pointer}form.svelte-yo0any select.svelte-yo0any.svelte-yo0any:hover{background-color:rgb(200, 200, 200)}form.svelte-yo0any input[type=submit].svelte-yo0any.svelte-yo0any{margin-top:20px}.grid_box.svelte-1fj8iha .back {grid-area:back}.grid_box.svelte-1fj8iha .user {grid-area:user}.grid_box.svelte-1fj8iha .close {grid-area:close}.grid_box.svelte-1fj8iha .room_name {grid-area:room_name}.grid_box.svelte-1fj8iha .panel_user{grid-area:panel_user}.grid_box.svelte-1fj8iha{grid:' back user close ' auto
' room_name room_name room_name ' auto
' panel_user panel_user panel_user ' 1fr
/ auto 1fr auto }.panel_user.svelte-1fj8iha{margin-top:-5px}.chat_msg.svelte-14xxpbz.svelte-14xxpbz{margin:5px auto;padding:5px;border-radius:5px}.chat_msg.svelte-14xxpbz.svelte-14xxpbz{margin-left:0px;background-color:rgb(210, 210, 210);max-width:80%}.chat_msg.svelte-14xxpbz p.svelte-14xxpbz{padding:0px}.chat_msg.svelte-14xxpbz p.name.svelte-14xxpbz{margin:0px;font-size:12px;color:rgb(100, 100, 100)}.chat_msg.svelte-14xxpbz p.msg.svelte-14xxpbz{margin:5px 0px}.chat_msg.svelte-14xxpbz p.msg.svelte-14xxpbz *{display:inline}.chat_msg.me.svelte-14xxpbz.svelte-14xxpbz{margin-right:0px;margin-left:auto;background-color:rgb(210, 110, 10)}.chat_msg.me.svelte-14xxpbz p.name.svelte-14xxpbz{display:none}.chat_msg.SERVER.svelte-14xxpbz.svelte-14xxpbz{margin-left:auto;background-color:transparent}.chat_msg.SERVER.svelte-14xxpbz p.name.svelte-14xxpbz{display:none}.chat_msg.SERVER.svelte-14xxpbz p.msg.svelte-14xxpbz{margin:0px auto;font-size:12px;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm{display:flex;position:fixed;bottom:20px;right:20px;padding:0px;width:auto;height:auto;border:1px solid black}.chat_box.svelte-auy6qm *{-ms-overflow-style:none;scrollbar-width:none}.chat_box.svelte-auy6qm *::-webkit-scrollbar{display:none}.chat_box.svelte-auy6qm .grid_box{display:grid;margin:5px;gap:5px;width:300px;height:400px}.chat_box.svelte-auy6qm .grid_box *{display:flex;flex-direction:column;position:relative;box-sizing:border-box}.chat_box.svelte-auy6qm .grid_box p{padding:10px;font-size:15px}.chat_box.svelte-auy6qm .panel{overflow-y:scroll}.chat_box.svelte-auy6qm .panel > *{margin-top:10px;margin-bottom:10px}.chat_box.svelte-auy6qm .__show_if_only_child{display:none}.chat_box.svelte-auy6qm .__show_if_only_child:only-child{display:flex;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__center{margin-left:auto;margin-right:auto}.chat_box.svelte-auy6qm .__border_top{border-top:1px solid black}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_show{display:flex}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block,.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block *{pointer-events:none;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__to_show{display:none}.grid_box.svelte-fc4a40 .chat{grid-area:chat}.grid_box.svelte-fc4a40{gap:0px;grid:' chat ' auto
/ auto }.chat_box.close .grid_box.svelte-fc4a40{margin:0px;width:auto;height:auto}.chat_box.svelte-auy6qm{display:flex;position:fixed;bottom:20px;right:20px;padding:0px;width:auto;height:auto;border:1px solid black}.chat_box.svelte-auy6qm *{-ms-overflow-style:none;scrollbar-width:none}.chat_box.svelte-auy6qm *::-webkit-scrollbar{display:none}.chat_box.svelte-auy6qm .grid_box{display:grid;margin:5px;gap:5px;width:300px;height:400px}.chat_box.svelte-auy6qm .grid_box *{display:flex;flex-direction:column;position:relative;box-sizing:border-box}.chat_box.svelte-auy6qm .grid_box p{padding:10px;font-size:15px}.chat_box.svelte-auy6qm .panel{overflow-y:scroll}.chat_box.svelte-auy6qm .panel > *{margin-top:10px;margin-bottom:10px}.chat_box.svelte-auy6qm .__show_if_only_child{display:none}.chat_box.svelte-auy6qm .__show_if_only_child:only-child{display:flex;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__center{margin-left:auto;margin-right:auto}.chat_box.svelte-auy6qm .__border_top{border-top:1px solid black}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_show{display:flex}.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block,.chat_box.svelte-auy6qm .__check_change_next:checked ~ .__to_block *{pointer-events:none;color:rgb(100, 100, 100)}.chat_box.svelte-auy6qm .__to_show{display:none}
/ auto 1fr auto }.panel_user.svelte-1fj8iha{margin-top:-5px}button.svelte-j5z24r.svelte-j5z24r{display:flex;padding:0px;margin:auto;width:100%;cursor:pointer;outline:none;border:none;background-color:rgb(220, 220, 220)}button.svelte-j5z24r p.svelte-j5z24r{width:100%;margin:auto;text-align:center}button.svelte-j5z24r.svelte-j5z24r:hover{background-color:rgb(200, 200, 200)}button.svelte-j5z24r.svelte-j5z24r:active{background-color:rgb(190, 190, 190)}.list.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:rgb(240, 240, 240)}.list.svelte-j5z24r p.svelte-j5z24r{text-align:left}.transparent.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:transparent}.deactivate.svelte-j5z24r.svelte-j5z24r{background-color:transparent;pointer-events:none}.icon.svelte-j5z24r p.svelte-j5z24r{display:none}.icon.svelte-j5z24r.svelte-j5z24r:not(:hover){background-color:transparent}.icon.svelte-j5z24r.svelte-j5z24r{width:30px;height:100%;padding:0px}.dots.svelte-j5z24r.svelte-j5z24r::after{content:'\2807';font-size:20px;position:absolute;top:50%;left:0px;width:100%;height:auto;text-align:center;transform:translateY(-50%);cursor:pointer}.close.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 1px);left:5px;width:20px;height:2px;background-color:black}.back.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 6px - 1px);left:6px;width:14px;height:14px;border-left:1px solid black;border-bottom:1px solid black;transform:rotate(45deg)}.blocked.svelte-j5z24r.svelte-j5z24r{padding-left:30px}.blocked.svelte-j5z24r.svelte-j5z24r::before{content:"";position:absolute;top:calc(50% - 2px);left:10px;cursor:pointer;width:13px;height:10px;border-radius:2px;background-color:rgb(110, 110, 110)}.blocked.svelte-j5z24r.svelte-j5z24r::after{content:"";position:absolute;top:calc(50% - 9px);left:12px;cursor:pointer;width:9px;height:13px;border-radius:5px;box-sizing:border-box;border:3px solid rgb(110, 110, 110)}.chat_msg.svelte-14xxpbz.svelte-14xxpbz{margin:5px auto;padding:5px;border-radius:5px}.chat_msg.svelte-14xxpbz.svelte-14xxpbz{margin-left:0px;background-color:rgb(210, 210, 210);max-width:80%}.chat_msg.svelte-14xxpbz p.svelte-14xxpbz{padding:0px}.chat_msg.svelte-14xxpbz p.name.svelte-14xxpbz{margin:0px;font-size:12px;color:rgb(100, 100, 100)}.chat_msg.svelte-14xxpbz p.msg.svelte-14xxpbz{margin:5px 0px}.chat_msg.svelte-14xxpbz p.msg.svelte-14xxpbz *{display:inline}.chat_msg.me.svelte-14xxpbz.svelte-14xxpbz{margin-right:0px;margin-left:auto;background-color:rgb(210, 110, 10)}.chat_msg.me.svelte-14xxpbz p.name.svelte-14xxpbz{display:none}.chat_msg.SERVER.svelte-14xxpbz.svelte-14xxpbz{margin-left:auto;background-color:transparent}.chat_msg.SERVER.svelte-14xxpbz p.name.svelte-14xxpbz{display:none}.chat_msg.SERVER.svelte-14xxpbz p.msg.svelte-14xxpbz{margin:0px auto;font-size:12px;color:rgb(100, 100, 100)}@font-face{font-family:"Bit5x3";src:url("/fonts/Bit5x3.woff2") format("woff2"),
local("Bit5x3"),
url("/fonts/Bit5x3.woff") format("woff");font-weight:normal;font-style:normal;font-display:swap}#game_page.svelte-y455cj.svelte-y455cj{margin:0;background-color:#222425;position:relative;width:100%;height:100%}#canvas_container.svelte-y455cj.svelte-y455cj{margin-top:20px;text-align:center}#users_name.svelte-y455cj.svelte-y455cj{text-align:center;font-family:"Bit5x3";color:rgb(245, 245, 245);font-size:x-large}#div_game.svelte-y455cj.svelte-y455cj{margin-top:20px;text-align:center;font-family:"Bit5x3";color:rgb(245, 245, 245);font-size:x-large}#error_notification.svelte-y455cj.svelte-y455cj{text-align:center;display:block;font-family:"Bit5x3";color:rgb(143, 19, 19);font-size:x-large}#div_game.svelte-y455cj fieldset.svelte-y455cj{max-width:50vw;width:auto;margin:0 auto}#div_game.svelte-y455cj fieldset div.svelte-y455cj{padding:10px}#pong_button.svelte-y455cj.svelte-y455cj{font-family:"Bit5x3";color:rgb(245, 245, 245);background-color:#333333;font-size:x-large;padding:10px}canvas.svelte-y455cj.svelte-y455cj{background-color:#333333;max-width:75vw;width:80%}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,107 +0,0 @@
import * as en from "../enums.js"
/* From Server */
class ServerEvent {
type: en.EventTypes;
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
class EventAssignId extends ServerEvent {
id: string;
constructor(id: string) {
super(en.EventTypes.assignId);
this.id = id;
}
}
class EventMatchmakingComplete extends ServerEvent {
side: en.PlayerSide;
constructor(side: en.PlayerSide) {
super(en.EventTypes.matchmakingComplete);
this.side = side;
}
}
class EventGameUpdate extends ServerEvent {
playerLeft = {
y: 0
};
playerRight = {
y: 0
};
ballsArr: {
x: number,
y: number,
dirX: number,
dirY: number,
speed: number
}[] = [];
wallTop? = {
y: 0
};
wallBottom? = {
y: 0
};
lastInputId = 0;
constructor() { // TODO: constructor that take GameComponentsServer maybe ?
super(en.EventTypes.gameUpdate);
}
}
class EventScoreUpdate extends ServerEvent {
scoreLeft: number;
scoreRight: number;
constructor(scoreLeft: number, scoreRight: number) {
super(en.EventTypes.scoreUpdate);
this.scoreLeft = scoreLeft;
this.scoreRight = scoreRight;
}
}
class EventMatchEnd extends ServerEvent {
winner: en.PlayerSide;
constructor(winner: en.PlayerSide) {
super(en.EventTypes.matchEnd);
this.winner = winner;
}
}
/* From Client */
class ClientEvent {
type: en.EventTypes; // readonly ?
constructor(type: en.EventTypes = 0) {
this.type = type;
}
}
class ClientAnnounce extends ClientEvent {
role: en.ClientRole;
clientId: string;
matchOptions: en.MatchOptions;
constructor(role: en.ClientRole, matchOptions: en.MatchOptions, clientId: string = "") {
super(en.EventTypes.clientAnnounce);
this.role = role;
this.clientId = clientId;
this.matchOptions = matchOptions;
}
}
class EventInput extends ClientEvent {
input: en.InputEnum;
id: number;
constructor(input: en.InputEnum = en.InputEnum.noInput, id: number = 0) {
super(en.EventTypes.clientInput);
this.input = input;
this.id = id;
}
}
export {
ServerEvent, EventAssignId, EventMatchmakingComplete,
EventGameUpdate, EventScoreUpdate, EventMatchEnd,
ClientEvent, ClientAnnounce, EventInput
}

View File

@@ -1,49 +0,0 @@
import * as c from "./constants.js";
import * as en from "../shared_js/enums.js"
import { gc, matchOptions, clientInfo } from "./global.js";
import { wallsMovements } from "../shared_js/wallsMovement.js";
let actual_time: number = Date.now();
let last_time: number;
let delta_time: number;
function gameLoop()
{
/* last_time = actual_time;
actual_time = Date.now();
delta_time = (actual_time - last_time) / 1000; */
delta_time = c.fixedDeltaTime;
// console.log(`delta_gameLoop: ${delta_time}`);
// interpolation
// console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`);
if (clientInfo.opponent.dir.y != 0 ) {
opponentInterpolation(delta_time);
}
// client prediction
gc.ballsArr.forEach((ball) => {
ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
});
if (matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(delta_time, gc);
}
}
function opponentInterpolation(delta: number)
{
// interpolation
clientInfo.opponent.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if ((clientInfo.opponent.dir.y > 0 && clientInfo.opponent.pos.y > clientInfo.opponentNextPos.y)
|| (clientInfo.opponent.dir.y < 0 && clientInfo.opponent.pos.y < clientInfo.opponentNextPos.y))
{
clientInfo.opponent.dir.y = 0;
clientInfo.opponent.pos.y = clientInfo.opponentNextPos.y;
}
}
export {gameLoop}

View File

@@ -1,3 +0,0 @@
export {pong, gc, matchOptions} from "./pong.js"
export {socket, clientInfo} from "./ws.js"

View File

@@ -1,99 +0,0 @@
initDom();
function initDom() {
document.getElementById("play_pong_button").addEventListener("click", init);
}
import * as c from "./constants.js"
import * as en from "../shared_js/enums.js"
import { GameArea } from "./class/GameArea.js";
import { GameComponentsClient } from "./class/GameComponentsClient.js";
import { handleInput } from "./handleInput.js";
// import { sendLoop } from "./handleInput.js";
import { gameLoop } from "./gameLoop.js"
import { drawLoop } from "./draw.js";
import { countdown } from "./utils.js";
import { initWebSocket } from "./ws.js";
import { initAudio } from "./audio.js";
/* Keys
Racket: W/S OR Up/Down
Grid On-Off: G
*/
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
export let pong: GameArea;
export let gc: GameComponentsClient;
export let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
function init()
{
console.log("multi_balls:"+(<HTMLInputElement>document.getElementById("multi_balls")).checked);
console.log("moving_walls:"+(<HTMLInputElement>document.getElementById("moving_walls")).checked);
console.log("sound_on:"+(<HTMLInputElement>document.getElementById("sound_on")).checked);
let soundMutedFlag = false;
if ( (<HTMLInputElement>document.getElementById("sound_off")).checked ) {
soundMutedFlag = true;
}
initAudio(soundMutedFlag);
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
matchOptions |= en.MatchOptions.multiBalls;
}
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
matchOptions |= en.MatchOptions.movingWalls;
}
document.getElementById("div_game_options").hidden = true;
pong = new GameArea();
gc = new GameComponentsClient(matchOptions, pong.ctx);
initWebSocket(matchOptions);
}
function matchmaking()
{
console.log("Searching an opponent...");
gc.text1.clear();
gc.text1.pos.assign(c.w/5, c.h_mid);
gc.text1.text = "Searching...";
gc.text1.update();
}
function matchmakingComplete()
{
console.log("Match Found !");
gc.text1.clear();
gc.text1.pos.assign(c.w/8, c.h_mid);
gc.text1.text = "Match Found !";
gc.text1.update();
}
function startGame() {
gc.text1.pos.assign(c.w_mid, c.h_mid+c.h/4);
countdown(c.matchStartDelay/1000, (count: number) => {
gc.text1.clear();
gc.text1.text = `${count}`;
gc.text1.update();
}, resumeGame);
}
function resumeGame()
{
gc.text1.text = "";
window.addEventListener('keydown', function (e) {
pong.addKey(e.key);
});
window.addEventListener('keyup', function (e) {
pong.deleteKey(e.key);
});
pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS);
// pong.handleInputInterval = window.setInterval(sendLoop, c.sendLoopIntervalMS);
pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS);
pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
}
export {matchmaking, matchmakingComplete, startGame}

View File

@@ -1,27 +0,0 @@
import { MovingRectangle } from "./class/Rectangle.js";
function random(min: number = 0, max: number = 1) {
return Math.random() * (max - min) + min;
}
function sleep (ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function clamp(n: number, min: number, max: number) : number
{
if (n < min)
n = min;
else if (n > max)
n = max;
return (n);
}
// Typescript hack, unused
function assertMovingRectangle(value: unknown): asserts value is MovingRectangle {
// if (value !== MovingRectangle) throw new Error("Not a MovingRectangle");
return;
}
export {random, sleep, clamp, assertMovingRectangle}

View File

@@ -4,9 +4,9 @@
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title>
<title>Potato Pong</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='icon' type='image/x-icon' href='/favicon.ico'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>

View File

@@ -2,7 +2,7 @@
// routing
// may not need {link} here
import Router, { link, replace } from "svelte-spa-router";
import { primaryRoutes } from "./routes/primaryRoutes.js";
import { primaryRoutes } from "./routes/primaryRoutes.js";
// import primaryRoutes from "./routes/primaryRoutes.svelte";
const conditionsFailed = (event) => {

View File

@@ -0,0 +1,9 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
// name: 'world'
}
});
export default app;
//# sourceMappingURL=main.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAE/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC;IACnB,MAAM,EAAE,QAAQ,CAAC,IAAI;IACrB,KAAK,EAAE;IACN,gBAAgB;KAChB;CACD,CAAC,CAAC;AAEH,eAAe,GAAG,CAAC"}

View File

@@ -1,106 +0,0 @@
<script lang="ts">
// import { initDom } from "../game/client/pong.js";
import {onMount} from 'svelte';
onMount(() => {
// initDom();
})
</script>
<body>
<div id="div_game_options">
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls">
<label for="multi_balls">multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls">
<label for="moving_walls">moving walls</label>
</div>
<div>
<label>sound :</label>
<input type="radio" id="sound_on" name="sound_selector" checked>
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector">
<label for="sound_off">off</label>
</div>
<div>
<button id="play_pong_button">PLAY</button>
</div>
</fieldset>
</div>
<div id="div_game_instructions">
<h2>--- keys ---</h2>
<p>move up: 'w' or 'up arrow'</p>
<p>move down: 's' OR 'down arrow'</p>
<p>grid on/off: 'g'</p>
</div>
<div id="canvas_container">
<!-- <p> =) </p> -->
</div>
<!-- <script src="http://localhost:8080/js/pong.js" type="module" defer></script> -->
</body>
<style>
@font-face {
font-family: "Bit5x3";
src: url("/fonts/Bit5x3.woff2") format("woff2"),
url("/fonts/Bit5x3.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
body {
margin: 0;
background-color: #222425;
}
#canvas_container {
margin-top: 20px;
text-align: center;
/* border: dashed rgb(245, 245, 245) 5px; */
/* max-height: 80vh; */
/* overflow: hidden; */
}
#div_game_instructions {
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: large;
}
#div_game_options {
margin-top: 20px;
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#div_game_options fieldset {
max-width: 50vw;
width: auto;
margin: 0 auto;
}
#div_game_options fieldset div {
padding: 10px;
}
#play_pong_button {
font-family: "Bit5x3";
color: rgb(245, 245, 245);
background-color: #333333;
font-size: x-large;
padding: 10px;
}
canvas {
background-color: #333333;
max-width: 75vw;
/* max-height: 100vh; */
width: 80%;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import Header from '../pieces/Header.svelte';
import MatchListElem from "../pieces/MatchListElem.svelte";
let arr = [
{
id: "match-01",
playerOneUsername: "toto",
playerTwoUsername: "bruno",
},
{
id: "match-02",
playerOneUsername: "bertand",
playerTwoUsername: "cassandre",
},
{
id: "match-03",
playerOneUsername: "madeleine",
playerTwoUsername: "jack",
},
];
</script>
<!-- -->
<Header/>
<menu>
{#each arr as match}
<MatchListElem match={match}/>
{/each}
</menu>
<!-- -->
<style>
</style>

View File

@@ -1,88 +1,381 @@
<script>
import "public/game/pong.js"
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Header from '../../pieces/Header.svelte';
import { fade, fly } from 'svelte/transition';
import * as pong from "./client/pong";
// Pour Chérif: variables indiquant l'état du match
import { matchEnded, matchAbort } from "./client/ws";
//user's stuff
let user;
let allUsers;
//Game's stuff
let optionsAreNotSet = true;
const options = new pong.InitOptions();
//Game's stuff client side only
const gameAreaId = "game_area";
//html boolean for pages
let showWaitPage = false;
let showInvitations = false;
let showGameOption = true;
let showError = false;
let hiddenGame = true;
let showMatchEnded = false;
let isThereAnyInvitation = false;
let invitations = [];
let waitingMessage = "Please wait..."
let errorMessageWhenAttemptingToGetATicket = "";
let idOfIntevalCheckTerminationOfTheMatch;
onMount( async() => {
user = await fetch('http://transcendance:8080/api/v2/user')
.then( x => x.json() );
allUsers = await fetch('http://transcendance:8080/api/v2/user/all')
.then( x => x.json() );
options.playerOneUsername = user.username;
})
onDestroy( async() => {
options.playerOneUsername = user.username;
showError = false;
showMatchEnded = false;
optionsAreNotSet = true
options.playerTwoUsername = "";
options.isSomeoneIsInvited = false;
options.isInvitedPerson = false;
options.moving_walls = false;
options.multi_balls = false;
errorMessageWhenAttemptingToGetATicket = "";
hiddenGame = true;
isThereAnyInvitation = false;
invitations = [];
pong.destroy();
clearInterval(idOfIntevalCheckTerminationOfTheMatch);
})
const initGame = async() =>
{
optionsAreNotSet = false;
showWaitPage = true;
idOfIntevalCheckTerminationOfTheMatch = setInterval(matchTermitation, 1000);
const matchOptions = pong.computeMatchOptions(options);
const responseWhenGrantToken = fetch("http://transcendance:8080/api/v2/game/ticket", {
method : "POST",
headers : {'Content-Type': 'application/json'},
body : JSON.stringify({
playerOneUsername : options.playerOneUsername,
playerTwoUsername : options.playerTwoUsername,
gameOptions : matchOptions,
isGameIsWithInvitation : options.isSomeoneIsInvited
})
})
const responseFromServer = await responseWhenGrantToken;
const responseInjson = await responseFromServer.json();
const token : string = responseInjson.token;
showWaitPage = false;
console.log("status : " + responseFromServer.status)
if (responseFromServer.status != 200)
{
console.log(responseInjson)
console.log("On refuse le ticket");
clearInterval(idOfIntevalCheckTerminationOfTheMatch);
errorMessageWhenAttemptingToGetATicket = responseInjson.message;
showError = true;
options.playerTwoUsername = "";
options.playerOneUsername = user.username;
options.isSomeoneIsInvited = false;
options.isInvitedPerson = false;
options.moving_walls = false;
options.multi_balls = false;
setTimeout(() => {
showError = false;
showWaitPage = false
optionsAreNotSet = true
}, 5000);
}
else if (token)
{
options.isInvitedPerson = false
pong.init(options, gameAreaId, token);
hiddenGame = false;
}
}
// Pour Cherif: renommer en un truc du genre "initGameForInvitedPlayer" ?
const initGameForPrivateParty = async(invitation : any) =>
{
idOfIntevalCheckTerminationOfTheMatch = setInterval(matchTermitation, 1000);
optionsAreNotSet = false
showWaitPage = true
console.log("invitation : ")
console.log(invitation)
if (invitation.token)
{
options.playerOneUsername = invitation.playerOneUsername;
options.playerTwoUsername = invitation.playerTwoUsername;
options.isSomeoneIsInvited = true;
options.isInvitedPerson = true
pong.init(options, gameAreaId, invitation.token);
showWaitPage = false
hiddenGame = false;
}
}
const matchTermitation = () => {
console.log("Ping matchTermitation")
if (matchAbort || matchEnded)
{
clearInterval(idOfIntevalCheckTerminationOfTheMatch);
console.log("matchTermitation was called")
showWaitPage = false
matchAbort ?
errorMessageWhenAttemptingToGetATicket = "The match has been aborted"
: errorMessageWhenAttemptingToGetATicket = "The match is finished !"
matchAbort ? showError = true : showMatchEnded = true;
setTimeout(() => {
hiddenGame = true;
showError = false;
showMatchEnded = false;
optionsAreNotSet = true
options.playerTwoUsername = "";
options.playerOneUsername = user.username;
options.isSomeoneIsInvited = false;
options.isInvitedPerson = false;
options.moving_walls = false;
options.multi_balls = false;
errorMessageWhenAttemptingToGetATicket = "";
options.playerTwoUsername = "";
hiddenGame = true;
isThereAnyInvitation = false;
invitations = [];
pong.destroy();
console.log("matchTermitation : setTimeout")
}, 5000);
}
}
const showOptions = () => {
showGameOption = true
showInvitations = false
}
const showInvitation = async() => {
showGameOption = false;
showInvitations = true;
invitations = await fetch("http://transcendance:8080/api/v2/game/invitations")
.then(x => x.json())
invitations.length !== 0 ? isThereAnyInvitation = true : isThereAnyInvitation = false
}
const rejectInvitation = async(invitation) => {
await fetch("http://transcendance:8080/api/v2/game/decline",{
method: "POST",
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
token : invitation.token
})
})
.then(x => x.json())
.catch(error => console.log(error))
showInvitation()
}
const acceptInvitation = async(invitation : any) => {
const res = await fetch("http://transcendance:8080/api/v2/game/accept",{
method: "POST",
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
token : invitation.token
})
})
.then(x => x.json())
.catch(error => {
console.log(error)
})
if (res.status === 200)
{
showInvitation()
initGameForPrivateParty(invitation)
}
//Au final c'est utile !
initGameForPrivateParty(invitation)
}
</script>
<Header />
<!-- <div id="game_page"> Replacement for <body>.
Might become useless after CSS rework. -->
<div id="game_page">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="preload_font">.</div>
<div id="div_game_options">
{#if showMatchEnded === true}
<div id="div_game" in:fly="{{ y: 10, duration: 1000 }}">
<p>{errorMessageWhenAttemptingToGetATicket}</p>
</div>
{/if}
{#if showError === true}
<div id="div_game" in:fly="{{ y: 10, duration: 1000 }}">
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls">
<label for="multi_balls">multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls">
<label for="moving_walls">moving walls</label>
</div>
<div>
<label>sound :</label>
<input type="radio" id="sound_on" name="sound_selector" checked>
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector">
<label for="sound_off">off</label>
</div>
<div>
<button id="play_pong_button">PLAY</button>
</div>
<legend>Error</legend>
<p>{errorMessageWhenAttemptingToGetATicket}</p>
</fieldset>
</div>
{/if}
<div id="canvas_container" hidden={hiddenGame}>
<canvas id={gameAreaId}/>
</div>
<div id="canvas_container">
<!-- <p> =) </p> -->
</div>
{#if showWaitPage === true}
<div id="div_game" in:fly="{{ y: 10, duration: 1000 }}">
<fieldset>
<legend>Connecting to the game...</legend>
<p>{waitingMessage}</p>
</fieldset>
</div>
{/if}
<script src="public/game/pong.js" type="module" defer></script>
</body>
{#if optionsAreNotSet}
{#if showGameOption === true}
<div id="game_option">
<form on:submit|preventDefault={() => initGame()}>
<div id="div_game">
<button id="pong_button" on:click={showInvitation}>Show invitations</button>
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls" bind:checked={options.multi_balls}>
<label for="multi_balls">Multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls" bind:checked={options.moving_walls}>
<label for="moving_walls">Moving walls</label>
</div>
<div>
<p>sound :</p>
<input type="radio" id="sound_on" name="sound_selector" bind:group={options.sound} value="on">
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector" bind:group={options.sound} value="off">
<label for="sound_off">off</label>
</div>
<div>
<input type="checkbox" id="isSomeoneIsInvited" bind:checked={options.isSomeoneIsInvited}>
<label for="moving_walls">Invite a friend</label>
</div>
{#if options.isSomeoneIsInvited === true}
<select bind:value={options.playerTwoUsername}>
{#each allUsers as user }
<option value={user.username}>{user.username}</option>
{/each}
</select>
{/if}
<div>
<button id="pong_button" >PLAY</button>
</div>
</fieldset>
</div>
</form>
</div>
{/if}
{#if showInvitations}
<div id="invitations_options" in:fly="{{ y: 10, duration: 1000 }}">
<div id="div_game">
<button id="pong_button" on:click={showOptions}>Play a Game</button>
<fieldset>
<legend>Current invitation(s)</legend>
{#if isThereAnyInvitation}
{#each invitations as invitation }
<div>
{invitation.playerOneUsername} has invited you to play a pong !
<button id="pong_button" on:click={() => acceptInvitation(invitation)}>V</button>
<button id="pong_button" on:click={() => rejectInvitation(invitation)}>X</button>
</div>
{/each}
{/if}
{#if isThereAnyInvitation === false}
<p>Currently, no one asked to play with you.</p>
<button id="pong_button" on:click={showInvitation}>Reload</button>
{/if}
</fieldset>
</div>
</div>
{/if}
{/if}
</div> <!-- div "game_page" -->
<style>
@font-face {
font-family: "Bit5x3";
src: url("/fonts/Bit5x3.woff2") format("woff2"),local("Bit5x3"), url("/fonts/Bit5x3.woff") format("woff");
src:
url("/fonts/Bit5x3.woff2") format("woff2"),
local("Bit5x3"),
url("/fonts/Bit5x3.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap;
}
#preload_font {
font-family: "Bit5x3";
opacity:0;
height:0;
width:0;
display:inline-block;
}
body {
#game_page {
margin: 0;
background-color: #222425;
position: relative;
width: 100%;
height: 100%;
}
#canvas_container {
margin-top: 20px;
text-align: center;
/* border: dashed rgb(245, 245, 245) 5px; */
/* max-height: 80vh; */
/* overflow: hidden; */
}
#div_game_options {
#users_name {
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#div_game_options fieldset {
#div_game {
margin-top: 20px;
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#error_notification {
text-align: center;
display: block;
font-family: "Bit5x3";
color: rgb(143, 19, 19);
font-size: x-large;
}
#div_game fieldset {
max-width: 50vw;
width: auto;
margin: 0 auto;
}
#div_game_options fieldset div {
#div_game fieldset div {
padding: 10px;
}
#play_pong_button {
#pong_button {
font-family: "Bit5x3";
color: rgb(245, 245, 245);
background-color: #333333;
@@ -90,6 +383,7 @@ body {
padding: 10px;
}
canvas {
/* background-color: #ff0000; */
background-color: #333333;
max-width: 75vw;
/* max-height: 100vh; */

Some files were not shown because too many files have changed in this diff Show More