diff --git a/JEU/jsconfig.json b/JEU/jsconfig.json new file mode 100644 index 00000000..347bf03f --- /dev/null +++ b/JEU/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + "target": "ES2020", + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file diff --git a/JEU/make.sh b/JEU/make.sh new file mode 100644 index 00000000..ebce024a --- /dev/null +++ b/JEU/make.sh @@ -0,0 +1,18 @@ +#!/bin/bash +npx tsc + +mkdir -p www +cp ./src/client/*.html ./www/ +cp ./src/client/*.css ./www/ + +mkdir -p www/js +cp ./src/client/*.js ./www/js/ + +mkdir -p www/js/class +cp ./src/client/class/*.js ./www/js/class/ + +mkdir -p www/shared_js/ +cp ./src/shared_js/*.js ./www/shared_js/ + +mkdir -p www/shared_js/class +cp ./src/shared_js/class/*.js ./www/shared_js/class/ diff --git a/JEU/memo.txt b/JEU/memo.txt new file mode 100644 index 00000000..20d493f3 --- /dev/null +++ b/JEU/memo.txt @@ -0,0 +1,45 @@ +Done: + - Connexion client/serveur via un Websocket + - implémentation basique (authoritative server) + - Matchmaking + - client prediction + - server reconciliation (buffer des inputs côté client + id sur les inputs) + - amélioration collision avec Hugo + - du son (rebonds de la balle, "Oof" de Roblox sur un point) + - init de GameComponents partagé entre serveur et client. + - draw on the canvas "WIN", "LOSE", "MATCHMAKING COMPLETE", ... + - interpolation (mis à jour progressif des mouvements de l'adversaire) + - traitement groupé des inputs clients toutes les x millisecondes + (BUG désynchronisation: revenu à un traitement immédiat en attendant) + - Détruire les GameSession une fois finies. + - mode multi-balles + - mode murs mouvant (la zone de jeu rétréci / agrandi en continu) + - Selection des modes de jeu via HTML + - Selection audio on/off via HTML + +TODO: +- Match Abort si tout les joueurs ne sont pas pret assez vite (~15 secondes) +- mode spectateur +- certaines utilisations de Math.floor() superflu ? Vérifier les appels. + (éventuellement Math.round() ?) +- un autre mode de jeu alternatif ? +- changer les "localhost:8080" dans le code. +- sélection couleur des raquettes (your color/opponent color) dans le profil utilisateur. + Enregistrement dans la DB. + init des couleurs dans GameComponentsClient() basé sur les variables de l'utilsateur connecté. +----------- +idées modes de jeu : + - mode 2 raquettes (un joueur haut/gauche et bas/droite) + - skin patate ??? +- (prediction de l'avancement de la balle basé sur la latence serveur ?) +- d'autres sons (foule qui applaudi/musique de victoire) +----------- +- BUG: Si la balle va très vite, elle peut ignorer la collision avec une raquette ou mur. +la collision est testée seulement après le mouvement. +Pour éviter ce bug il faudrait diviser le mouvement pour faire plusieurs tests de collision successifs. +- BUG mineur: sur un changement de fenêtre, les touches restent enfoncées et il faut les "décoincer" +en réappuyant. Ce n'est pas grave mais peut-on faire mieux ? +---------- +OSEF, rebuts: +- reconnection +- amélioration du protocole, remplacement du JSON (compression. moins de bande passante). diff --git a/JEU/package-lock.json b/JEU/package-lock.json new file mode 100644 index 00000000..807c07eb --- /dev/null +++ b/JEU/package-lock.json @@ -0,0 +1,121 @@ +{ + "name": "ft_transcendence", + "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.8.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.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "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.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "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": {} + } + } +} diff --git a/JEU/package.json b/JEU/package.json new file mode 100644 index 00000000..f8f23534 --- /dev/null +++ b/JEU/package.json @@ -0,0 +1,13 @@ +{ + "type": "module", + "devDependencies": { + "@types/node": "^18.11.5", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.5.3", + "typescript": "^4.8.4" + }, + "dependencies": { + "uuid": "^9.0.0", + "ws": "^8.10.0" + } +} diff --git a/JEU/src/client/audio.ts b/JEU/src/client/audio.ts new file mode 100644 index 00000000..74c73336 --- /dev/null +++ b/JEU/src/client/audio.ts @@ -0,0 +1,16 @@ + +import * as c from "./constants.js" + +export const soundPongArr: HTMLAudioElement[] = []; +export const soundRoblox = new Audio("http://localhost:8080/sound/roblox-oof.ogg"); + +export function initAudio(muteFlag: boolean) +{ + for (let i = 0; i <= 32; i++) { + soundPongArr.push(new Audio("http://localhost:8080/sound/pong/"+i+".ogg")); + soundPongArr[i].volume = c.soundPongVolume; + soundPongArr[i].muted = muteFlag; + } + soundRoblox.volume = c.soundRobloxVolume; + soundRoblox.muted = muteFlag; +} diff --git a/JEU/src/client/class/GameArea.ts b/JEU/src/client/class/GameArea.ts new file mode 100644 index 00000000..e6921e4e --- /dev/null +++ b/JEU/src/client/class/GameArea.ts @@ -0,0 +1,38 @@ + +import * as c from ".././constants.js" + +class GameArea { + keys: string[] = []; + handleInputInterval: number = 0; + gameLoopInterval: number = 0; + drawLoopInterval: number = 0; + canvas: HTMLCanvasElement; + ctx: CanvasRenderingContext2D; + constructor() { + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D; + this.canvas.width = c.CanvasWidth; + this.canvas.height = c.CanvasWidth / c.CanvasRatio; + let container = document.getElementById("canvas_container"); + if (container) + container.insertBefore(this.canvas, container.childNodes[0]); + } + addKey(key: string) { + key = key.toLowerCase(); + var i = this.keys.indexOf(key); + if (i == -1) + this.keys.push(key); + } + deleteKey(key: string) { + key = key.toLowerCase(); + var i = this.keys.indexOf(key); + if (i != -1) { + this.keys.splice(i, 1); + } + } + clear() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } +} + +export {GameArea} diff --git a/JEU/src/client/class/GameComponentsClient.ts b/JEU/src/client/class/GameComponentsClient.ts new file mode 100644 index 00000000..bf90f66f --- /dev/null +++ b/JEU/src/client/class/GameComponentsClient.ts @@ -0,0 +1,114 @@ + +import * as c from "../constants.js" +import * as en from "../../shared_js/enums.js" +import { Vector, VectorInteger } from "../../shared_js/class/Vector.js"; +import { TextElem, TextNumericValue } from "./Text.js"; +import { RectangleClient, MovingRectangleClient, RacketClient, BallClient, Line } from "./RectangleClient.js"; +import { GameComponents } from "../../shared_js/class/GameComponents.js"; +import { MovingRectangle } from "../../shared_js/class/Rectangle.js"; + +class GameComponentsExtensionForClient extends GameComponents { + wallTop: RectangleClient | MovingRectangleClient; + wallBottom: RectangleClient | MovingRectangleClient; + playerLeft: RacketClient; + playerRight: RacketClient; + ballsArr: BallClient[]; + constructor(options: en.MatchOptions, ctx: CanvasRenderingContext2D) + { + super(options); + + // Rackets + const basePL = this.playerLeft; + const basePR = this.playerRight; + this.playerLeft = new RacketClient( + basePL.pos, basePL.width, basePL.height, basePL.baseSpeed, + ctx, "white"); + this.playerRight = new RacketClient( + basePR.pos, basePR.width, basePR.height, basePR.baseSpeed, + ctx, "white"); + + // Balls + const newBallsArr: BallClient[] = []; + this.ballsArr.forEach((ball) => { + newBallsArr.push(new BallClient(ball.pos, ball.width, ball.baseSpeed, ball.speedIncrease, + ctx, "white") + ); + }); + this.ballsArr = newBallsArr; + + // Walls + if (options & en.MatchOptions.movingWalls) + { + const baseWT = this.wallTop; + const baseWB = this.wallBottom; + + this.wallTop = new MovingRectangleClient(baseWT.pos, baseWT.width, baseWT.height, baseWT.baseSpeed, + ctx, "grey"); + (this.wallTop).dir.assign(baseWT.dir.x, baseWT.dir.y); + + this.wallBottom = new MovingRectangleClient(baseWB.pos, baseWB.width, baseWB.height, baseWB.baseSpeed, + ctx, "grey"); + (this.wallBottom).dir.assign(baseWB.dir.x, baseWB.dir.y); + } + else + { + const baseWT = this.wallTop; + const baseWB = this.wallBottom; + this.wallTop = new RectangleClient(baseWT.pos, baseWT.width, baseWT.height, + ctx, "grey"); + this.wallBottom = new RectangleClient(baseWB.pos, baseWB.width, baseWB.height, + ctx, "grey"); + } + } +} + + +class GameComponentsClient extends GameComponentsExtensionForClient { + midLine: Line; + scoreLeft: TextNumericValue; + scoreRight: TextNumericValue; + text1: TextElem; + + w_grid_mid: RectangleClient; + w_grid_u1: RectangleClient; + w_grid_d1: RectangleClient; + h_grid_mid: RectangleClient; + h_grid_u1: RectangleClient; + h_grid_d1: RectangleClient; + constructor(options: en.MatchOptions, ctx: CanvasRenderingContext2D) + { + super(options, ctx); + let pos = new VectorInteger; + // Scores + pos.assign(c.w_mid-c.scoreSize*1.6, c.scoreSize*1.5); + this.scoreLeft = new TextNumericValue(pos, c.scoreSize, ctx, "white"); + pos.assign(c.w_mid+c.scoreSize*1.1, c.scoreSize*1.5); + this.scoreRight = new TextNumericValue(pos, c.scoreSize, ctx, "white"); + this.scoreLeft.value = 0; + this.scoreRight.value = 0; + + // Text + pos.assign(0, c.h_mid); + this.text1 = new TextElem(pos, Math.floor(c.w/8), ctx, "white"); + + // Dotted Midline + pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize); + this.midLine = new Line(pos, c.midLineSize, c.h-c.wallSize*2, ctx, "white", 15); + + // Grid + pos.assign(0, c.h_mid); + this.w_grid_mid = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); + pos.assign(0, c.h/4); + this.w_grid_u1 = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); + pos.assign(0, c.h-c.h/4); + this.w_grid_d1 = new RectangleClient(pos, c.w, c.gridSize, ctx, "darkgreen"); + pos.assign(c.w_mid, 0); + this.h_grid_mid = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); + pos.assign(c.w/4, 0); + this.h_grid_u1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); + pos.assign(c.w-c.w/4, 0); + this.h_grid_d1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); + } +} + +export {GameComponentsClient} diff --git a/JEU/src/client/class/InputHistory.ts b/JEU/src/client/class/InputHistory.ts new file mode 100644 index 00000000..e4d3b8f1 --- /dev/null +++ b/JEU/src/client/class/InputHistory.ts @@ -0,0 +1,16 @@ + +import * as en from "../../shared_js/enums.js" +import * as ev from "../../shared_js/class/Event.js" + +class InputHistory { + input: en.InputEnum; + id: number; + deltaTime: number; + constructor(inputState: ev.EventInput, deltaTime: number) { + this.input = inputState.input; + this.id = inputState.id; + this.deltaTime = deltaTime; + } +} + +export {InputHistory} diff --git a/JEU/src/client/class/RectangleClient.ts b/JEU/src/client/class/RectangleClient.ts new file mode 100644 index 00000000..5c251704 --- /dev/null +++ b/JEU/src/client/class/RectangleClient.ts @@ -0,0 +1,141 @@ + +import { Vector, VectorInteger } from "../../shared_js/class/Vector.js"; +import { Component, GraphicComponent, Moving } from "../../shared_js/class/interface.js"; +import { Rectangle, MovingRectangle, Racket, Ball } from "../../shared_js/class/Rectangle.js"; +import { soundPongArr } from "../audio.js" +import { random } from "../utils.js"; + +function updateRectangle(this: RectangleClient) { + this.ctx.fillStyle = this.color; + this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height); +} + +function clearRectangle(this: RectangleClient, pos?: VectorInteger) { + if (pos) + this.ctx.clearRect(pos.x, pos.y, this.width, this.height); + else + this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); +} + +class RectangleClient extends Rectangle implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } + // update() { + // this.ctx.fillStyle = this.color; + // this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height); + // } + // clear(pos?: VectorInteger) { + // if (pos) + // this.ctx.clearRect(pos.x, pos.y, this.width, this.height); + // else + // this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); + // } +} + +class MovingRectangleClient extends MovingRectangle implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height, baseSpeed); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } +} + +class RacketClient extends Racket implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, width, height, baseSpeed); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } +} + +class BallClient extends Ball implements GraphicComponent { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; + constructor(pos: VectorInteger, size: number, baseSpeed: number, speedIncrease: number, + ctx: CanvasRenderingContext2D, color: string) + { + super(pos, size, baseSpeed, speedIncrease); + this.ctx = ctx; + this.color = color; + this.update = updateRectangle; + this.clear = clearRectangle; + } + bounce(collider?: Rectangle) { + this._bounceAlgo(collider); + soundPongArr[ Math.floor(random(0, soundPongArr.length)) ].play(); + } + /* protected _bounceRacket(collider: Racket) { + this._bounceRacketAlgo(collider); + soundRoblox.play(); + } */ +} + +function updateLine(this: Line) { + this.ctx.fillStyle = this.color; + let pos: VectorInteger = new VectorInteger; + let i = 0; + while (i < this.segmentCount) + { + // for Horizontal Line + // pos.y = this.pos.y; + // pos.x = this.pos.x + this.segmentWidth * i; + pos.x = this.pos.x; + pos.y = this.pos.y + this.segmentHeight * i; + this.ctx.fillRect(pos.x, pos.y, this.segmentWidth, this.segmentHeight); + i += 2; + } +} + +class Line extends RectangleClient { + gapeCount: number = 0; + segmentCount: number; + segmentWidth: number; + segmentHeight: number; + constructor(pos: VectorInteger, width: number, height: number, + ctx: CanvasRenderingContext2D, color: string, gapeCount?: number) + { + super(pos, width, height, ctx, color); + this.update = updateLine; + if (gapeCount) + this.gapeCount = gapeCount; + this.segmentCount = this.gapeCount * 2 + 1; + + this.segmentWidth = this.width; + this.segmentHeight = this.height / this.segmentCount; + + // for Horizontal Line + // this.segmentWidth = this.width / this.segmentCount; + // this.segmentHeight = this.height; + } +} + +export {RectangleClient, MovingRectangleClient, RacketClient, BallClient, Line} diff --git a/JEU/src/client/class/Text.ts b/JEU/src/client/class/Text.ts new file mode 100644 index 00000000..88111131 --- /dev/null +++ b/JEU/src/client/class/Text.ts @@ -0,0 +1,58 @@ + +import { Vector, VectorInteger } from "../../shared_js/class/Vector.js"; +import { Component } from "../../shared_js/class/interface.js"; + +// conflict with Text +class TextElem implements Component { + ctx: CanvasRenderingContext2D; + pos: VectorInteger; + color: string; + size: number; + font: string; + text: string = ""; + constructor(pos: VectorInteger, size: number, + ctx: CanvasRenderingContext2D, color: string, font: string = "Bit5x3") + { + // this.pos = Object.assign({}, pos); // create bug, Uncaught TypeError: X is not a function + this.pos = new VectorInteger(pos.x, pos.y); + this.size = size; + this.ctx = ctx; + this.color = color; + this.font = font; + } + update() { + this.ctx.font = this.size + "px" + " " + this.font; + this.ctx.fillStyle = this.color; + this.ctx.fillText(this.text, this.pos.x, this.pos.y); + } + clear() { + // clear no very accurate for Text + // https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics + let textMetric = this.ctx.measureText(this.text); + // console.log("textMetric.width = "+textMetric.width); + // console.log("size = "+this.size); + // console.log("x = "+this.pos.x); + // console.log("y = "+this.pos.y); + this.ctx.clearRect(this.pos.x - 1, this.pos.y-this.size + 1, textMetric.width, this.size); + // +1 and -1 because float imprecision (and Math.floor() with VectorInteger dont work for the moment) + // (or maybe its textMetric imprecision ?) + } +} + +class TextNumericValue extends TextElem { + private _value: number = 0; + constructor(pos: VectorInteger, size: number, + ctx: CanvasRenderingContext2D, color: string, font?: string) + { + super(pos, size, ctx, color, font); + } + get value() { + return this._value; + } + set value(v: number) { + this._value = v; + this.text = v.toString(); + } +} + +export {TextElem, TextNumericValue} diff --git a/JEU/src/client/constants.ts b/JEU/src/client/constants.ts new file mode 100644 index 00000000..97bae265 --- /dev/null +++ b/JEU/src/client/constants.ts @@ -0,0 +1,18 @@ + +import { w } from "../shared_js/constants.js" +export * from "../shared_js/constants.js" + +export const midLineSize = Math.floor(w/150); +export const scoreSize = Math.floor(w/16); +export const gridSize = Math.floor(w/500); + +// min interval on Firefox seems to be 15. Chrome can go lower. +export const handleInputIntervalMS = 15; // millisecond +export const sendLoopIntervalMS = 15; // millisecond // unused +export const gameLoopIntervalMS = 15; // millisecond +export const drawLoopIntervalMS = 15; // millisecond + +export const fixedDeltaTime = gameLoopIntervalMS/1000; // second + +export const soundRobloxVolume = 0.3; // between 0 and 1 +export const soundPongVolume = 0.3; // between 0 and 1 diff --git a/JEU/src/client/draw.ts b/JEU/src/client/draw.ts new file mode 100644 index 00000000..984c2acb --- /dev/null +++ b/JEU/src/client/draw.ts @@ -0,0 +1,51 @@ + +import { pong, gc } from "./global.js" +import * as c from "./constants.js" +import * as en from "../shared_js/enums.js" +import { gridDisplay } from "./handleInput.js"; + +function drawLoop() +{ + pong.clear(); + + if (gridDisplay) { + drawGrid(); + } + + drawStatic(); + + gc.text1.update(); + + drawDynamic(); +} + +function drawDynamic() +{ + gc.scoreLeft.update(); + gc.scoreRight.update(); + gc.playerLeft.update(); + gc.playerRight.update(); + gc.ballsArr.forEach((ball) => { + ball.update(); + }); +} + +function drawStatic() +{ + gc.midLine.update(); + gc.wallTop.update(); + gc.wallBottom.update(); +} + +function drawGrid() +{ + gc.w_grid_mid.update(); + gc.w_grid_u1.update(); + gc.w_grid_d1.update(); + + gc.h_grid_mid.update(); + gc.h_grid_u1.update(); + gc.h_grid_d1.update(); +} + +export {drawLoop} diff --git a/JEU/src/client/gameLoop.ts b/JEU/src/client/gameLoop.ts new file mode 100644 index 00000000..593d1eaa --- /dev/null +++ b/JEU/src/client/gameLoop.ts @@ -0,0 +1,49 @@ + +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} diff --git a/JEU/src/client/global.ts b/JEU/src/client/global.ts new file mode 100644 index 00000000..7d0a7126 --- /dev/null +++ b/JEU/src/client/global.ts @@ -0,0 +1,3 @@ + +export {pong, gc, matchOptions} from "./pong.js" +export {socket, clientInfo} from "./ws.js" diff --git a/JEU/src/client/handleInput.ts b/JEU/src/client/handleInput.ts new file mode 100644 index 00000000..164680e1 --- /dev/null +++ b/JEU/src/client/handleInput.ts @@ -0,0 +1,110 @@ + +import { pong, gc, socket, clientInfo } from "./global.js" +import * as ev from "../shared_js/class/Event.js" +import * as en from "../shared_js/enums.js" +import { InputHistory } from "./class/InputHistory.js" +import * as c from "./constants.js"; + +export let gridDisplay = false; + +let actual_time: number = Date.now(); +let last_time: number; +let delta_time: number; + +const inputState: ev.EventInput = new ev.EventInput(); +const inputHistoryArr: InputHistory[] = []; + +// test +/* export function sendLoop() +{ + socket.send(JSON.stringify(inputState)); +} */ + +function handleInput() +{ + /* last_time = actual_time; + actual_time = Date.now(); + delta_time = (actual_time - last_time) / 1000; */ + + delta_time = c.fixedDeltaTime; + // console.log(`delta_time: ${delta_time}`); + + inputState.id = Date.now(); + inputState.input = en.InputEnum.noInput; + + const keys = pong.keys; + if (keys.length !== 0) + { + if (keys.indexOf("g") != -1) + { + gridDisplay = !gridDisplay; + pong.deleteKey("g"); + } + playerMovements(delta_time, keys); + } + + socket.send(JSON.stringify(inputState)); + // setTimeout(testInputDelay, 100); + inputHistoryArr.push(new InputHistory(inputState, delta_time)); + + // client prediction + if (inputState.input !== en.InputEnum.noInput) { + // TODO: peut-etre le mettre dans game loop ? + // Attention au delta time dans ce cas ! + playerMovePrediction(delta_time, inputState.input); + } +} + +function playerMovements(delta: number, keys: string[]) +{ + if (keys.indexOf("w") !== -1 || keys.indexOf("ArrowUp".toLowerCase()) !== -1) + { + if (keys.indexOf("s") === -1 && keys.indexOf("ArrowDown".toLowerCase()) === -1) { + inputState.input = en.InputEnum.up; + } + } + else if (keys.indexOf("s") !== -1 || keys.indexOf("ArrowDown".toLowerCase()) !== -1) { + inputState.input = en.InputEnum.down; + } +} + +function testInputDelay() { + socket.send(JSON.stringify(inputState)); +} + + +function playerMovePrediction(delta: number, input: en.InputEnum) +{ + // client prediction + const racket = clientInfo.racket; + if (input === en.InputEnum.up) { + racket.dir.y = -1; + } + else if (input === en.InputEnum.down) { + racket.dir.y = 1; + } + racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]); +} + +function repeatInput(lastInputId: number) +{ + // server reconciliation + let i = inputHistoryArr.findIndex((value: InputHistory) => { + if (value.id === lastInputId) { + return true; + } + return false; + }); + + // console.log(`inputHistory total: ${inputHistoryArr.length}` ); + inputHistoryArr.splice(0, i+1); + // console.log(`inputHistory left: ${inputHistoryArr.length}` ); + + inputHistoryArr.forEach((value: InputHistory) => { + if (value.input !== en.InputEnum.noInput) { + playerMovePrediction(value.deltaTime, value.input); + } + }); +} + +export {handleInput, repeatInput} diff --git a/JEU/src/client/pong.css b/JEU/src/client/pong.css new file mode 100644 index 00000000..c481c502 --- /dev/null +++ b/JEU/src/client/pong.css @@ -0,0 +1,53 @@ + +@font-face { + font-family: "Bit5x3"; + src: url("http://localhost:8080/Bit5x3.woff2") format("woff2"), + url("http://localhost:8080/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 { + margin: 0; + background-color: #222425; +} +#canvas_container { + text-align: center; + /* border: dashed rgb(245, 245, 245) 5px; */ + /* max-height: 80vh; */ + /* overflow: hidden; */ +} +#div_game_options { + 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%; +} diff --git a/JEU/src/client/pong.html b/JEU/src/client/pong.html new file mode 100644 index 00000000..d23e5590 --- /dev/null +++ b/JEU/src/client/pong.html @@ -0,0 +1,42 @@ + + + + + + + + +
.
+ +
+
+ game options +
+ + +
+
+ + +
+
+ + + + + +
+
+ +
+
+
+ +
+ +
+ + + + + diff --git a/JEU/src/client/pong.ts b/JEU/src/client/pong.ts new file mode 100644 index 00000000..5c9dcce0 --- /dev/null +++ b/JEU/src/client/pong.ts @@ -0,0 +1,99 @@ + +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:"+(document.getElementById("multi_balls")).checked); + console.log("moving_walls:"+(document.getElementById("moving_walls")).checked); + console.log("sound_on:"+(document.getElementById("sound_on")).checked); + + let soundMutedFlag = false; + if ( (document.getElementById("sound_off")).checked ) { + soundMutedFlag = true; + } + initAudio(soundMutedFlag); + + if ( (document.getElementById("multi_balls")).checked ) { + matchOptions |= en.MatchOptions.multiBalls; + } + if ( (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} diff --git a/JEU/src/client/utils.ts b/JEU/src/client/utils.ts new file mode 100644 index 00000000..db971447 --- /dev/null +++ b/JEU/src/client/utils.ts @@ -0,0 +1,18 @@ + +export * from "../shared_js/utils.js" + +function countdown(count: number, callback?: (count: number) => void, endCallback?: () => void) +{ + console.log("countdown ", count); + if (count > 0) { + if (callback) { + callback(count); + } + setTimeout(countdown, 1000, --count, callback, endCallback); + } + else if (endCallback) { + endCallback(); + } +} + +export {countdown} diff --git a/JEU/src/client/ws.ts b/JEU/src/client/ws.ts new file mode 100644 index 00000000..60f1ab87 --- /dev/null +++ b/JEU/src/client/ws.ts @@ -0,0 +1,182 @@ + +import * as c from "./constants.js" +import { gc, matchOptions } from "./global.js" +import * as ev from "../shared_js/class/Event.js" +import * as en from "../shared_js/enums.js" +import { matchmaking, matchmakingComplete, startGame } from "./pong.js"; +import { RacketClient } from "./class/RectangleClient.js"; +import { repeatInput } from "./handleInput.js"; +import { soundRoblox } from "./audio.js" +import { sleep } from "./utils.js"; +import { Vector, VectorInteger } from "../shared_js/class/Vector.js"; + +class ClientInfo { + id = ""; + side: en.PlayerSide; + racket: RacketClient; + opponent: RacketClient; + opponentNextPos: VectorInteger; +} + +const wsPort = 8042; +const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; +export let socket: WebSocket; /* TODO: A way to still use "const" not "let" ? */ +export const clientInfo = new ClientInfo(); + +export function initWebSocket(options: en.MatchOptions) +{ + socket = new WebSocket(wsUrl, "json"); + socket.addEventListener("open", (event) => { + socket.send(JSON.stringify( new ev.ClientAnnounce(en.ClientRole.player, options, clientInfo.id) )); + }); + // socket.addEventListener("message", logListener); // for testing purpose + socket.addEventListener("message", preMatchListener); +} + +function logListener(this: WebSocket, event: MessageEvent) { + console.log("%i: " + event.data, Date.now()); +} + +function preMatchListener(this: WebSocket, event: MessageEvent) +{ + const data: ev.ServerEvent = JSON.parse(event.data); + switch (data.type) { + case en.EventTypes.assignId: + clientInfo.id = (data).id; + break; + case en.EventTypes.matchmakingInProgress: + matchmaking(); + break; + case en.EventTypes.matchmakingComplete: + clientInfo.side = (data).side; + if (clientInfo.side === en.PlayerSide.left) + { + clientInfo.racket = gc.playerLeft; + clientInfo.opponent = gc.playerRight; + } + else if (clientInfo.side === en.PlayerSide.right) + { + clientInfo.racket = gc.playerRight; + clientInfo.opponent = gc.playerLeft; + } + clientInfo.opponentNextPos = new VectorInteger(clientInfo.opponent.pos.x, clientInfo.opponent.pos.y); + clientInfo.racket.color = "darkgreen"; // for testing purpose + socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) )); // TODO: set an interval/timeout to resend until matchStart response (in case of network problem) + matchmakingComplete(); + break; + case en.EventTypes.matchStart: + socket.removeEventListener("message", preMatchListener); + socket.addEventListener("message", inGameListener); + startGame(); + break; + } +} + +function inGameListener(event: MessageEvent) +{ + const data: ev.ServerEvent = JSON.parse(event.data); + switch (data.type) { + case en.EventTypes.gameUpdate: + // setTimeout(gameUpdate, 500, data as ev.EventGameUpdate); // artificial latency for testing purpose + gameUpdate(data as ev.EventGameUpdate); + break; + case en.EventTypes.scoreUpdate: + scoreUpdate(data as ev.EventScoreUpdate); + break; + case en.EventTypes.matchEnd: + matchEnd(data as ev.EventMatchEnd); + break; + } +} + +function gameUpdate(data: ev.EventGameUpdate) +{ + console.log("gameUpdate"); + + if (matchOptions & en.MatchOptions.movingWalls) { + gc.wallTop.pos.y = data.wallTop.y; + gc.wallBottom.pos.y = data.wallBottom.y; + } + + data.ballsArr.forEach((ball, i) => { + gc.ballsArr[i].pos.assign(ball.x, ball.y); + gc.ballsArr[i].dir.assign(ball.dirX, ball.dirY); + gc.ballsArr[i].speed = ball.speed; + }); + /* // Equivalent to + gc.ballsArr.forEach((ball, i) => { + ball.pos.assign(data.ballsArr[i].x, data.ballsArr[i].y); + ball.dir.assign(data.ballsArr[i].dirX, data.ballsArr[i].dirY); + ball.speed = data.ballsArr[i].speed; + }); */ + + const predictionPos = new VectorInteger(clientInfo.racket.pos.x, clientInfo.racket.pos.y); // debug + + if (clientInfo.side === en.PlayerSide.left) { + clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerLeft.y); + } + else if (clientInfo.side === en.PlayerSide.right) { + clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerRight.y); + } + + // interpolation + clientInfo.opponent.pos.assign(clientInfo.opponentNextPos.x, clientInfo.opponentNextPos.y); + if (clientInfo.side === en.PlayerSide.left) { + clientInfo.opponentNextPos.assign(clientInfo.opponent.pos.x, data.playerRight.y); + } + else if (clientInfo.side === en.PlayerSide.right) { + clientInfo.opponentNextPos.assign(clientInfo.opponent.pos.x, data.playerLeft.y); + } + + clientInfo.opponent.dir = new Vector( + clientInfo.opponentNextPos.x - clientInfo.opponent.pos.x, + clientInfo.opponentNextPos.y - clientInfo.opponent.pos.y + ); + + if (Math.abs(clientInfo.opponent.dir.x) + Math.abs(clientInfo.opponent.dir.y) !== 0) { + clientInfo.opponent.dir = clientInfo.opponent.dir.normalized(); + } + + // server reconciliation + repeatInput(data.lastInputId); + + // debug + if (clientInfo.racket.pos.y > predictionPos.y + 1 + || clientInfo.racket.pos.y < predictionPos.y - 1) + { + console.log( + `Reconciliation error: + server y: ${data.playerLeft.y} + reconciliation y: ${clientInfo.racket.pos.y} + prediction y: ${predictionPos.y}` + ); + } +} + +function scoreUpdate(data: ev.EventScoreUpdate) +{ + // console.log("scoreUpdate"); + if (clientInfo.side === en.PlayerSide.left && data.scoreRight > gc.scoreRight.value) { + soundRoblox.play(); + } + else if (clientInfo.side === en.PlayerSide.right && data.scoreLeft > gc.scoreLeft.value) { + soundRoblox.play(); + } + gc.scoreLeft.value = data.scoreLeft; + gc.scoreRight.value = data.scoreRight; +} + +function matchEnd(data: ev.EventMatchEnd) +{ + if (data.winner === clientInfo.side) { + gc.text1.pos.assign(c.w*0.415, c.h_mid); + gc.text1.text = "WIN"; + } + else { + gc.text1.pos.assign(c.w*0.383, c.h_mid); + gc.text1.text = "LOSE"; + } + // matchEnded = true; +} + +// export let matchEnded = false; diff --git a/JEU/src/server/class/Client.ts b/JEU/src/server/class/Client.ts new file mode 100644 index 00000000..fbdd8fd3 --- /dev/null +++ b/JEU/src/server/class/Client.ts @@ -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" + +class Client { + socket: WebSocket; + id: string; // Pas indispensable si "socket" a une copie de "id" + isAlive: boolean = true; + gameSession: GameSession = null; + matchOptions: en.MatchOptions = 0; + constructor(socket: WebSocket, id: string) { + this.socket = socket; + this.id = id; + } +} + +class ClientPlayer extends Client { + inputBuffer: ev.EventInput = new ev.EventInput(); + lastInputId: number = 0; + racket: Racket; + constructor(socket: WebSocket, id: string, racket: Racket) { + super(socket, id); + this.racket = racket; + } +} + +class ClientSpectator extends Client { // Wip, unused + constructor(socket: WebSocket, id: string) { + super(socket, id); + } +} + +export {Client, ClientPlayer, ClientSpectator} diff --git a/JEU/src/server/class/GameComponentsServer.ts b/JEU/src/server/class/GameComponentsServer.ts new file mode 100644 index 00000000..d8f2c044 --- /dev/null +++ b/JEU/src/server/class/GameComponentsServer.ts @@ -0,0 +1,15 @@ + +import * as c from "../constants.js" +import * as en from "../../shared_js/enums.js" +import { GameComponents } from "../../shared_js/class/GameComponents.js"; + +class GameComponentsServer extends GameComponents { + scoreLeft: number = 0; + scoreRight: number = 0; + constructor(options: en.MatchOptions) + { + super(options); + } +} + +export {GameComponentsServer} diff --git a/JEU/src/server/class/GameSession.ts b/JEU/src/server/class/GameSession.ts new file mode 100644 index 00000000..93cc544a --- /dev/null +++ b/JEU/src/server/class/GameSession.ts @@ -0,0 +1,188 @@ + +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 } from "./Client"; +import { GameComponentsServer } from "./GameComponentsServer.js"; +import { clientInputListener } from "../wsServer.js"; +import { random } from "../utils.js"; +import { Ball } from "../../shared_js/class/Rectangle.js"; +import { wallsMovements } from "../../shared_js/wallsMovement.js"; + +/* + Arg "s: GameSession" replace "this: GameSession" for use with setTimeout(), + because "this" is equal to "this: Timeout" +*/ +class GameSession { + id: string; // url ? + playersMap: Map = new Map(); + unreadyPlayersMap: Map = new Map(); + gameLoopInterval: NodeJS.Timer | number = 0; + clientsUpdateInterval: NodeJS.Timer | number = 0; + components: GameComponentsServer; + matchOptions: en.MatchOptions; + matchEnded: boolean = false; + + actual_time: number; + last_time: number; + delta_time: number; + + constructor(id: string, matchOptions: en.MatchOptions) { + this.id = id; + this.matchOptions = matchOptions; + 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.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s); + s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s); + } + pause(s: GameSession) { + s.playersMap.forEach( (client) => { + client.socket.off("message", clientInputListener); + }); + + clearInterval(s.gameLoopInterval); + clearInterval(s.clientsUpdateInterval); + } + 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; + } + + 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))); + }); + setTimeout(this._newRound, c.newRoundDelay, this, ball); + } + } + } + private _clientsUpdate(s: GameSession) { + const gc = s.components; + const update = new ev.EventGameUpdate(); + update.playerLeft.y = gc.playerLeft.pos.y; + update.playerRight.y = gc.playerRight.pos.y; + gc.ballsArr.forEach((ball) => { + update.ballsArr.push({ + x: ball.pos.x, + y: ball.pos.y, + dirX: ball.dir.x, + dirY: ball.dir.y, + speed: ball.speed + }); + }); + if (s.matchOptions & en.MatchOptions.movingWalls) { + update.wallTop.y = gc.wallTop.pos.y; + update.wallBottom.y = gc.wallBottom.pos.y; + } + + s.playersMap.forEach( (client) => { + update.lastInputId = client.lastInputId; + client.socket.send(JSON.stringify(update)); + }); + } + private _newRound(s: GameSession, ball: Ball) { + const gc = s.components; + // https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches + if (gc.scoreLeft >= 11 || gc.scoreRight >= 11) + // if (gc.scoreLeft >= 2 || gc.scoreRight >= 2) // WIP: for testing + { + if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2) + { + s._matchEnd(s); + 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 _matchEnd(s: GameSession) { + s.matchEnded = true; + const gc = s.components; + + let eventEnd: ev.EventMatchEnd; + if (gc.scoreLeft > gc.scoreRight) { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.left); + console.log("Player Left WIN"); + } + else { + eventEnd = new ev.EventMatchEnd(en.PlayerSide.right); + console.log("Player Right WIN"); + } + + s.playersMap.forEach( (client) => { + client.socket.send(JSON.stringify(eventEnd)); + }); + } +} + +export {GameSession} diff --git a/JEU/src/server/constants.ts b/JEU/src/server/constants.ts new file mode 100644 index 00000000..bd5f8e9e --- /dev/null +++ b/JEU/src/server/constants.ts @@ -0,0 +1,9 @@ + +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 clientsUpdateIntervalMS = 1000/30; // millisecond diff --git a/JEU/src/server/server.ts b/JEU/src/server/server.ts new file mode 100644 index 00000000..20801d7f --- /dev/null +++ b/JEU/src/server/server.ts @@ -0,0 +1,41 @@ + +import http from "http"; +import url from "url"; +import fs from "fs"; +import path from "path"; + +import {wsServer} from "./wsServer.js"; wsServer; // no-op, just for loading + +const hostname = "localhost"; +const port = 8080; +const root = "../../www/"; + +const server = http.createServer((req, res) => { + // let q = new URL(req.url, `http://${req.getHeaders().host}`) + let q = url.parse(req.url, true); + let filename = root + q.pathname; + fs.readFile(filename, (err, data) => { + if (err) { + res.writeHead(404, {"Content-Type": "text/html"}); + return res.end("404 Not Found"); + } + if (path.extname(filename) === ".html") { + res.writeHead(200, {"Content-Type": "text/html"}); + } + else if (path.extname(filename) === ".js") { + res.writeHead(200, {"Content-Type": "application/javascript"}); + } + else if (path.extname(filename) === ".mp3") { + res.writeHead(200, {"Content-Type": "audio/mpeg"}); + } + else if (path.extname(filename) === ".ogg") { + res.writeHead(200, {"Content-Type": "audio/ogg"}); + } + res.write(data); + return res.end(); + }); +}); + +server.listen(port, hostname, () => { + console.log(`Pong running at http://${hostname}:${port}/pong.html`); +}); diff --git a/JEU/src/server/utils.ts b/JEU/src/server/utils.ts new file mode 100644 index 00000000..0c939b8c --- /dev/null +++ b/JEU/src/server/utils.ts @@ -0,0 +1,8 @@ + +export * from "../shared_js/utils.js" + +function shortId(id: string): string { + return id.substring(0, id.indexOf("-")); +} + +export {shortId} diff --git a/JEU/src/server/wsServer.ts b/JEU/src/server/wsServer.ts new file mode 100644 index 00000000..a82df62d --- /dev/null +++ b/JEU/src/server/wsServer.ts @@ -0,0 +1,238 @@ + +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 { Client, ClientPlayer } from "./class/Client.js" +import { GameSession } from "./class/GameSession.js" +import { shortId } from "./utils.js"; + +// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ? +const wsPort = 8042; +export const wsServer = new WebSocketServer({port: wsPort, path: "/pong"}); + +const clientsMap: Map = new Map; // socket.id/Client +const matchmakingPlayersMap: Map = new Map; // socket.id/ClientPlayer (duplicates with clientsMap) +const gameSessionsMap: Map = 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); +} + + +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 ? + // TODO: spectator/player distinction with msg.role ? + // msg.role is probably not a good idea. + // something like a different route could be better + // "/pong" to play, "/ID_OF_A_GAMESESSION" to spectate + + const player = clientsMap.get(this.id) as ClientPlayer; + player.matchOptions = msg.matchOptions; + this.send(JSON.stringify( new ev.EventAssignId(this.id) )); + this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) )); + matchmaking(player); + } + else { + console.log("Invalid ClientAnnounce"); + } + return; + } + catch (e) { + console.log("Invalid JSON (clientAnnounceListener)"); + } + this.once("message", clientAnnounceListener); +} + + +function matchmaking(player: ClientPlayer) +{ + const minPlayersNumber = 2; + const maxPlayersNumber = 2; + const matchOptions = player.matchOptions; + matchmakingPlayersMap.set(player.id, player); + + const compatiblePlayers: ClientPlayer[] = []; + for (const [id, client] of matchmakingPlayersMap) + { + if (client.matchOptions === matchOptions) + { + compatiblePlayers.push(client); + if (compatiblePlayers.length === maxPlayersNumber) { + break; + } + } + } + + if (compatiblePlayers.length < minPlayersNumber) { + return; + } + + const id = uuidv4(); + const gameSession = new GameSession(id, matchOptions); + gameSessionsMap.set(id, gameSession); + + compatiblePlayers.forEach((client) => { + matchmakingPlayersMap.delete(client.id); + client.gameSession = gameSession; + gameSession.playersMap.set(client.id, client); + gameSession.unreadyPlayersMap.set(client.id, client); + }); + + // WIP: Not pretty, hardcoded two players. + // Could be done in gameSession maybe ? + compatiblePlayers[0].racket = gameSession.components.playerRight; + compatiblePlayers[1].racket = gameSession.components.playerLeft; + + compatiblePlayers.forEach((client) => { + client.socket.once("message", playerReadyConfirmationListener); + }); + + compatiblePlayers[0].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) )); + compatiblePlayers[1].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) )); +} + + +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) { + 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, key, map) => { + if (!client.isAlive) { + clientTerminate(client, key, map); + deleteLog += ` ${shortId(key)} |`; + } + 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("matchmakingPlayersMap size: " + matchmakingPlayersMap.size); + console.log(""); +}, 4200); + + +function clientTerminate(client: Client, key: string, map: Map) +{ + client.socket.terminate(); + if (client.gameSession) + { + client.gameSession.playersMap.delete(key); + if (client.gameSession.playersMap.size === 0) + { + clearInterval(client.gameSession.clientsUpdateInterval); + clearInterval(client.gameSession.gameLoopInterval); + gameSessionsMap.delete(client.gameSession.id); + } + } + map.delete(key); + if (matchmakingPlayersMap.has(key)) { + matchmakingPlayersMap.delete(key); + } +} + + +function closeListener() +{ + clearInterval(pingInterval); +} + + +function errorListener(error: Error) +{ + console.log("Error: " + JSON.stringify(error)); +} diff --git a/JEU/src/shared_js/class/Event.ts b/JEU/src/shared_js/class/Event.ts new file mode 100644 index 00000000..3f0d440a --- /dev/null +++ b/JEU/src/shared_js/class/Event.ts @@ -0,0 +1,107 @@ + +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 +} diff --git a/JEU/src/shared_js/class/GameComponents.ts b/JEU/src/shared_js/class/GameComponents.ts new file mode 100644 index 00000000..10e60932 --- /dev/null +++ b/JEU/src/shared_js/class/GameComponents.ts @@ -0,0 +1,65 @@ + +import * as c from "../constants.js" +import * as en from "../../shared_js/enums.js" +import { VectorInteger } from "./Vector.js"; +import { Rectangle, MovingRectangle, Racket, Ball } from "./Rectangle.js"; +import { random } from "../utils.js"; + +class GameComponents { + wallTop: Rectangle | MovingRectangle; + wallBottom: Rectangle | MovingRectangle; + playerLeft: Racket; + playerRight: Racket; + ballsArr: Ball[] = []; + constructor(options: en.MatchOptions) + { + const pos = new VectorInteger; + + // Rackets + pos.assign(0+c.pw, c.h_mid-c.ph/2); + this.playerLeft = new Racket(pos, c.pw, c.ph, c.racketSpeed); + pos.assign(c.w-c.pw-c.pw, c.h_mid-c.ph/2); + this.playerRight = new Racket(pos, c.pw, c.ph, c.racketSpeed); + + // Balls + let ballsCount = 1; + if (options & en.MatchOptions.multiBalls) { + ballsCount = c.multiBallsCount; + } + pos.assign(-c.ballSize, -c.ballSize); // ball out =) + while (this.ballsArr.length < ballsCount) { + this.ballsArr.push(new Ball(pos, c.ballSize, c.ballSpeed, c.ballSpeedIncrease)) + } + this.ballsArr.forEach((ball) => { + ball.dir.x = 1; + if (random() > 0.5) { + ball.dir.x *= -1; + } + + ball.dir.y = random(0, 0.2); + if (random() > 0.5) { + ball.dir.y *= -1; + } + + ball.dir = ball.dir.normalized(); + }); + + // Walls + if (options & en.MatchOptions.movingWalls) { + pos.assign(0, 0); + this.wallTop = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + (this.wallTop).dir.y = -1; + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed); + (this.wallBottom).dir.y = 1; + } + else { + pos.assign(0, 0); + this.wallTop = new Rectangle(pos, c.w, c.wallSize); + pos.assign(0, c.h-c.wallSize); + this.wallBottom = new Rectangle(pos, c.w, c.wallSize); + } + } +} + +export {GameComponents} diff --git a/JEU/src/shared_js/class/Rectangle.ts b/JEU/src/shared_js/class/Rectangle.ts new file mode 100644 index 00000000..fff71dc9 --- /dev/null +++ b/JEU/src/shared_js/class/Rectangle.ts @@ -0,0 +1,144 @@ + +import { Vector, VectorInteger } from "./Vector.js"; +import { Component, Moving } from "./interface.js"; +import * as c from "../constants.js" + +class Rectangle implements Component { + pos: VectorInteger; + width: number; + height: number; + constructor(pos: VectorInteger, width: number, height: number) { + this.pos = new VectorInteger(pos.x, pos.y); + this.width = width; + this.height = height; + } + collision(collider: Rectangle): boolean { + const thisLeft = this.pos.x; + const thisRight = this.pos.x + this.width; + const thisTop = this.pos.y; + const thisBottom = this.pos.y + this.height; + const colliderLeft = collider.pos.x; + const colliderRight = collider.pos.x + collider.width; + const colliderTop = collider.pos.y; + const colliderBottom = collider.pos.y + collider.height; + if ((thisBottom < colliderTop) + || (thisTop > colliderBottom) + || (thisRight < colliderLeft) + || (thisLeft > colliderRight)) { + return false; + } + else { + return true; + } + } +} + +class MovingRectangle extends Rectangle implements Moving { + dir: Vector = new Vector(0,0); + speed: number; + readonly baseSpeed: number; + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height); + this.baseSpeed = baseSpeed; + this.speed = baseSpeed; + } + move(delta: number) { // Math.floor WIP until VectorInteger debug + // console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`); + // this.pos.x += Math.floor(this.dir.x * this.speed * delta); + // this.pos.y += Math.floor(this.dir.y * this.speed * delta); + this.pos.x += this.dir.x * this.speed * delta; + this.pos.y += this.dir.y * this.speed * delta; + } + moveAndCollide(delta: number, colliderArr: Rectangle[]) { + this._moveAndCollideAlgo(delta, colliderArr); + } + protected _moveAndCollideAlgo(delta: number, colliderArr: Rectangle[]) { + let oldPos = new VectorInteger(this.pos.x, this.pos.y); + this.move(delta); + if (colliderArr.some(this.collision, this)) { + this.pos = oldPos; + } + } +} + +class Racket extends MovingRectangle { + constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) { + super(pos, width, height, baseSpeed); + } + moveAndCollide(delta: number, colliderArr: Rectangle[]) { + // let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug + this._moveAndCollideAlgo(delta, colliderArr); + // console.log(`y change: ${this.pos.y - oldPos.y}`); + } +} + +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; + } + bounce(collider?: Rectangle) { + this._bounceAlgo(collider); + } + protected _bounceAlgo(collider?: Rectangle) { + /* Could be more generic, but testing only Racket is enough, + because in Pong collider can only be Racket or Wall. */ + if (collider instanceof Racket) { + this._bounceRacket(collider); + } + else { + 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; + } + protected _bounceRacket(racket: Racket) { + this._bounceRacketAlgo(racket); + } + protected _bounceRacketAlgo(racket: Racket) { + this.speed += this.speedIncrease; + + let x = this.dir.x * -1; + + const angleFactorDegree = 60; + const angleFactor = angleFactorDegree / 90; + const racketHalf = racket.height/2; + const ballMid = this.pos.y + this.height/2; + const racketMid = racket.pos.y + racketHalf; + + let impact = ballMid - racketMid; + const horizontalMargin = racketHalf * 0.15; + if (impact < horizontalMargin && impact > -horizontalMargin) { + impact = 0; + } + else if (impact > 0) { + impact = impact - horizontalMargin; + } + else if (impact < 0) { + impact = impact + horizontalMargin; + } + + let y = impact / (racketHalf - horizontalMargin) * angleFactor; + + this.dir.assign(x, y); + // Normalize Vector (for consistency in speed independent of direction) + if (c.normalizedSpeed) { + this.dir = this.dir.normalized(); + } + // console.log(`x: ${this.dir.x}, y: ${this.dir.y}`); + } +} + +export {Rectangle, MovingRectangle, Racket, Ball} diff --git a/JEU/src/shared_js/class/Vector.ts b/JEU/src/shared_js/class/Vector.ts new file mode 100644 index 00000000..025bca36 --- /dev/null +++ b/JEU/src/shared_js/class/Vector.ts @@ -0,0 +1,49 @@ + +class Vector { + x: number; + y: number; + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + assign(x: number, y: number) { + this.x = x; + this.y = y; + } + normalized() : Vector { + const normalizationFactor = Math.abs(this.x) + Math.abs(this.y); + return new Vector(this.x/normalizationFactor, this.y/normalizationFactor); + } +} + +class VectorInteger extends Vector { + // PLACEHOLDER + // VectorInteger with set/get dont work (No draw on the screen). Why ? +} + +/* +class VectorInteger { + // private _x: number = 0; + // private _y: number = 0; + // constructor(x: number = 0, y: number = 0) { + // this._x = x; + // this._y = y; + // } + // get x(): number { + // return this._x; + // } + // set x(v: number) { + // // this._x = Math.floor(v); + // this._x = v; + // } + // get y(): number { + // return this._y; + // } + // set y(v: number) { + // // this._y = Math.floor(v); + // this._y = v; + // } +} +*/ + +export {Vector, VectorInteger} diff --git a/JEU/src/shared_js/class/interface.ts b/JEU/src/shared_js/class/interface.ts new file mode 100644 index 00000000..39753de1 --- /dev/null +++ b/JEU/src/shared_js/class/interface.ts @@ -0,0 +1,21 @@ + +import { Vector, VectorInteger } from "./Vector.js"; + +interface Component { + pos: VectorInteger; +} + +interface GraphicComponent extends Component { + ctx: CanvasRenderingContext2D; + color: string; + update: () => void; + clear: (pos?: VectorInteger) => void; +} + +interface Moving { + dir: Vector; + speed: number; // pixel per second + move(delta: number): void; +} + +export {Component, GraphicComponent, Moving} diff --git a/JEU/src/shared_js/constants.ts b/JEU/src/shared_js/constants.ts new file mode 100644 index 00000000..ae3320e5 --- /dev/null +++ b/JEU/src/shared_js/constants.ts @@ -0,0 +1,26 @@ + +export const CanvasWidth = 1500; +export const CanvasRatio = 1.66666; +/* ratio 5/3 (1.66) */ + +export const w = CanvasWidth; +export const h = CanvasWidth / CanvasRatio; +export const w_mid = Math.floor(w/2); +export const h_mid = Math.floor(h/2); +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 ballSpeedIncrease = Math.floor(ballSpeed*0.05); // pixel per second + +export const normalizedSpeed = false; // for consistency in speed independent of direction + +export const matchStartDelay = 3000; // millisecond +export const newRoundDelay = 1500; // millisecond + +// Game Variantes +export const multiBallsCount = 3; +export const movingWallPosMax = Math.floor(w*0.12); +export const movingWallSpeed = Math.floor(w*0.08); diff --git a/JEU/src/shared_js/enums.ts b/JEU/src/shared_js/enums.ts new file mode 100644 index 00000000..dfba2aa3 --- /dev/null +++ b/JEU/src/shared_js/enums.ts @@ -0,0 +1,47 @@ + +enum EventTypes { + // Class Implemented + gameUpdate = 1, + scoreUpdate, + matchEnd, + assignId, + matchmakingComplete, + + // Generic + matchmakingInProgress, + matchStart, + matchNewRound, // unused + matchPause, // unused + matchResume, // unused + + // Client + clientAnnounce, + clientPlayerReady, + clientInput, + +} + +enum InputEnum { + noInput = 0, + up = 1, + down, +} + +enum PlayerSide { + left = 1, + right +} + +enum ClientRole { + player = 1, + spectator +} + +enum MatchOptions { + // binary flags, can be mixed + noOption = 0b0, + multiBalls = 1 << 0, + movingWalls = 1 << 1 +} + +export {EventTypes, InputEnum, PlayerSide, ClientRole, MatchOptions} diff --git a/JEU/src/shared_js/utils.ts b/JEU/src/shared_js/utils.ts new file mode 100644 index 00000000..e8f7bca3 --- /dev/null +++ b/JEU/src/shared_js/utils.ts @@ -0,0 +1,27 @@ + +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} diff --git a/JEU/src/shared_js/wallsMovement.ts b/JEU/src/shared_js/wallsMovement.ts new file mode 100644 index 00000000..dbf3f558 --- /dev/null +++ b/JEU/src/shared_js/wallsMovement.ts @@ -0,0 +1,20 @@ + +import * as c from "./constants.js"; +import { MovingRectangle } from "../shared_js/class/Rectangle.js"; +import { GameComponents } from "./class/GameComponents.js"; + +function wallsMovements(delta: number, gc: GameComponents) +{ + const wallTop = gc.wallTop; + const wallBottom = gc.wallBottom; + if (wallTop.pos.y <= 0 || wallTop.pos.y >= c.movingWallPosMax) { + wallTop.dir.y *= -1; + } + if (wallBottom.pos.y >= c.h-c.wallSize || wallBottom.pos.y <= c.h-c.movingWallPosMax) { + wallBottom.dir.y *= -1; + } + wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); + wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]); +} + +export {wallsMovements} diff --git a/JEU/tsconfig.json b/JEU/tsconfig.json new file mode 100644 index 00000000..988126ba --- /dev/null +++ b/JEU/tsconfig.json @@ -0,0 +1,107 @@ +{ + "include": ["src"], +// "exclude": ["node_modules"], + "compilerOptions": { + // "outDir": "./build", + // "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + /* 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.gc. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.gc. '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": "commonjs", /* Specify what module code is generated. */ + "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'. */ + // "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 ''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. */ + } +} diff --git a/JEU/www/Bit5x3.woff b/JEU/www/Bit5x3.woff new file mode 100644 index 00000000..72c8b293 Binary files /dev/null and b/JEU/www/Bit5x3.woff differ diff --git a/JEU/www/Bit5x3.woff2 b/JEU/www/Bit5x3.woff2 new file mode 100644 index 00000000..27d538bf Binary files /dev/null and b/JEU/www/Bit5x3.woff2 differ diff --git a/JEU/www/favicon.ico b/JEU/www/favicon.ico new file mode 100644 index 00000000..b7e7e5ca Binary files /dev/null and b/JEU/www/favicon.ico differ diff --git a/JEU/www/sound/pong/0.ogg b/JEU/www/sound/pong/0.ogg new file mode 100644 index 00000000..93d05409 Binary files /dev/null and b/JEU/www/sound/pong/0.ogg differ diff --git a/JEU/www/sound/pong/1.ogg b/JEU/www/sound/pong/1.ogg new file mode 100644 index 00000000..3a268b34 Binary files /dev/null and b/JEU/www/sound/pong/1.ogg differ diff --git a/JEU/www/sound/pong/10.ogg b/JEU/www/sound/pong/10.ogg new file mode 100644 index 00000000..855ad78b Binary files /dev/null and b/JEU/www/sound/pong/10.ogg differ diff --git a/JEU/www/sound/pong/11.ogg b/JEU/www/sound/pong/11.ogg new file mode 100644 index 00000000..655917b5 Binary files /dev/null and b/JEU/www/sound/pong/11.ogg differ diff --git a/JEU/www/sound/pong/12.ogg b/JEU/www/sound/pong/12.ogg new file mode 100644 index 00000000..11336a76 Binary files /dev/null and b/JEU/www/sound/pong/12.ogg differ diff --git a/JEU/www/sound/pong/13.ogg b/JEU/www/sound/pong/13.ogg new file mode 100644 index 00000000..71cfead6 Binary files /dev/null and b/JEU/www/sound/pong/13.ogg differ diff --git a/JEU/www/sound/pong/14.ogg b/JEU/www/sound/pong/14.ogg new file mode 100644 index 00000000..066fff69 Binary files /dev/null and b/JEU/www/sound/pong/14.ogg differ diff --git a/JEU/www/sound/pong/15.ogg b/JEU/www/sound/pong/15.ogg new file mode 100644 index 00000000..011f139c Binary files /dev/null and b/JEU/www/sound/pong/15.ogg differ diff --git a/JEU/www/sound/pong/16.ogg b/JEU/www/sound/pong/16.ogg new file mode 100644 index 00000000..7e852275 Binary files /dev/null and b/JEU/www/sound/pong/16.ogg differ diff --git a/JEU/www/sound/pong/17.ogg b/JEU/www/sound/pong/17.ogg new file mode 100644 index 00000000..9860139d Binary files /dev/null and b/JEU/www/sound/pong/17.ogg differ diff --git a/JEU/www/sound/pong/18.ogg b/JEU/www/sound/pong/18.ogg new file mode 100644 index 00000000..6ad25391 Binary files /dev/null and b/JEU/www/sound/pong/18.ogg differ diff --git a/JEU/www/sound/pong/19.ogg b/JEU/www/sound/pong/19.ogg new file mode 100644 index 00000000..f6fc42d6 Binary files /dev/null and b/JEU/www/sound/pong/19.ogg differ diff --git a/JEU/www/sound/pong/2.ogg b/JEU/www/sound/pong/2.ogg new file mode 100644 index 00000000..0f09bb30 Binary files /dev/null and b/JEU/www/sound/pong/2.ogg differ diff --git a/JEU/www/sound/pong/20.ogg b/JEU/www/sound/pong/20.ogg new file mode 100644 index 00000000..11ac780e Binary files /dev/null and b/JEU/www/sound/pong/20.ogg differ diff --git a/JEU/www/sound/pong/21.ogg b/JEU/www/sound/pong/21.ogg new file mode 100644 index 00000000..7c724dd4 Binary files /dev/null and b/JEU/www/sound/pong/21.ogg differ diff --git a/JEU/www/sound/pong/22.ogg b/JEU/www/sound/pong/22.ogg new file mode 100644 index 00000000..b2ca9758 Binary files /dev/null and b/JEU/www/sound/pong/22.ogg differ diff --git a/JEU/www/sound/pong/23.ogg b/JEU/www/sound/pong/23.ogg new file mode 100644 index 00000000..f57724b9 Binary files /dev/null and b/JEU/www/sound/pong/23.ogg differ diff --git a/JEU/www/sound/pong/24.ogg b/JEU/www/sound/pong/24.ogg new file mode 100644 index 00000000..90093efc Binary files /dev/null and b/JEU/www/sound/pong/24.ogg differ diff --git a/JEU/www/sound/pong/25.ogg b/JEU/www/sound/pong/25.ogg new file mode 100644 index 00000000..27dfe8eb Binary files /dev/null and b/JEU/www/sound/pong/25.ogg differ diff --git a/JEU/www/sound/pong/26.ogg b/JEU/www/sound/pong/26.ogg new file mode 100644 index 00000000..80cb60fa Binary files /dev/null and b/JEU/www/sound/pong/26.ogg differ diff --git a/JEU/www/sound/pong/27.ogg b/JEU/www/sound/pong/27.ogg new file mode 100644 index 00000000..13332de6 Binary files /dev/null and b/JEU/www/sound/pong/27.ogg differ diff --git a/JEU/www/sound/pong/28.ogg b/JEU/www/sound/pong/28.ogg new file mode 100644 index 00000000..29615795 Binary files /dev/null and b/JEU/www/sound/pong/28.ogg differ diff --git a/JEU/www/sound/pong/29.ogg b/JEU/www/sound/pong/29.ogg new file mode 100644 index 00000000..41f95343 Binary files /dev/null and b/JEU/www/sound/pong/29.ogg differ diff --git a/JEU/www/sound/pong/3.ogg b/JEU/www/sound/pong/3.ogg new file mode 100644 index 00000000..12448222 Binary files /dev/null and b/JEU/www/sound/pong/3.ogg differ diff --git a/JEU/www/sound/pong/30.ogg b/JEU/www/sound/pong/30.ogg new file mode 100644 index 00000000..bd4e4ffd Binary files /dev/null and b/JEU/www/sound/pong/30.ogg differ diff --git a/JEU/www/sound/pong/31.ogg b/JEU/www/sound/pong/31.ogg new file mode 100644 index 00000000..4447b52a Binary files /dev/null and b/JEU/www/sound/pong/31.ogg differ diff --git a/JEU/www/sound/pong/32.ogg b/JEU/www/sound/pong/32.ogg new file mode 100644 index 00000000..a58240a1 Binary files /dev/null and b/JEU/www/sound/pong/32.ogg differ diff --git a/JEU/www/sound/pong/4.ogg b/JEU/www/sound/pong/4.ogg new file mode 100644 index 00000000..688b1f81 Binary files /dev/null and b/JEU/www/sound/pong/4.ogg differ diff --git a/JEU/www/sound/pong/5.ogg b/JEU/www/sound/pong/5.ogg new file mode 100644 index 00000000..d2163268 Binary files /dev/null and b/JEU/www/sound/pong/5.ogg differ diff --git a/JEU/www/sound/pong/6.ogg b/JEU/www/sound/pong/6.ogg new file mode 100644 index 00000000..34bdd117 Binary files /dev/null and b/JEU/www/sound/pong/6.ogg differ diff --git a/JEU/www/sound/pong/7.ogg b/JEU/www/sound/pong/7.ogg new file mode 100644 index 00000000..9c9c994f Binary files /dev/null and b/JEU/www/sound/pong/7.ogg differ diff --git a/JEU/www/sound/pong/8.ogg b/JEU/www/sound/pong/8.ogg new file mode 100644 index 00000000..0f9acf99 Binary files /dev/null and b/JEU/www/sound/pong/8.ogg differ diff --git a/JEU/www/sound/pong/9.ogg b/JEU/www/sound/pong/9.ogg new file mode 100644 index 00000000..15d82091 Binary files /dev/null and b/JEU/www/sound/pong/9.ogg differ diff --git a/JEU/www/sound/roblox-oof.ogg b/JEU/www/sound/roblox-oof.ogg new file mode 100644 index 00000000..689946ed Binary files /dev/null and b/JEU/www/sound/roblox-oof.ogg differ diff --git a/srcs/requirements/nestjs/api_back/src/app.module.ts b/srcs/requirements/nestjs/api_back/src/app.module.ts index ba0f94a2..17d38d49 100644 --- a/srcs/requirements/nestjs/api_back/src/app.module.ts +++ b/srcs/requirements/nestjs/api_back/src/app.module.ts @@ -7,6 +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'; @Module({ imports: [UsersModule, @@ -26,6 +27,7 @@ import { PassportModule } from '@nestjs/passport'; //avec une classe pour le module synchronize: true, }), + GameModule, ], controllers: [AppController], providers: [AppService], diff --git a/srcs/requirements/nestjs/api_back/src/auth/42/authentication.controller.ts b/srcs/requirements/nestjs/api_back/src/auth/42/authentication.controller.ts index 94b955bb..7d331a55 100644 --- a/srcs/requirements/nestjs/api_back/src/auth/42/authentication.controller.ts +++ b/srcs/requirements/nestjs/api_back/src/auth/42/authentication.controller.ts @@ -4,6 +4,7 @@ import { AuthenticationService } from './authentication.service'; import { Response } from 'express'; import { TwoFaDto } from './dto/2fa.dto'; import { UsersService } from 'src/users/users.service'; +import { User } from 'src/users/entities/user.entity'; @Controller('auth') export class AuthenticationController { @@ -33,7 +34,8 @@ export class AuthenticationController { async redirect(@Res() response : Response, @Req() request) { console.log('ON EST DANS REDIRECT AUTH CONTROLLER'); console.log('On redirige'); - if (request.user.isEnabledTwoFactorAuth === false) + const user : User = request.user + if (user.isEnabledTwoFactorAuth === false || user.isTwoFactorAuthenticated === true) return response.status(200).redirect('http://transcendance:8080/#/profile'); return response.status(200).redirect('http://transcendance:8080/#/2fa'); } @@ -58,25 +60,33 @@ export class AuthenticationController { @Post('2fa/generate') @UseGuards(AuthenticateGuard) async register(@Req() request, @Res() response){ - console.log('ON EST DANS REGISTER POUR 2FA AUTH CONTROLLER') - const { otpauth } = await this.authService.generate2FaSecret(request.user); - return this.authService.pipeQrCodeStream(response, otpauth); + const user : User = request.user; + if (user.isEnabledTwoFactorAuth === true) + { + console.log('ON EST DANS REGISTER POUR 2FA AUTH CONTROLLER') + const { otpauth } = await this.authService.generate2FaSecret(request.user); + return this.authService.pipeQrCodeStream(response, otpauth); + } } @Post('2fa/turn-on') @UseGuards(AuthenticateGuard) async verify(@Req() request, @Body() {twoFaCode} : TwoFaDto, @Res() response){ - console.log('ON EST DANS VERIFY POUR 2FA AUTH CONTROLLER') - const isCodeIsValid = await this.authService.verify2FaCode(request.user, twoFaCode); - if (isCodeIsValid === false) + const user : User = request.user; + if (user.isEnabledTwoFactorAuth === true) { - throw new UnauthorizedException('Wrong Code.'); + console.log('ON EST DANS VERIFY POUR 2FA AUTH CONTROLLER') + const isCodeIsValid = await this.authService.verify2FaCode(request.user, twoFaCode); + if (isCodeIsValid === false) + { + throw new UnauthorizedException('Wrong Code.'); + } + await this.userService.enableTwoFactorAuth(request.user.id); + console.log('ON REDIRIGE'); + // return response.status(200); + // return 200; + // needs to be looked at by Cherif } - await this.userService.enableTwoFactorAuth(request.user.id); - console.log('ON REDIRIGE'); - // return response.status(200); - // return 200; - // needs to be looked at by Cherif return response.status(200).redirect('http://transcendance:8080/'); } } diff --git a/srcs/requirements/nestjs/api_back/src/game/game.controller.spec.ts b/srcs/requirements/nestjs/api_back/src/game/game.controller.spec.ts new file mode 100644 index 00000000..f70c47c2 --- /dev/null +++ b/srcs/requirements/nestjs/api_back/src/game/game.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GameController } from './game.controller'; + +describe('GameController', () => { + let controller: GameController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [GameController], + }).compile(); + + controller = module.get(GameController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/srcs/requirements/nestjs/api_back/src/game/game.controller.ts b/srcs/requirements/nestjs/api_back/src/game/game.controller.ts new file mode 100644 index 00000000..d86df072 --- /dev/null +++ b/srcs/requirements/nestjs/api_back/src/game/game.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('game') +export class GameController {} diff --git a/srcs/requirements/nestjs/api_back/src/game/game.module.ts b/srcs/requirements/nestjs/api_back/src/game/game.module.ts new file mode 100644 index 00000000..17a50c06 --- /dev/null +++ b/srcs/requirements/nestjs/api_back/src/game/game.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GameController } from './game.controller'; +import { GameService } from './game.service'; + +@Module({ + controllers: [GameController], + providers: [GameService] +}) +export class GameModule {} diff --git a/srcs/requirements/nestjs/api_back/src/game/game.service.spec.ts b/srcs/requirements/nestjs/api_back/src/game/game.service.spec.ts new file mode 100644 index 00000000..f4a1db7e --- /dev/null +++ b/srcs/requirements/nestjs/api_back/src/game/game.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GameService } from './game.service'; + +describe('GameService', () => { + let service: GameService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [GameService], + }).compile(); + + service = module.get(GameService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/srcs/requirements/nestjs/api_back/src/game/game.service.ts b/srcs/requirements/nestjs/api_back/src/game/game.service.ts new file mode 100644 index 00000000..18ca270d --- /dev/null +++ b/srcs/requirements/nestjs/api_back/src/game/game.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class GameService {} diff --git a/srcs/requirements/nestjs/api_back/src/users/users.controller.ts b/srcs/requirements/nestjs/api_back/src/users/users.controller.ts index eaa71624..d2319810 100644 --- a/srcs/requirements/nestjs/api_back/src/users/users.controller.ts +++ b/srcs/requirements/nestjs/api_back/src/users/users.controller.ts @@ -63,9 +63,12 @@ export class UsersController { @UseGuards(AuthenticateGuard) @UseGuards(TwoFactorGuard) @Patch() - update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto) { + update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto, @Res() response) { console.log("DANS PATCH USERS"); - return this.usersService.update(req.user.id, usersUpdateDto); + this.usersService.update(req.user.id, usersUpdateDto); + const user : User = req.user; + if (user.isEnabledTwoFactorAuth === true && user.isTwoFactorAuthenticated === false) + return response.status.redirect("http://transcendance:8080/#/2fa"); } @UseGuards(AuthenticateGuard)