Ajout des fichiers de lucky + quelques corrections mineures sur l'auth (2FA) et le flow pour le user
This commit is contained in:
13
JEU/jsconfig.json
Normal file
13
JEU/jsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2020",
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"**/node_modules/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
JEU/make.sh
Normal file
18
JEU/make.sh
Normal file
@@ -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/
|
||||||
45
JEU/memo.txt
Normal file
45
JEU/memo.txt
Normal file
@@ -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).
|
||||||
121
JEU/package-lock.json
generated
Normal file
121
JEU/package-lock.json
generated
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
JEU/package.json
Normal file
13
JEU/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.11.5",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@types/ws": "^8.5.3",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"ws": "^8.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
JEU/src/client/audio.ts
Normal file
16
JEU/src/client/audio.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
38
JEU/src/client/class/GameArea.ts
Normal file
38
JEU/src/client/class/GameArea.ts
Normal file
@@ -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}
|
||||||
114
JEU/src/client/class/GameComponentsClient.ts
Normal file
114
JEU/src/client/class/GameComponentsClient.ts
Normal file
@@ -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 = <MovingRectangle>this.wallTop;
|
||||||
|
const baseWB = <MovingRectangle>this.wallBottom;
|
||||||
|
|
||||||
|
this.wallTop = new MovingRectangleClient(baseWT.pos, baseWT.width, baseWT.height, baseWT.baseSpeed,
|
||||||
|
ctx, "grey");
|
||||||
|
(<MovingRectangleClient>this.wallTop).dir.assign(baseWT.dir.x, baseWT.dir.y);
|
||||||
|
|
||||||
|
this.wallBottom = new MovingRectangleClient(baseWB.pos, baseWB.width, baseWB.height, baseWB.baseSpeed,
|
||||||
|
ctx, "grey");
|
||||||
|
(<MovingRectangleClient>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}
|
||||||
16
JEU/src/client/class/InputHistory.ts
Normal file
16
JEU/src/client/class/InputHistory.ts
Normal file
@@ -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}
|
||||||
141
JEU/src/client/class/RectangleClient.ts
Normal file
141
JEU/src/client/class/RectangleClient.ts
Normal file
@@ -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}
|
||||||
58
JEU/src/client/class/Text.ts
Normal file
58
JEU/src/client/class/Text.ts
Normal file
@@ -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}
|
||||||
18
JEU/src/client/constants.ts
Normal file
18
JEU/src/client/constants.ts
Normal file
@@ -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
|
||||||
51
JEU/src/client/draw.ts
Normal file
51
JEU/src/client/draw.ts
Normal file
@@ -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}
|
||||||
49
JEU/src/client/gameLoop.ts
Normal file
49
JEU/src/client/gameLoop.ts
Normal file
@@ -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}
|
||||||
3
JEU/src/client/global.ts
Normal file
3
JEU/src/client/global.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export {pong, gc, matchOptions} from "./pong.js"
|
||||||
|
export {socket, clientInfo} from "./ws.js"
|
||||||
110
JEU/src/client/handleInput.ts
Normal file
110
JEU/src/client/handleInput.ts
Normal file
@@ -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}
|
||||||
53
JEU/src/client/pong.css
Normal file
53
JEU/src/client/pong.css
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
42
JEU/src/client/pong.html
Normal file
42
JEU/src/client/pong.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="stylesheet" href="pong.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="preload_font">.</div>
|
||||||
|
|
||||||
|
<div id="div_game_options">
|
||||||
|
<fieldset>
|
||||||
|
<legend>game options</legend>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="multi_balls" name="multi_balls">
|
||||||
|
<label for="multi_balls">multiples balls</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="moving_walls" name="moving_walls">
|
||||||
|
<label for="moving_walls">moving walls</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>sound :</label>
|
||||||
|
<input type="radio" id="sound_on" name="sound_selector" checked>
|
||||||
|
<label for="sound_on">on</label>
|
||||||
|
<input type="radio" id="sound_off" name="sound_selector">
|
||||||
|
<label for="sound_off">off</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="play_pong_button">PLAY</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="canvas_container">
|
||||||
|
<!-- <p> =) </p> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="http://localhost:8080/js/pong.js" type="module" defer></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
99
JEU/src/client/pong.ts
Normal file
99
JEU/src/client/pong.ts
Normal file
@@ -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:"+(<HTMLInputElement>document.getElementById("multi_balls")).checked);
|
||||||
|
console.log("moving_walls:"+(<HTMLInputElement>document.getElementById("moving_walls")).checked);
|
||||||
|
console.log("sound_on:"+(<HTMLInputElement>document.getElementById("sound_on")).checked);
|
||||||
|
|
||||||
|
let soundMutedFlag = false;
|
||||||
|
if ( (<HTMLInputElement>document.getElementById("sound_off")).checked ) {
|
||||||
|
soundMutedFlag = true;
|
||||||
|
}
|
||||||
|
initAudio(soundMutedFlag);
|
||||||
|
|
||||||
|
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
|
||||||
|
matchOptions |= en.MatchOptions.multiBalls;
|
||||||
|
}
|
||||||
|
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
|
||||||
|
matchOptions |= en.MatchOptions.movingWalls;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("div_game_options").hidden = true;
|
||||||
|
|
||||||
|
pong = new GameArea();
|
||||||
|
gc = new GameComponentsClient(matchOptions, pong.ctx);
|
||||||
|
initWebSocket(matchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchmaking()
|
||||||
|
{
|
||||||
|
console.log("Searching an opponent...");
|
||||||
|
gc.text1.clear();
|
||||||
|
gc.text1.pos.assign(c.w/5, c.h_mid);
|
||||||
|
gc.text1.text = "Searching...";
|
||||||
|
gc.text1.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchmakingComplete()
|
||||||
|
{
|
||||||
|
console.log("Match Found !");
|
||||||
|
gc.text1.clear();
|
||||||
|
gc.text1.pos.assign(c.w/8, c.h_mid);
|
||||||
|
gc.text1.text = "Match Found !";
|
||||||
|
gc.text1.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
gc.text1.pos.assign(c.w_mid, c.h_mid+c.h/4);
|
||||||
|
countdown(c.matchStartDelay/1000, (count: number) => {
|
||||||
|
gc.text1.clear();
|
||||||
|
gc.text1.text = `${count}`;
|
||||||
|
gc.text1.update();
|
||||||
|
}, resumeGame);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeGame()
|
||||||
|
{
|
||||||
|
gc.text1.text = "";
|
||||||
|
window.addEventListener('keydown', function (e) {
|
||||||
|
pong.addKey(e.key);
|
||||||
|
});
|
||||||
|
window.addEventListener('keyup', function (e) {
|
||||||
|
pong.deleteKey(e.key);
|
||||||
|
});
|
||||||
|
pong.handleInputInterval = window.setInterval(handleInput, c.handleInputIntervalMS);
|
||||||
|
// pong.handleInputInterval = window.setInterval(sendLoop, c.sendLoopIntervalMS);
|
||||||
|
pong.gameLoopInterval = window.setInterval(gameLoop, c.gameLoopIntervalMS);
|
||||||
|
pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export {matchmaking, matchmakingComplete, startGame}
|
||||||
18
JEU/src/client/utils.ts
Normal file
18
JEU/src/client/utils.ts
Normal file
@@ -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}
|
||||||
182
JEU/src/client/ws.ts
Normal file
182
JEU/src/client/ws.ts
Normal file
@@ -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 = (<ev.EventAssignId>data).id;
|
||||||
|
break;
|
||||||
|
case en.EventTypes.matchmakingInProgress:
|
||||||
|
matchmaking();
|
||||||
|
break;
|
||||||
|
case en.EventTypes.matchmakingComplete:
|
||||||
|
clientInfo.side = (<ev.EventMatchmakingComplete>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;
|
||||||
36
JEU/src/server/class/Client.ts
Normal file
36
JEU/src/server/class/Client.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
import { WebSocket } from "../wsServer.js";
|
||||||
|
import { Racket } from "../../shared_js/class/Rectangle.js";
|
||||||
|
import { GameSession } from "./GameSession.js";
|
||||||
|
import * as ev from "../../shared_js/class/Event.js"
|
||||||
|
import * as en from "../../shared_js/enums.js"
|
||||||
|
|
||||||
|
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}
|
||||||
15
JEU/src/server/class/GameComponentsServer.ts
Normal file
15
JEU/src/server/class/GameComponentsServer.ts
Normal file
@@ -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}
|
||||||
188
JEU/src/server/class/GameSession.ts
Normal file
188
JEU/src/server/class/GameSession.ts
Normal file
@@ -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<string, ClientPlayer> = new Map();
|
||||||
|
unreadyPlayersMap: Map<string, ClientPlayer> = 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}
|
||||||
9
JEU/src/server/constants.ts
Normal file
9
JEU/src/server/constants.ts
Normal file
@@ -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
|
||||||
41
JEU/src/server/server.ts
Normal file
41
JEU/src/server/server.ts
Normal file
@@ -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`);
|
||||||
|
});
|
||||||
8
JEU/src/server/utils.ts
Normal file
8
JEU/src/server/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
export * from "../shared_js/utils.js"
|
||||||
|
|
||||||
|
function shortId(id: string): string {
|
||||||
|
return id.substring(0, id.indexOf("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export {shortId}
|
||||||
238
JEU/src/server/wsServer.ts
Normal file
238
JEU/src/server/wsServer.ts
Normal file
@@ -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<WebSocket>({port: wsPort, path: "/pong"});
|
||||||
|
|
||||||
|
const clientsMap: Map<string, Client> = new Map; // socket.id/Client
|
||||||
|
const matchmakingPlayersMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
|
||||||
|
const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession
|
||||||
|
|
||||||
|
wsServer.on("connection", connectionListener);
|
||||||
|
wsServer.on("error", errorListener);
|
||||||
|
wsServer.on("close", closeListener);
|
||||||
|
|
||||||
|
|
||||||
|
function connectionListener(socket: WebSocket, request: IncomingMessage)
|
||||||
|
{
|
||||||
|
const id = uuidv4();
|
||||||
|
const client = new Client(socket, id);
|
||||||
|
clientsMap.set(id, client);
|
||||||
|
socket.id = id;
|
||||||
|
|
||||||
|
socket.on("pong", function heartbeat() {
|
||||||
|
client.isAlive = true;
|
||||||
|
// console.log(`client ${shortId(client.id)} is alive`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("message", function log(data: string) {
|
||||||
|
try {
|
||||||
|
const event: ev.ClientEvent = JSON.parse(data);
|
||||||
|
if (event.type === en.EventTypes.clientInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
console.log("data: " + data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.once("message", clientAnnounceListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<string, Client>)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
107
JEU/src/shared_js/class/Event.ts
Normal file
107
JEU/src/shared_js/class/Event.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
65
JEU/src/shared_js/class/GameComponents.ts
Normal file
65
JEU/src/shared_js/class/GameComponents.ts
Normal file
@@ -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);
|
||||||
|
(<MovingRectangle>this.wallTop).dir.y = -1;
|
||||||
|
pos.assign(0, c.h-c.wallSize);
|
||||||
|
this.wallBottom = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed);
|
||||||
|
(<MovingRectangle>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}
|
||||||
144
JEU/src/shared_js/class/Rectangle.ts
Normal file
144
JEU/src/shared_js/class/Rectangle.ts
Normal file
@@ -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}
|
||||||
49
JEU/src/shared_js/class/Vector.ts
Normal file
49
JEU/src/shared_js/class/Vector.ts
Normal file
@@ -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}
|
||||||
21
JEU/src/shared_js/class/interface.ts
Normal file
21
JEU/src/shared_js/class/interface.ts
Normal file
@@ -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}
|
||||||
26
JEU/src/shared_js/constants.ts
Normal file
26
JEU/src/shared_js/constants.ts
Normal file
@@ -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);
|
||||||
47
JEU/src/shared_js/enums.ts
Normal file
47
JEU/src/shared_js/enums.ts
Normal file
@@ -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}
|
||||||
27
JEU/src/shared_js/utils.ts
Normal file
27
JEU/src/shared_js/utils.ts
Normal file
@@ -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}
|
||||||
20
JEU/src/shared_js/wallsMovement.ts
Normal file
20
JEU/src/shared_js/wallsMovement.ts
Normal file
@@ -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 = <MovingRectangle>gc.wallTop;
|
||||||
|
const wallBottom = <MovingRectangle>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}
|
||||||
107
JEU/tsconfig.json
Normal file
107
JEU/tsconfig.json
Normal file
@@ -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 '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
"strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
JEU/www/Bit5x3.woff
Normal file
BIN
JEU/www/Bit5x3.woff
Normal file
Binary file not shown.
BIN
JEU/www/Bit5x3.woff2
Normal file
BIN
JEU/www/Bit5x3.woff2
Normal file
Binary file not shown.
BIN
JEU/www/favicon.ico
Normal file
BIN
JEU/www/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
JEU/www/sound/pong/0.ogg
Normal file
BIN
JEU/www/sound/pong/0.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/1.ogg
Normal file
BIN
JEU/www/sound/pong/1.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/10.ogg
Normal file
BIN
JEU/www/sound/pong/10.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/11.ogg
Normal file
BIN
JEU/www/sound/pong/11.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/12.ogg
Normal file
BIN
JEU/www/sound/pong/12.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/13.ogg
Normal file
BIN
JEU/www/sound/pong/13.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/14.ogg
Normal file
BIN
JEU/www/sound/pong/14.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/15.ogg
Normal file
BIN
JEU/www/sound/pong/15.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/16.ogg
Normal file
BIN
JEU/www/sound/pong/16.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/17.ogg
Normal file
BIN
JEU/www/sound/pong/17.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/18.ogg
Normal file
BIN
JEU/www/sound/pong/18.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/19.ogg
Normal file
BIN
JEU/www/sound/pong/19.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/2.ogg
Normal file
BIN
JEU/www/sound/pong/2.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/20.ogg
Normal file
BIN
JEU/www/sound/pong/20.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/21.ogg
Normal file
BIN
JEU/www/sound/pong/21.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/22.ogg
Normal file
BIN
JEU/www/sound/pong/22.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/23.ogg
Normal file
BIN
JEU/www/sound/pong/23.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/24.ogg
Normal file
BIN
JEU/www/sound/pong/24.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/25.ogg
Normal file
BIN
JEU/www/sound/pong/25.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/26.ogg
Normal file
BIN
JEU/www/sound/pong/26.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/27.ogg
Normal file
BIN
JEU/www/sound/pong/27.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/28.ogg
Normal file
BIN
JEU/www/sound/pong/28.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/29.ogg
Normal file
BIN
JEU/www/sound/pong/29.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/3.ogg
Normal file
BIN
JEU/www/sound/pong/3.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/30.ogg
Normal file
BIN
JEU/www/sound/pong/30.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/31.ogg
Normal file
BIN
JEU/www/sound/pong/31.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/32.ogg
Normal file
BIN
JEU/www/sound/pong/32.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/4.ogg
Normal file
BIN
JEU/www/sound/pong/4.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/5.ogg
Normal file
BIN
JEU/www/sound/pong/5.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/6.ogg
Normal file
BIN
JEU/www/sound/pong/6.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/7.ogg
Normal file
BIN
JEU/www/sound/pong/7.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/8.ogg
Normal file
BIN
JEU/www/sound/pong/8.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/pong/9.ogg
Normal file
BIN
JEU/www/sound/pong/9.ogg
Normal file
Binary file not shown.
BIN
JEU/www/sound/roblox-oof.ogg
Normal file
BIN
JEU/www/sound/roblox-oof.ogg
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
import { FriendshipsModule } from './friendship/friendships.module';
|
import { FriendshipsModule } from './friendship/friendships.module';
|
||||||
import { AuthenticationModule } from './auth/42/authentication.module';
|
import { AuthenticationModule } from './auth/42/authentication.module';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { GameModule } from './game/game/game.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule,
|
imports: [UsersModule,
|
||||||
@@ -26,6 +27,7 @@ import { PassportModule } from '@nestjs/passport';
|
|||||||
//avec une classe pour le module
|
//avec une classe pour le module
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
}),
|
}),
|
||||||
|
GameModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthenticationService } from './authentication.service';
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { TwoFaDto } from './dto/2fa.dto';
|
import { TwoFaDto } from './dto/2fa.dto';
|
||||||
import { UsersService } from 'src/users/users.service';
|
import { UsersService } from 'src/users/users.service';
|
||||||
|
import { User } from 'src/users/entities/user.entity';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthenticationController {
|
export class AuthenticationController {
|
||||||
@@ -33,7 +34,8 @@ export class AuthenticationController {
|
|||||||
async redirect(@Res() response : Response, @Req() request) {
|
async redirect(@Res() response : Response, @Req() request) {
|
||||||
console.log('ON EST DANS REDIRECT AUTH CONTROLLER');
|
console.log('ON EST DANS REDIRECT AUTH CONTROLLER');
|
||||||
console.log('On redirige');
|
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/#/profile');
|
||||||
return response.status(200).redirect('http://transcendance:8080/#/2fa');
|
return response.status(200).redirect('http://transcendance:8080/#/2fa');
|
||||||
}
|
}
|
||||||
@@ -58,25 +60,33 @@ export class AuthenticationController {
|
|||||||
@Post('2fa/generate')
|
@Post('2fa/generate')
|
||||||
@UseGuards(AuthenticateGuard)
|
@UseGuards(AuthenticateGuard)
|
||||||
async register(@Req() request, @Res() response){
|
async register(@Req() request, @Res() response){
|
||||||
console.log('ON EST DANS REGISTER POUR 2FA AUTH CONTROLLER')
|
const user : User = request.user;
|
||||||
const { otpauth } = await this.authService.generate2FaSecret(request.user);
|
if (user.isEnabledTwoFactorAuth === true)
|
||||||
return this.authService.pipeQrCodeStream(response, otpauth);
|
{
|
||||||
|
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')
|
@Post('2fa/turn-on')
|
||||||
@UseGuards(AuthenticateGuard)
|
@UseGuards(AuthenticateGuard)
|
||||||
async verify(@Req() request, @Body() {twoFaCode} : TwoFaDto, @Res() response){
|
async verify(@Req() request, @Body() {twoFaCode} : TwoFaDto, @Res() response){
|
||||||
console.log('ON EST DANS VERIFY POUR 2FA AUTH CONTROLLER')
|
const user : User = request.user;
|
||||||
const isCodeIsValid = await this.authService.verify2FaCode(request.user, twoFaCode);
|
if (user.isEnabledTwoFactorAuth === true)
|
||||||
if (isCodeIsValid === false)
|
|
||||||
{
|
{
|
||||||
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/');
|
return response.status(200).redirect('http://transcendance:8080/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(GameController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('game')
|
||||||
|
export class GameController {}
|
||||||
@@ -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 {}
|
||||||
@@ -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>(GameService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameService {}
|
||||||
@@ -61,9 +61,12 @@ export class UsersController {
|
|||||||
@UseGuards(AuthenticateGuard)
|
@UseGuards(AuthenticateGuard)
|
||||||
@UseGuards(TwoFactorGuard)
|
@UseGuards(TwoFactorGuard)
|
||||||
@Patch()
|
@Patch()
|
||||||
update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto) {
|
update(@Req() req, @Body(new ValidationPipe()) usersUpdateDto: UpdateUsersDto, @Res() response) {
|
||||||
console.log("DANS PATCH USERS");
|
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)
|
@UseGuards(AuthenticateGuard)
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const primaryRoutes = {
|
|||||||
"/profile": ProfilePage,
|
"/profile": ProfilePage,
|
||||||
"/profile/*": ProfilePage,
|
"/profile/*": ProfilePage,
|
||||||
'/unauthorized-access': UnauthorizedAccessPage,
|
'/unauthorized-access': UnauthorizedAccessPage,
|
||||||
|
"/game" : GamePage,
|
||||||
"*": NotFound
|
"*": NotFound
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user