début de la mise en place du jeu (partie client uniquement).
This commit is contained in:
36
JEU2/src/server/class/Client.ts
Normal file
36
JEU2/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}
|
||||
107
JEU2/src/server/class/Event.ts
Normal file
107
JEU2/src/server/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
JEU2/src/server/class/GameComponents.ts
Normal file
65
JEU2/src/server/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}
|
||||
15
JEU2/src/server/class/GameComponentsServer.ts
Normal file
15
JEU2/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
JEU2/src/server/class/GameSession.ts
Normal file
188
JEU2/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}
|
||||
144
JEU2/src/server/class/Rectangle.ts
Normal file
144
JEU2/src/server/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
JEU2/src/server/class/Vector.ts
Normal file
49
JEU2/src/server/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
JEU2/src/server/class/interface.ts
Normal file
21
JEU2/src/server/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}
|
||||
35
JEU2/src/server/constants.ts
Normal file
35
JEU2/src/server/constants.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
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
|
||||
|
||||
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
JEU2/src/server/enums.ts
Normal file
47
JEU2/src/server/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}
|
||||
41
JEU2/src/server/server.ts
Normal file
41
JEU2/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`);
|
||||
});
|
||||
33
JEU2/src/server/utils.ts
Normal file
33
JEU2/src/server/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { MovingRectangle } from "./class/Rectangle.js";
|
||||
export * from "../shared_js/utils.js"
|
||||
|
||||
function shortId(id: string): string {
|
||||
return id.substring(0, id.indexOf("-"));
|
||||
}
|
||||
|
||||
export {shortId}
|
||||
|
||||
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
JEU2/src/server/wallsMovement.ts
Normal file
20
JEU2/src/server/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}
|
||||
238
JEU2/src/server/wsServer.ts
Normal file
238
JEU2/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));
|
||||
}
|
||||
Reference in New Issue
Block a user