serveur de jeu - work in progress

This commit is contained in:
batche
2022-12-14 10:45:10 +01:00
parent 6ca35ccaa7
commit f1f94cb9bc
125 changed files with 3947 additions and 719 deletions

12
jeu/src/client/audio.js Normal file
View File

@@ -0,0 +1,12 @@
import * as c from "./constants.js";
export const soundPongArr = [];
export const soundRoblox = new Audio("http://localhost:8080/sound/roblox-oof.ogg");
export function initAudio(muteFlag) {
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;
}

16
jeu/src/client/audio.ts Normal file
View 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;
}

View File

@@ -0,0 +1,32 @@
import * as c from ".././constants.js";
export class GameArea {
constructor() {
this.keys = [];
this.handleInputInterval = 0;
this.gameLoopInterval = 0;
this.drawLoopInterval = 0;
this.canvas = document.createElement("canvas");
this.ctx = this.canvas.getContext("2d");
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) {
key = key.toLowerCase();
var i = this.keys.indexOf(key);
if (i == -1)
this.keys.push(key);
}
deleteKey(key) {
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);
}
}

View File

@@ -0,0 +1,36 @@
import * as c from ".././constants.js"
export 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);
}
}

View File

@@ -0,0 +1,71 @@
import * as c from "../constants.js";
import * as en from "../../shared_js/enums.js";
import { 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";
class GameComponentsExtensionForClient extends GameComponents {
constructor(options, ctx) {
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 = [];
this.ballsArr.forEach((ball) => {
newBallsArr.push(new BallClient(ball.pos, ball.width, ball.baseSpeed, ball.speedIncrease, ctx, "white"));
});
this.ballsArr = newBallsArr;
// Walls
if (options & en.MatchOptions.movingWalls) {
const baseWT = this.wallTop;
const baseWB = this.wallBottom;
this.wallTop = new MovingRectangleClient(baseWT.pos, baseWT.width, baseWT.height, baseWT.baseSpeed, ctx, "grey");
this.wallTop.dir.assign(baseWT.dir.x, baseWT.dir.y);
this.wallBottom = new MovingRectangleClient(baseWB.pos, baseWB.width, baseWB.height, baseWB.baseSpeed, ctx, "grey");
this.wallBottom.dir.assign(baseWB.dir.x, baseWB.dir.y);
}
else {
const baseWT = this.wallTop;
const baseWB = this.wallBottom;
this.wallTop = new RectangleClient(baseWT.pos, baseWT.width, baseWT.height, ctx, "grey");
this.wallBottom = new RectangleClient(baseWB.pos, baseWB.width, baseWB.height, ctx, "grey");
}
}
}
export class GameComponentsClient extends GameComponentsExtensionForClient {
constructor(options, ctx) {
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");
this.text2 = new TextElem(pos, Math.floor(c.w / 24), ctx, "white");
this.text3 = new TextElem(pos, Math.floor(c.w / 24), 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");
}
}

View File

@@ -0,0 +1,115 @@
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");
}
}
}
export class GameComponentsClient extends GameComponentsExtensionForClient {
midLine: Line;
scoreLeft: TextNumericValue;
scoreRight: TextNumericValue;
text1: TextElem;
text2: TextElem;
text3: 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");
this.text2 = new TextElem(pos, Math.floor(c.w/24), ctx, "white");
this.text3 = new TextElem(pos, Math.floor(c.w/24), 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");
}
}

View File

@@ -0,0 +1,7 @@
export class InputHistory {
constructor(inputState, deltaTime) {
this.input = inputState.input;
this.id = inputState.id;
this.deltaTime = deltaTime;
}
}

View File

@@ -0,0 +1,14 @@
import * as en from "../../shared_js/enums.js"
import * as ev from "../../shared_js/class/Event.js"
export 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;
}
}

View File

@@ -0,0 +1,85 @@
import { VectorInteger } from "../../shared_js/class/Vector.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.ctx.fillStyle = this.color;
this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height);
}
function clearRectangle(pos) {
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);
}
export class RectangleClient extends Rectangle {
constructor(pos, width, height, ctx, color) {
super(pos, width, height);
this.ctx = ctx;
this.color = color;
this.update = updateRectangle;
this.clear = clearRectangle;
}
}
export class MovingRectangleClient extends MovingRectangle {
constructor(pos, width, height, baseSpeed, ctx, color) {
super(pos, width, height, baseSpeed);
this.ctx = ctx;
this.color = color;
this.update = updateRectangle;
this.clear = clearRectangle;
}
}
export class RacketClient extends Racket {
constructor(pos, width, height, baseSpeed, ctx, color) {
super(pos, width, height, baseSpeed);
this.ctx = ctx;
this.color = color;
this.update = updateRectangle;
this.clear = clearRectangle;
}
}
export class BallClient extends Ball {
constructor(pos, size, baseSpeed, speedIncrease, ctx, color) {
super(pos, size, baseSpeed, speedIncrease);
this.ctx = ctx;
this.color = color;
this.update = updateRectangle;
this.clear = clearRectangle;
}
bounce(collider) {
this._bounceAlgo(collider);
soundPongArr[Math.floor(random(0, soundPongArr.length))].play();
}
}
function updateLine() {
this.ctx.fillStyle = this.color;
let pos = new VectorInteger;
let i = 0;
while (i < this.segmentCount) {
/* Horizontal Line */
// pos.y = this.pos.y;
// pos.x = this.pos.x + this.segmentWidth * i;
/* Vertical Line */
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;
}
}
export class Line extends RectangleClient {
constructor(pos, width, height, ctx, color, gapeCount) {
super(pos, width, height, ctx, color);
this.gapeCount = 0;
this.update = updateLine;
if (gapeCount)
this.gapeCount = gapeCount;
this.segmentCount = this.gapeCount * 2 + 1;
/* Vertical Line */
this.segmentWidth = this.width;
this.segmentHeight = this.height / this.segmentCount;
/* Horizontal Line */
// this.segmentWidth = this.width / this.segmentCount;
// this.segmentHeight = this.height;
}
}

View File

@@ -0,0 +1,129 @@
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);
}
export 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;
}
}
export 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;
}
}
export 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;
}
}
export 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();
}
}
function updateLine(this: Line) {
this.ctx.fillStyle = this.color;
let pos: VectorInteger = new VectorInteger;
let i = 0;
while (i < this.segmentCount)
{
/* Horizontal Line */
// pos.y = this.pos.y;
// pos.x = this.pos.x + this.segmentWidth * i;
/* Vertical Line */
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;
}
}
export 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;
/* Vertical Line */
this.segmentWidth = this.width;
this.segmentHeight = this.height / this.segmentCount;
/* Horizontal Line */
// this.segmentWidth = this.width / this.segmentCount;
// this.segmentHeight = this.height;
}
}

View File

@@ -0,0 +1,43 @@
import { VectorInteger } from "../../shared_js/class/Vector.js";
// conflict with Text
export class TextElem {
constructor(pos, size, ctx, color, font = "Bit5x3") {
this.text = "";
// 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 ?)
}
}
export class TextNumericValue extends TextElem {
constructor(pos, size, ctx, color, font) {
super(pos, size, ctx, color, font);
this._value = 0;
}
get value() {
return this._value;
}
set value(v) {
this._value = v;
this.text = v.toString();
}
}

View File

@@ -0,0 +1,56 @@
import { Vector, VectorInteger } from "../../shared_js/class/Vector.js";
import { Component } from "../../shared_js/class/interface.js";
// conflict with Text
export 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 ?)
}
}
export 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();
}
}

View File

@@ -0,0 +1,13 @@
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

View 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

35
jeu/src/client/draw.js Normal file
View File

@@ -0,0 +1,35 @@
import { pong, gc } from "./global.js";
import { gridDisplay } from "./handleInput.js";
export function drawLoop() {
pong.clear();
if (gridDisplay) {
drawGrid();
}
drawStatic();
gc.text1.update();
gc.text2.update();
gc.text3.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();
}

49
jeu/src/client/draw.ts Normal file
View File

@@ -0,0 +1,49 @@
import { pong, gc } from "./global.js"
import { gridDisplay } from "./handleInput.js";
export function drawLoop()
{
pong.clear();
if (gridDisplay) {
drawGrid();
}
drawStatic();
gc.text1.update();
gc.text2.update();
gc.text3.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();
}

View File

@@ -0,0 +1,52 @@
import * as c from "./constants.js";
import * as en from "../shared_js/enums.js";
import { gc, matchOptions, clientInfo, clientInfoSpectator } from "./global.js";
import { wallsMovements } from "../shared_js/wallsMovement.js";
let actual_time = Date.now();
let last_time;
let delta_time;
export 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) {
racketInterpolation(delta_time, clientInfo.opponent, clientInfo.opponentNextPos);
}
// 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);
}
}
export function gameLoopSpectator() {
delta_time = c.fixedDeltaTime;
// interpolation
if (gc.playerLeft.dir.y != 0) {
racketInterpolation(delta_time, gc.playerLeft, clientInfoSpectator.playerLeftNextPos);
}
if (gc.playerRight.dir.y != 0) {
racketInterpolation(delta_time, gc.playerRight, clientInfoSpectator.playerRightNextPos);
}
// 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 racketInterpolation(delta, racket, nextPos) {
// interpolation
racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if ((racket.dir.y > 0 && racket.pos.y > nextPos.y)
|| (racket.dir.y < 0 && racket.pos.y < nextPos.y)) {
racket.dir.y = 0;
racket.pos.y = nextPos.y;
}
}

View File

@@ -0,0 +1,71 @@
import * as c from "./constants.js";
import * as en from "../shared_js/enums.js"
import { gc, matchOptions, clientInfo, clientInfoSpectator} from "./global.js";
import { wallsMovements } from "../shared_js/wallsMovement.js";
import { RacketClient } from "./class/RectangleClient.js";
import { VectorInteger } from "../shared_js/class/Vector.js";
let actual_time: number = Date.now();
let last_time: number;
let delta_time: number;
export 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 ) {
racketInterpolation(delta_time, clientInfo.opponent, clientInfo.opponentNextPos);
}
// 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);
}
}
export function gameLoopSpectator()
{
delta_time = c.fixedDeltaTime;
// interpolation
if (gc.playerLeft.dir.y != 0 ) {
racketInterpolation(delta_time, gc.playerLeft, clientInfoSpectator.playerLeftNextPos);
}
if (gc.playerRight.dir.y != 0 ) {
racketInterpolation(delta_time, gc.playerRight, clientInfoSpectator.playerRightNextPos);
}
// 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 racketInterpolation(delta: number, racket: RacketClient, nextPos: VectorInteger)
{
// interpolation
racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if ((racket.dir.y > 0 && racket.pos.y > nextPos.y)
|| (racket.dir.y < 0 && racket.pos.y < nextPos.y))
{
racket.dir.y = 0;
racket.pos.y = nextPos.y;
}
}

19
jeu/src/client/global.js Normal file
View File

@@ -0,0 +1,19 @@
import * as en from "../shared_js/enums.js";
// export {pong, gc, matchOptions} from "./pong.js"
export { socket, clientInfo, clientInfoSpectator } from "./ws.js";
export let pong;
export let gc;
export let matchOptions = en.MatchOptions.noOption;
export function initPong(value) {
pong = value;
}
export function initGc(value) {
gc = value;
}
export function initMatchOptions(value) {
matchOptions = value;
}
export let startFunction;
export function initStartFunction(value) {
startFunction = value;
}

29
jeu/src/client/global.ts Normal file
View File

@@ -0,0 +1,29 @@
import * as en from "../shared_js/enums.js";
import { GameArea } from "./class/GameArea.js";
import { GameComponentsClient } from "./class/GameComponentsClient.js";
// export {pong, gc, matchOptions} from "./pong.js"
export {socket, clientInfo, clientInfoSpectator} from "./ws.js"
export let pong: GameArea;
export let gc: GameComponentsClient;
export let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
export function initPong(value: GameArea) {
pong = value;
}
export function initGc(value: GameComponentsClient) {
gc = value;
}
export function initMatchOptions(value: en.MatchOptions) {
matchOptions = value;
}
export let startFunction: () => void;
export function initStartFunction(value: () => void) {
startFunction = value;
}

View File

@@ -0,0 +1,83 @@
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 = Date.now();
let last_time;
let delta_time;
const inputState = new ev.EventInput();
const inputHistoryArr = [];
// test
/* export function sendLoop()
{
socket.send(JSON.stringify(inputState));
} */
export 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, keys) {
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, input) {
// 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]);
}
export function repeatInput(lastInputId) {
// server reconciliation
let i = inputHistoryArr.findIndex((value) => {
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) => {
if (value.input !== en.InputEnum.noInput) {
playerMovePrediction(value.deltaTime, value.input);
}
});
}

View File

@@ -0,0 +1,108 @@
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));
} */
export 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]);
}
export 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);
}
});
}

65
jeu/src/client/message.js Normal file
View File

@@ -0,0 +1,65 @@
import * as c from "./constants.js";
import { gc } from "./global.js";
import * as en from "../shared_js/enums.js";
/*
before game
*/
export function matchmaking() {
const text = "searching...";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w * 0.2, c.h * 0.5);
gc.text1.text = text;
gc.text1.update();
}
export function matchmakingComplete() {
const text = "match found !";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w * 0.15, c.h * 0.5);
gc.text1.text = text;
gc.text1.update();
}
export function matchAbort() {
const text = "match abort";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w * 0.15, c.h * 0.5);
gc.text1.text = text;
gc.text1.update();
setTimeout(() => {
gc.text2.pos.assign(c.w * 0.44, c.h * 0.6);
gc.text2.text = "pardon =(";
const oriSize = gc.text2.size;
gc.text2.size = c.w * 0.025;
gc.text2.update();
gc.text2.size = oriSize;
}, 2500);
}
/*
in game
*/
export function win() {
gc.text1.pos.assign(c.w * 0.415, c.h * 0.5);
gc.text1.text = "WIN";
}
export function lose() {
gc.text1.pos.assign(c.w * 0.383, c.h * 0.5);
gc.text1.text = "LOSE";
}
export function forfeit(playerSide) {
if (playerSide === en.PlayerSide.left) {
gc.text2.pos.assign(c.w * 0.65, c.h * 0.42);
gc.text3.pos.assign(c.w * 0.65, c.h * 0.52);
}
else {
gc.text2.pos.assign(c.w * 0.09, c.h * 0.42);
gc.text3.pos.assign(c.w * 0.09, c.h * 0.52);
}
setTimeout(() => {
gc.text2.text = "par forfait";
}, 1500);
setTimeout(() => {
gc.text3.text = "calme ta joie";
}, 3500);
}

80
jeu/src/client/message.ts Normal file
View File

@@ -0,0 +1,80 @@
import * as c from "./constants.js"
import { gc } from "./global.js"
import * as en from "../shared_js/enums.js"
/*
before game
*/
export function matchmaking()
{
const text = "searching...";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w*0.2, c.h*0.5);
gc.text1.text = text;
gc.text1.update();
}
export function matchmakingComplete()
{
const text = "match found !";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w*0.15, c.h*0.5);
gc.text1.text = text;
gc.text1.update();
}
export function matchAbort()
{
const text = "match abort";
console.log(text);
gc.text1.clear();
gc.text1.pos.assign(c.w*0.15, c.h*0.5);
gc.text1.text = text;
gc.text1.update();
setTimeout(() => {
gc.text2.pos.assign(c.w*0.44, c.h*0.6);
gc.text2.text = "pardon =(";
const oriSize = gc.text2.size;
gc.text2.size = c.w*0.025;
gc.text2.update();
gc.text2.size = oriSize;
}, 2500);
}
/*
in game
*/
export function win()
{
gc.text1.pos.assign(c.w*0.415, c.h*0.5);
gc.text1.text = "WIN";
}
export function lose()
{
gc.text1.pos.assign(c.w*0.383, c.h*0.5);
gc.text1.text = "LOSE";
}
export function forfeit(playerSide: en.PlayerSide)
{
if (playerSide === en.PlayerSide.left) {
gc.text2.pos.assign(c.w*0.65, c.h*0.42);
gc.text3.pos.assign(c.w*0.65, c.h*0.52);
}
else {
gc.text2.pos.assign(c.w*0.09, c.h*0.42);
gc.text3.pos.assign(c.w*0.09, c.h*0.52);
}
setTimeout(() => {
gc.text2.text = "par forfait";
}, 1500);
setTimeout(() => {
gc.text3.text = "calme ta joie";
}, 3500);
}

54
jeu/src/client/pong.css Normal file
View File

@@ -0,0 +1,54 @@
@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;
}
body {
margin: 0;
background-color: #222425;
}
#canvas_container {
margin-top: 20px;
text-align: center;
/* border: dashed rgb(245, 245, 245) 5px; */
/* max-height: 80vh; */
/* overflow: hidden; */
}
#div_game_instructions {
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: large;
}
#div_game_options {
margin-top: 20px;
text-align: center;
font-family: "Bit5x3";
color: rgb(245, 245, 245);
font-size: x-large;
}
#div_game_options fieldset {
max-width: 50vw;
width: auto;
margin: 0 auto;
}
#div_game_options fieldset div {
padding: 10px;
}
#play_pong_button {
font-family: "Bit5x3";
color: rgb(245, 245, 245);
background-color: #333333;
font-size: x-large;
padding: 10px;
}
canvas {
background-color: #333333;
max-width: 75vw;
/* max-height: 100vh; */
width: 80%;
}

47
jeu/src/client/pong.html Normal file
View File

@@ -0,0 +1,47 @@
<!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="div_game_options">
<fieldset>
<legend>game options</legend>
<div>
<input type="checkbox" id="multi_balls" name="multi_balls">
<label for="multi_balls">multiples balls</label>
</div>
<div>
<input type="checkbox" id="moving_walls" name="moving_walls">
<label for="moving_walls">moving walls</label>
</div>
<div>
<label>sound :</label>
<input type="radio" id="sound_on" name="sound_selector" checked>
<label for="sound_on">on</label>
<input type="radio" id="sound_off" name="sound_selector">
<label for="sound_off">off</label>
</div>
<div>
<button id="play_pong_button">PLAY</button>
</div>
</fieldset>
</div>
<div id="div_game_instructions">
<h2>--- keys ---</h2>
<p>move up: 'w' or 'up arrow'</p>
<p>move down: 's' OR 'down arrow'</p>
<p>grid on/off: 'g'</p>
</div>
<div id="canvas_container">
<!-- <p> =) </p> -->
</div>
<script src="http://localhost:8080/js/pong.js" type="module" defer></script>
</body>
</html>

63
jeu/src/client/pong.js Normal file
View File

@@ -0,0 +1,63 @@
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";
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
import { pong, gc } from "./global.js";
import { initPong, initGc, initMatchOptions, initStartFunction } from "./global.js";
function init() {
console.log("multi_balls:" + document.getElementById("multi_balls").checked);
console.log("moving_walls:" + document.getElementById("moving_walls").checked);
console.log("sound_on:" + document.getElementById("sound_on").checked);
let soundMutedFlag = false;
if (document.getElementById("sound_off").checked) {
soundMutedFlag = true;
}
initAudio(soundMutedFlag);
let matchOptions = en.MatchOptions.noOption;
if (document.getElementById("multi_balls").checked) {
matchOptions |= en.MatchOptions.multiBalls;
}
if (document.getElementById("moving_walls").checked) {
matchOptions |= en.MatchOptions.movingWalls;
}
initMatchOptions(matchOptions);
document.getElementById("div_game_options").remove();
document.getElementById("div_game_instructions").remove();
initPong(new GameArea());
initGc(new GameComponentsClient(matchOptions, pong.ctx));
initStartFunction(start);
initWebSocket(matchOptions);
}
function start() {
gc.text1.pos.assign(c.w * 0.5, c.h * 0.75);
countdown(c.matchStartDelay / 1000, (count) => {
gc.text1.clear();
gc.text1.text = `${count}`;
gc.text1.update();
}, resume);
}
function resume() {
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);
}

78
jeu/src/client/pong.ts Normal file
View File

@@ -0,0 +1,78 @@
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";
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
import { pong, gc } from "./global.js"
import { initPong, initGc, initMatchOptions, initStartFunction } from "./global.js"
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);
let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
matchOptions |= en.MatchOptions.multiBalls;
}
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
matchOptions |= en.MatchOptions.movingWalls;
}
initMatchOptions(matchOptions);
document.getElementById("div_game_options").remove();
document.getElementById("div_game_instructions").remove();
initPong(new GameArea());
initGc(new GameComponentsClient(matchOptions, pong.ctx));
initStartFunction(start);
initWebSocket(matchOptions);
}
function start()
{
gc.text1.pos.assign(c.w*0.5, c.h*0.75);
countdown(c.matchStartDelay/1000, (count: number) => {
gc.text1.clear();
gc.text1.text = `${count}`;
gc.text1.update();
}, resume);
}
function resume()
{
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);
}

View File

@@ -0,0 +1,19 @@
<!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="canvas_container">
<!-- <p> =) </p> -->
</div>
<div>
<h2>Spectator View</h2>
</div>
<script src="http://localhost:8080/js/pongSpectator.js" type="module" defer></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
initSpectator();
function initSpectator() {
// Wip
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 { gameLoopSpectator } from "./gameLoop.js";
import { drawLoop } from "./draw.js";
import { initWebSocketSpectator } from "./ws.js";
import { initAudio } from "./audio.js";
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
import { pong } from "./global.js";
import { initPong, initGc, initMatchOptions, initStartFunction } from "./global.js";
function init() {
initAudio(false);
// WIP matchOptions
let matchOptions = en.MatchOptions.noOption;
initMatchOptions(matchOptions);
initPong(new GameArea());
initGc(new GameComponentsClient(matchOptions, pong.ctx));
initStartFunction(start);
initWebSocketSpectator(c.gameSessionIdPLACEHOLDER);
}
function start() {
resume();
}
function resume() {
pong.gameLoopInterval = window.setInterval(gameLoopSpectator, c.gameLoopIntervalMS);
pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
}

View File

@@ -0,0 +1,46 @@
initSpectator();
function initSpectator() {
// Wip
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 { gameLoopSpectator } from "./gameLoop.js"
import { drawLoop } from "./draw.js";
import { initWebSocketSpectator } from "./ws.js";
import { initAudio } from "./audio.js";
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
import { pong, gc } from "./global.js"
import { initPong, initGc, initMatchOptions, initStartFunction } from "./global.js"
function init()
{
initAudio(false);
// WIP matchOptions
let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
initMatchOptions(matchOptions);
initPong(new GameArea());
initGc(new GameComponentsClient(matchOptions, pong.ctx));
initStartFunction(start);
initWebSocketSpectator(c.gameSessionIdPLACEHOLDER);
}
function start()
{
resume();
}
function resume()
{
pong.gameLoopInterval = window.setInterval(gameLoopSpectator, c.gameLoopIntervalMS);
pong.drawLoopInterval = window.setInterval(drawLoop, c.drawLoopIntervalMS);
}

13
jeu/src/client/utils.js Normal file
View File

@@ -0,0 +1,13 @@
export * from "../shared_js/utils.js";
export function countdown(count, callback, endCallback) {
console.log("countdown ", count);
if (count > 0) {
if (callback) {
callback(count);
}
setTimeout(countdown, 1000, --count, callback, endCallback);
}
else if (endCallback) {
endCallback();
}
}

16
jeu/src/client/utils.ts Normal file
View File

@@ -0,0 +1,16 @@
export * from "../shared_js/utils.js"
export 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();
}
}

230
jeu/src/client/ws.js Normal file
View File

@@ -0,0 +1,230 @@
import { gc, matchOptions, startFunction } from "./global.js";
import * as ev from "../shared_js/class/Event.js";
import * as en from "../shared_js/enums.js";
import * as msg from "./message.js";
import { repeatInput } from "./handleInput.js";
import { soundRoblox } from "./audio.js";
import { Vector, VectorInteger } from "../shared_js/class/Vector.js";
class ClientInfo {
constructor() {
this.id = "";
}
}
class ClientInfoSpectator {
}
const wsPort = 8042;
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong";
export let socket; /* TODO: A way to still use "const" not "let" ? */
export const clientInfo = new ClientInfo();
export const clientInfoSpectator = new ClientInfoSpectator(); // WIP, could refactor this
export function initWebSocket(options) {
socket = new WebSocket(wsUrl, "json");
socket.addEventListener("open", (event) => {
socket.send(JSON.stringify(new ev.ClientAnnouncePlayer(options, clientInfo.id)));
});
// socket.addEventListener("message", logListener); // for testing purpose
socket.addEventListener("message", preMatchListener);
}
function logListener(event) {
console.log("%i: " + event.data, Date.now());
}
function preMatchListener(event) {
const data = JSON.parse(event.data);
switch (data.type) {
case en.EventTypes.assignId:
clientInfo.id = data.id;
break;
case en.EventTypes.matchmakingInProgress:
msg.matchmaking();
break;
case en.EventTypes.matchmakingComplete:
clientInfo.side = data.side;
if (clientInfo.side === en.PlayerSide.left) {
clientInfo.racket = gc.playerLeft;
clientInfo.opponent = gc.playerRight;
}
else if (clientInfo.side === en.PlayerSide.right) {
clientInfo.racket = gc.playerRight;
clientInfo.opponent = gc.playerLeft;
}
clientInfo.opponentNextPos = new VectorInteger(clientInfo.opponent.pos.x, clientInfo.opponent.pos.y);
clientInfo.racket.color = "darkgreen"; // for testing purpose
socket.send(JSON.stringify(new ev.ClientEvent(en.EventTypes.clientPlayerReady))); // TODO: set an interval/timeout to resend until matchStart response (in case of network problem)
msg.matchmakingComplete();
break;
case en.EventTypes.matchStart:
socket.removeEventListener("message", preMatchListener);
socket.addEventListener("message", inGameListener);
startFunction();
break;
case en.EventTypes.matchAbort:
socket.removeEventListener("message", preMatchListener);
msg.matchAbort();
break;
}
}
function inGameListener(event) {
const data = 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);
break;
case en.EventTypes.scoreUpdate:
scoreUpdate(data);
break;
case en.EventTypes.matchEnd:
matchEnd(data);
break;
}
}
function gameUpdate(data) {
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) {
// 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) {
if (data.winner === clientInfo.side) {
msg.win();
if (data.forfeit) {
msg.forfeit(clientInfo.side);
}
}
else {
msg.lose();
}
// matchEnded = true; // unused
}
// export let matchEnded = false; // unused
/* Spectator */
export function initWebSocketSpectator(gameSessionId) {
socket = new WebSocket(wsUrl, "json");
socket.addEventListener("open", (event) => {
socket.send(JSON.stringify(new ev.ClientAnnounceSpectator(gameSessionId)));
});
// socket.addEventListener("message", logListener); // for testing purpose
socket.addEventListener("message", preMatchListenerSpectator);
clientInfoSpectator.playerLeftNextPos = new VectorInteger(gc.playerLeft.pos.x, gc.playerLeft.pos.y);
clientInfoSpectator.playerRightNextPos = new VectorInteger(gc.playerRight.pos.x, gc.playerRight.pos.y);
}
export function preMatchListenerSpectator(event) {
const data = JSON.parse(event.data);
if (data.type === en.EventTypes.matchStart) {
socket.removeEventListener("message", preMatchListenerSpectator);
socket.addEventListener("message", inGameListenerSpectator);
startFunction();
}
}
function inGameListenerSpectator(event) {
const data = JSON.parse(event.data);
switch (data.type) {
case en.EventTypes.gameUpdate:
gameUpdateSpectator(data);
break;
case en.EventTypes.scoreUpdate:
scoreUpdateSpectator(data);
break;
case en.EventTypes.matchEnd:
matchEndSpectator(data);
break;
}
}
function gameUpdateSpectator(data) {
console.log("gameUpdateSpectator");
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;
});
// interpolation
for (const racket of [gc.playerLeft, gc.playerRight]) {
let nextPos;
if (racket === gc.playerLeft) {
nextPos = clientInfoSpectator.playerLeftNextPos;
}
else {
nextPos = clientInfoSpectator.playerRightNextPos;
}
racket.pos.assign(nextPos.x, nextPos.y);
if (racket === gc.playerLeft) {
nextPos.assign(racket.pos.x, data.playerLeft.y);
}
else {
nextPos.assign(racket.pos.x, data.playerRight.y);
}
racket.dir = new Vector(nextPos.x - racket.pos.x, nextPos.y - racket.pos.y);
if (Math.abs(racket.dir.x) + Math.abs(racket.dir.y) !== 0) {
racket.dir = racket.dir.normalized();
}
}
}
function scoreUpdateSpectator(data) {
console.log("scoreUpdateSpectator");
gc.scoreLeft.value = data.scoreLeft;
gc.scoreRight.value = data.scoreRight;
}
function matchEndSpectator(data) {
console.log("matchEndSpectator");
// WIP
/* msg.win();
if (data.forfeit) {
msg.forfeit(clientInfo.side);
} */
}

302
jeu/src/client/ws.ts Normal file
View File

@@ -0,0 +1,302 @@
import * as c from "./constants.js"
import { gc, matchOptions, startFunction } from "./global.js"
import * as ev from "../shared_js/class/Event.js"
import * as en from "../shared_js/enums.js"
import * as msg from "./message.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;
}
class ClientInfoSpectator {
// side: en.PlayerSide;
/* WIP: playerLeftNextPos and playerRightNextPos could be in clientInfo for simplicity */
playerLeftNextPos: VectorInteger;
playerRightNextPos: 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 const clientInfoSpectator = new ClientInfoSpectator(); // WIP, could refactor this
export function initWebSocket(options: en.MatchOptions)
{
socket = new WebSocket(wsUrl, "json");
socket.addEventListener("open", (event) => {
socket.send(JSON.stringify( new ev.ClientAnnouncePlayer(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:
msg.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)
msg.matchmakingComplete();
break;
case en.EventTypes.matchStart:
socket.removeEventListener("message", preMatchListener);
socket.addEventListener("message", inGameListener);
startFunction();
break;
case en.EventTypes.matchAbort:
socket.removeEventListener("message", preMatchListener);
msg.matchAbort();
break;
}
}
function inGameListener(this: WebSocket, 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) {
msg.win();
if (data.forfeit) {
msg.forfeit(clientInfo.side);
}
}
else {
msg.lose();
}
// matchEnded = true; // unused
}
// export let matchEnded = false; // unused
/* Spectator */
export function initWebSocketSpectator(gameSessionId: string)
{
socket = new WebSocket(wsUrl, "json");
socket.addEventListener("open", (event) => {
socket.send(JSON.stringify( new ev.ClientAnnounceSpectator(gameSessionId) ));
});
// socket.addEventListener("message", logListener); // for testing purpose
socket.addEventListener("message", preMatchListenerSpectator);
clientInfoSpectator.playerLeftNextPos = new VectorInteger(gc.playerLeft.pos.x, gc.playerLeft.pos.y);
clientInfoSpectator.playerRightNextPos = new VectorInteger(gc.playerRight.pos.x, gc.playerRight.pos.y);
}
export function preMatchListenerSpectator(this: WebSocket, event: MessageEvent)
{
const data: ev.ServerEvent = JSON.parse(event.data);
if (data.type === en.EventTypes.matchStart)
{
socket.removeEventListener("message", preMatchListenerSpectator);
socket.addEventListener("message", inGameListenerSpectator);
startFunction();
}
}
function inGameListenerSpectator(this: WebSocket, event: MessageEvent)
{
const data: ev.ServerEvent = JSON.parse(event.data);
switch (data.type) {
case en.EventTypes.gameUpdate:
gameUpdateSpectator(data as ev.EventGameUpdate);
break;
case en.EventTypes.scoreUpdate:
scoreUpdateSpectator(data as ev.EventScoreUpdate);
break;
case en.EventTypes.matchEnd:
matchEndSpectator(data as ev.EventMatchEnd);
break;
}
}
function gameUpdateSpectator(data: ev.EventGameUpdate)
{
console.log("gameUpdateSpectator");
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;
});
// interpolation
for (const racket of [gc.playerLeft, gc.playerRight])
{
let nextPos: VectorInteger;
if (racket === gc.playerLeft) {
nextPos = clientInfoSpectator.playerLeftNextPos;
}
else {
nextPos = clientInfoSpectator.playerRightNextPos;
}
racket.pos.assign(nextPos.x, nextPos.y);
if (racket === gc.playerLeft) {
nextPos.assign(racket.pos.x, data.playerLeft.y);
}
else {
nextPos.assign(racket.pos.x, data.playerRight.y);
}
racket.dir = new Vector(
nextPos.x - racket.pos.x,
nextPos.y - racket.pos.y
);
if (Math.abs(racket.dir.x) + Math.abs(racket.dir.y) !== 0) {
racket.dir = racket.dir.normalized();
}
}
}
function scoreUpdateSpectator(data: ev.EventScoreUpdate)
{
console.log("scoreUpdateSpectator");
gc.scoreLeft.value = data.scoreLeft;
gc.scoreRight.value = data.scoreRight;
}
function matchEndSpectator(data: ev.EventMatchEnd)
{
console.log("matchEndSpectator");
// WIP
/* msg.win();
if (data.forfeit) {
msg.forfeit(clientInfo.side);
} */
}

View File

@@ -0,0 +1,23 @@
import * as ev from "../../shared_js/class/Event.js";
export class Client {
constructor(socket, id) {
this.isAlive = true;
this.gameSession = null;
this.socket = socket;
this.id = id;
}
}
export class ClientPlayer extends Client {
constructor(socket, id, racket) {
super(socket, id);
this.matchOptions = 0;
this.inputBuffer = new ev.EventInput();
this.lastInputId = 0;
this.racket = racket;
}
}
export class ClientSpectator extends Client {
constructor(socket, id) {
super(socket, id);
}
}

View File

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

View File

@@ -0,0 +1,8 @@
import { GameComponents } from "../../shared_js/class/GameComponents.js";
export class GameComponentsServer extends GameComponents {
constructor(options) {
super(options);
this.scoreLeft = 0;
this.scoreRight = 0;
}
}

View File

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

View File

@@ -0,0 +1,212 @@
import * as en from "../../shared_js/enums.js";
import * as ev from "../../shared_js/class/Event.js";
import * as c from "../constants.js";
import { GameComponentsServer } from "./GameComponentsServer.js";
import { clientInputListener } from "../wsServer.js";
import { random } from "../utils.js";
import { wallsMovements } from "../../shared_js/wallsMovement.js";
/*
multiples methods of GameSession have parameter "s: GameSession".
its used with calls to setTimeout(),
because "this" is not equal to the GameSession but to "this: Timeout"
*/
export class GameSession {
constructor(id, matchOptions) {
this.playersMap = new Map();
this.unreadyPlayersMap = new Map();
this.spectatorsMap = new Map();
this.gameLoopInterval = 0;
this.playersUpdateInterval = 0;
this.spectatorsUpdateInterval = 0;
this.matchEnded = false;
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) {
s.playersMap.forEach((client) => {
client.socket.on("message", clientInputListener);
});
s.actual_time = Date.now();
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
s.playersUpdateInterval = setInterval(s._playersUpdate, c.playersUpdateIntervalMS, s);
s.spectatorsUpdateInterval = setInterval(s._spectatorsUpdate, c.spectatorsUpdateIntervalMS, s);
}
pause(s) {
s.playersMap.forEach((client) => {
client.socket.off("message", clientInputListener);
});
clearInterval(s.gameLoopInterval);
clearInterval(s.playersUpdateInterval);
clearInterval(s.spectatorsUpdateInterval);
}
instantInputDebug(client) {
this._handleInput(c.fixedDeltaTime, client);
}
_handleInput(delta, client) {
// 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;
}
_gameLoop(s) {
/* 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);
}
}
_ballMovement(delta, ball) {
const gc = this.components;
if (ball.ballInPlay) {
ball.moveAndBounce(delta, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
if (ball.pos.x > c.w
|| ball.pos.x < 0 - ball.width) {
ball.ballInPlay = false;
if (this.matchEnded) {
return;
}
this._scoreUpdate(ball);
setTimeout(this._newRound, c.newRoundDelay, this, ball);
}
}
}
_scoreUpdate(ball) {
const gc = this.components;
if (ball.pos.x > c.w) {
++gc.scoreLeft;
}
else if (ball.pos.x < 0 - ball.width) {
++gc.scoreRight;
}
this.playersMap.forEach((client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
this.spectatorsMap.forEach((client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
}
_playersUpdate(s) {
const gameState = s._gameStateSnapshot();
s.playersMap.forEach((client) => {
gameState.lastInputId = client.lastInputId;
client.socket.send(JSON.stringify(gameState));
});
}
_spectatorsUpdate(s) {
const gameState = s._gameStateSnapshot();
s.spectatorsMap.forEach((client) => {
client.socket.send(JSON.stringify(gameState));
});
}
_gameStateSnapshot() {
const gc = this.components;
const snapshot = new ev.EventGameUpdate();
snapshot.playerLeft.y = gc.playerLeft.pos.y;
snapshot.playerRight.y = gc.playerRight.pos.y;
gc.ballsArr.forEach((ball) => {
snapshot.ballsArr.push({
x: ball.pos.x,
y: ball.pos.y,
dirX: ball.dir.x,
dirY: ball.dir.y,
speed: ball.speed
});
});
if (this.matchOptions & en.MatchOptions.movingWalls) {
snapshot.wallTop.y = gc.wallTop.pos.y;
snapshot.wallBottom.y = gc.wallBottom.pos.y;
}
return (snapshot);
}
_newRound(s, ball) {
if (s._checkDisconnexions()) {
return;
}
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
const gc = s.components;
const minScore = 11; // can be changed for testing
if (gc.scoreLeft >= minScore || gc.scoreRight >= minScore) {
if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2) {
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;
}
_checkDisconnexions() {
if (this.playersMap.size !== 2) {
this.matchEnded = true;
if (this.playersMap.size != 0) {
const gc = this.components;
const luckyWinner = this.playersMap.values().next().value;
let eventEnd;
if (luckyWinner.racket === gc.playerLeft) {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.left, true);
console.log("Player Left WIN (by forfeit)");
}
else {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.right, true);
console.log("Player Right WIN (by forfeit)");
}
luckyWinner.socket.send(JSON.stringify(eventEnd));
this.spectatorsMap.forEach((client) => {
client.socket.send(JSON.stringify(eventEnd));
});
}
return true;
}
return false;
}
_matchEnd(s) {
s.matchEnded = true;
const gc = s.components;
let eventEnd;
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));
});
s.spectatorsMap.forEach((client) => {
client.socket.send(JSON.stringify(eventEnd));
});
}
}

View File

@@ -0,0 +1,241 @@
import * as en from "../../shared_js/enums.js"
import * as ev from "../../shared_js/class/Event.js"
import * as c from "../constants.js"
import { ClientPlayer, ClientSpectator } from "./Client";
import { GameComponentsServer } from "./GameComponentsServer.js";
import { clientInputListener } from "../wsServer.js";
import { random } from "../utils.js";
import { Ball } from "../../shared_js/class/Rectangle.js";
import { wallsMovements } from "../../shared_js/wallsMovement.js";
/*
multiples methods of GameSession have parameter "s: GameSession".
its used with calls to setTimeout(),
because "this" is not equal to the GameSession but to "this: Timeout"
*/
export class GameSession {
id: string; // url ?
playersMap: Map<string, ClientPlayer> = new Map();
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
spectatorsMap: Map<string, ClientSpectator> = new Map();
gameLoopInterval: NodeJS.Timer | number = 0;
playersUpdateInterval: NodeJS.Timer | number = 0;
spectatorsUpdateInterval: NodeJS.Timer | number = 0;
components: GameComponentsServer;
matchOptions: en.MatchOptions;
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.playersUpdateInterval = setInterval(s._playersUpdate, c.playersUpdateIntervalMS, s);
s.spectatorsUpdateInterval = setInterval(s._spectatorsUpdate, c.spectatorsUpdateIntervalMS, s);
}
pause(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.off("message", clientInputListener);
});
clearInterval(s.gameLoopInterval);
clearInterval(s.playersUpdateInterval);
clearInterval(s.spectatorsUpdateInterval);
}
instantInputDebug(client: ClientPlayer) {
this._handleInput(c.fixedDeltaTime, client);
}
private _handleInput(delta: number, client: ClientPlayer) {
// if (client.inputBuffer === null) {return;}
const gc = this.components;
const input = client.inputBuffer.input;
if (input === en.InputEnum.up) {
client.racket.dir.y = -1;
}
else if (input === en.InputEnum.down) {
client.racket.dir.y = 1;
}
if (input !== en.InputEnum.noInput) {
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
}
client.lastInputId = client.inputBuffer.id;
// client.inputBuffer = null;
}
private _gameLoop(s: GameSession) {
/* s.last_time = s.actual_time;
s.actual_time = Date.now();
s.delta_time = (s.actual_time - s.last_time) / 1000; */
s.delta_time = c.fixedDeltaTime;
// WIP, replaced by instantInputDebug() to prevent desynchro
/* s.playersMap.forEach( (client) => {
s._handleInput(s.delta_time, client);
}); */
const gc = s.components;
gc.ballsArr.forEach((ball) => {
s._ballMovement(s.delta_time, ball);
});
if (s.matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(s.delta_time, gc);
}
}
private _ballMovement(delta: number, ball: Ball) {
const gc = this.components;
if (ball.ballInPlay)
{
ball.moveAndBounce(delta, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
if (ball.pos.x > c.w
|| ball.pos.x < 0 - ball.width)
{
ball.ballInPlay = false;
if (this.matchEnded) {
return;
}
this._scoreUpdate(ball);
setTimeout(this._newRound, c.newRoundDelay, this, ball);
}
}
}
private _scoreUpdate(ball: Ball) {
const gc = this.components;
if (ball.pos.x > c.w) {
++gc.scoreLeft;
}
else if (ball.pos.x < 0 - ball.width) {
++gc.scoreRight;
}
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
}
private _playersUpdate(s: GameSession) {
const gameState: ev.EventGameUpdate = s._gameStateSnapshot();
s.playersMap.forEach( (client) => {
gameState.lastInputId = client.lastInputId;
client.socket.send(JSON.stringify(gameState));
});
}
private _spectatorsUpdate(s: GameSession) {
const gameState = s._gameStateSnapshot();
s.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(gameState));
});
}
private _gameStateSnapshot() : ev.EventGameUpdate {
const gc = this.components;
const snapshot = new ev.EventGameUpdate();
snapshot.playerLeft.y = gc.playerLeft.pos.y;
snapshot.playerRight.y = gc.playerRight.pos.y;
gc.ballsArr.forEach((ball) => {
snapshot.ballsArr.push({
x: ball.pos.x,
y: ball.pos.y,
dirX: ball.dir.x,
dirY: ball.dir.y,
speed: ball.speed
});
});
if (this.matchOptions & en.MatchOptions.movingWalls) {
snapshot.wallTop.y = gc.wallTop.pos.y;
snapshot.wallBottom.y = gc.wallBottom.pos.y;
}
return (snapshot);
}
private _newRound(s: GameSession, ball: Ball) {
if (s._checkDisconnexions()) {
return;
}
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
const gc = s.components;
const minScore = 11;// can be changed for testing
if (gc.scoreLeft >= minScore || gc.scoreRight >= minScore)
{
if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2)
{
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 _checkDisconnexions() {
if (this.playersMap.size !== 2)
{
this.matchEnded = true;
if (this.playersMap.size != 0)
{
const gc = this.components;
const luckyWinner: ClientPlayer = this.playersMap.values().next().value;
let eventEnd: ev.EventMatchEnd;
if (luckyWinner.racket === gc.playerLeft) {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.left, true);
console.log("Player Left WIN (by forfeit)");
}
else {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.right, true);
console.log("Player Right WIN (by forfeit)");
}
luckyWinner.socket.send(JSON.stringify(eventEnd));
this.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
}
return true;
}
return false;
}
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));
});
s.spectatorsMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
}
}

View File

@@ -0,0 +1,7 @@
export * from "../shared_js/constants.js";
// 15ms == 1000/66.666
export const serverGameLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = serverGameLoopIntervalMS / 1000; // second
// 33.333ms == 1000/30
export const playersUpdateIntervalMS = 1000 / 30; // millisecond
export const spectatorsUpdateIntervalMS = 1000 / 30; // millisecond

View File

@@ -0,0 +1,10 @@
export * from "../shared_js/constants.js"
// 15ms == 1000/66.666
export const serverGameLoopIntervalMS = 15; // millisecond
export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
// 33.333ms == 1000/30
export const playersUpdateIntervalMS = 1000/30; // millisecond
export const spectatorsUpdateIntervalMS = 1000/30; // millisecond

37
jeu/src/server/server.js Normal file
View File

@@ -0,0 +1,37 @@
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`);
});

41
jeu/src/server/server.ts Normal file
View 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`);
});

4
jeu/src/server/utils.js Normal file
View File

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

6
jeu/src/server/utils.ts Normal file
View File

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

208
jeu/src/server/wsServer.js Normal file
View File

@@ -0,0 +1,208 @@
import { WebSocketServer, WebSocket as BaseLibWebSocket } from "ws";
export class WebSocket extends BaseLibWebSocket {
}
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 } from "./class/Client.js";
import { GameSession } from "./class/GameSession.js";
import { shortId } from "./utils.js";
// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ?
const wsPort = 8042;
export const wsServer = new WebSocketServer({ port: wsPort, path: "/pong" });
const clientsMap = new Map; // socket.id/Client
const matchmakingPlayersMap = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const gameSessionsMap = new Map; // GameSession.id(url)/GameSession
wsServer.on("connection", connectionListener);
wsServer.on("error", errorListener);
wsServer.on("close", closeListener);
function connectionListener(socket, request) {
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) {
try {
const event = JSON.parse(data);
if (event.type === en.EventTypes.clientInput) {
return;
}
}
catch (e) { }
console.log("data: " + data);
});
socket.once("message", clientAnnounceListener);
}
function clientAnnounceListener(data) {
try {
const msg = JSON.parse(data);
if (msg.type === en.EventTypes.clientAnnounce) {
// TODO: reconnection with msg.clientId ?
// "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that)
if (msg.role === en.ClientRole.player) {
const announce = msg;
const player = clientsMap.get(this.id);
player.matchOptions = announce.matchOptions;
this.send(JSON.stringify(new ev.EventAssignId(this.id)));
this.send(JSON.stringify(new ev.ServerEvent(en.EventTypes.matchmakingInProgress)));
matchmaking(player);
}
else if (msg.role === en.ClientRole.spectator) {
const announce = msg;
const gameSession = gameSessionsMap.get(announce.gameSessionId);
if (!gameSession) {
// WIP: send "invalid game session"
return;
}
const spectator = clientsMap.get(this.id);
spectator.gameSession = gameSession;
gameSession.spectatorsMap.set(spectator.id, spectator);
this.send(JSON.stringify(new ev.ServerEvent(en.EventTypes.matchStart)));
}
}
else {
console.log("Invalid ClientAnnounce");
}
return;
}
catch (e) {
console.log("Invalid JSON (clientAnnounceListener)");
}
this.once("message", clientAnnounceListener);
}
function matchmaking(player) {
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
const matchOptions = player.matchOptions;
matchmakingPlayersMap.set(player.id, player);
const compatiblePlayers = [];
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 = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR
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)));
setTimeout(function abortMatch() {
if (gameSession.unreadyPlayersMap.size !== 0) {
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify(new ev.ServerEvent(en.EventTypes.matchAbort)));
client.gameSession = null;
clientTerminate(client);
});
}
}, 5000);
}
function playerReadyConfirmationListener(data) {
try {
const msg = 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(data) {
try {
// const input: ev.ClientEvent = JSON.parse(data);
const input = JSON.parse(data);
if (input.type === en.EventTypes.clientInput) {
const client = clientsMap.get(this.id);
client.inputBuffer = input;
client.gameSession.instantInputDebug(client); // wip
}
else {
console.log("Invalid clientInput");
}
}
catch (e) {
console.log("Invalid JSON (clientInputListener)");
}
}
////////////
////////////
const pingInterval = setInterval(() => {
let deleteLog = "";
clientsMap.forEach((client) => {
if (!client.isAlive) {
clientTerminate(client);
deleteLog += ` ${shortId(client.id)} |`;
}
else {
client.isAlive = false;
client.socket.ping();
}
});
if (deleteLog) {
console.log(`Disconnected:${deleteLog}`);
}
console.log("gameSessionMap size: " + gameSessionsMap.size);
console.log("clientsMap size: " + clientsMap.size);
console.log("matchmakingPlayersMap size: " + matchmakingPlayersMap.size);
console.log("");
}, 4200);
function clientTerminate(client) {
client.socket.terminate();
if (client.gameSession) {
client.gameSession.playersMap.delete(client.id);
if (client.gameSession.playersMap.size === 0) {
clearInterval(client.gameSession.playersUpdateInterval);
clearInterval(client.gameSession.spectatorsUpdateInterval);
clearInterval(client.gameSession.gameLoopInterval);
gameSessionsMap.delete(client.gameSession.id);
}
}
clientsMap.delete(client.id);
if (matchmakingPlayersMap.has(client.id)) {
matchmakingPlayersMap.delete(client.id);
}
}
function closeListener() {
clearInterval(pingInterval);
}
function errorListener(error) {
console.log("Error: " + JSON.stringify(error));
}

266
jeu/src/server/wsServer.ts Normal file
View File

@@ -0,0 +1,266 @@
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, ClientSpectator } from "./class/Client.js"
import { GameSession } from "./class/GameSession.js"
import { shortId } from "./utils.js";
import { gameSessionIdPLACEHOLDER } from "./constants.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 ?
// "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that)
if (msg.role === en.ClientRole.player)
{
const announce: ev.ClientAnnouncePlayer = <ev.ClientAnnouncePlayer>msg;
const player = clientsMap.get(this.id) as ClientPlayer;
player.matchOptions = announce.matchOptions;
this.send(JSON.stringify( new ev.EventAssignId(this.id) ));
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
matchmaking(player);
}
else if (msg.role === en.ClientRole.spectator)
{
const announce: ev.ClientAnnounceSpectator = <ev.ClientAnnounceSpectator>msg;
const gameSession = gameSessionsMap.get(announce.gameSessionId);
if (!gameSession) {
// WIP: send "invalid game session"
return;
}
const spectator = clientsMap.get(this.id) as ClientSpectator;
spectator.gameSession = gameSession;
gameSession.spectatorsMap.set(spectator.id, spectator);
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
}
}
else {
console.log("Invalid ClientAnnounce");
}
return;
}
catch (e) {
console.log("Invalid JSON (clientAnnounceListener)");
}
this.once("message", clientAnnounceListener);
}
function 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 = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR
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) ));
setTimeout(function abortMatch() {
if (gameSession.unreadyPlayersMap.size !== 0)
{
gameSessionsMap.delete(gameSession.id);
gameSession.playersMap.forEach((client) => {
client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchAbort) ));
client.gameSession = null;
clientTerminate(client);
});
}
}, 5000);
}
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) => {
if (!client.isAlive) {
clientTerminate(client);
deleteLog += ` ${shortId(client.id)} |`;
}
else {
client.isAlive = false;
client.socket.ping();
}
});
if (deleteLog) {
console.log(`Disconnected:${deleteLog}`);
}
console.log("gameSessionMap size: " + gameSessionsMap.size);
console.log("clientsMap size: " + clientsMap.size);
console.log("matchmakingPlayersMap size: " + matchmakingPlayersMap.size);
console.log("");
}, 4200);
function clientTerminate(client: Client)
{
client.socket.terminate();
if (client.gameSession)
{
client.gameSession.playersMap.delete(client.id);
if (client.gameSession.playersMap.size === 0)
{
clearInterval(client.gameSession.playersUpdateInterval);
clearInterval(client.gameSession.spectatorsUpdateInterval);
clearInterval(client.gameSession.gameLoopInterval);
gameSessionsMap.delete(client.gameSession.id);
}
}
clientsMap.delete(client.id);
if (matchmakingPlayersMap.has(client.id)) {
matchmakingPlayersMap.delete(client.id);
}
}
function closeListener()
{
clearInterval(pingInterval);
}
function errorListener(error: Error)
{
console.log("Error: " + JSON.stringify(error));
}

View File

@@ -0,0 +1,84 @@
import * as en from "../enums.js";
/* From Server */
export class ServerEvent {
constructor(type = 0) {
this.type = type;
}
}
export class EventAssignId extends ServerEvent {
constructor(id) {
super(en.EventTypes.assignId);
this.id = id;
}
}
export class EventMatchmakingComplete extends ServerEvent {
constructor(side) {
super(en.EventTypes.matchmakingComplete);
this.side = side;
}
}
export class EventGameUpdate extends ServerEvent {
constructor() {
super(en.EventTypes.gameUpdate);
this.playerLeft = {
y: 0
};
this.playerRight = {
y: 0
};
this.ballsArr = [];
this.wallTop = {
y: 0
};
this.wallBottom = {
y: 0
};
this.lastInputId = 0;
}
}
export class EventScoreUpdate extends ServerEvent {
constructor(scoreLeft, scoreRight) {
super(en.EventTypes.scoreUpdate);
this.scoreLeft = scoreLeft;
this.scoreRight = scoreRight;
}
}
export class EventMatchEnd extends ServerEvent {
constructor(winner, forfeit = false) {
super(en.EventTypes.matchEnd);
this.winner = winner;
this.forfeit = forfeit;
}
}
/* From Client */
export class ClientEvent {
constructor(type = 0) {
this.type = type;
}
}
export class ClientAnnounce extends ClientEvent {
constructor(role) {
super(en.EventTypes.clientAnnounce);
this.role = role;
}
}
export class ClientAnnouncePlayer extends ClientAnnounce {
constructor(matchOptions, clientId = "") {
super(en.ClientRole.player);
this.clientId = clientId;
this.matchOptions = matchOptions;
}
}
export class ClientAnnounceSpectator extends ClientAnnounce {
constructor(gameSessionId) {
super(en.ClientRole.spectator);
this.gameSessionId = gameSessionId;
}
}
export class EventInput extends ClientEvent {
constructor(input = en.InputEnum.noInput, id = 0) {
super(en.EventTypes.clientInput);
this.input = input;
this.id = id;
}
}

View File

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

View File

@@ -0,0 +1,51 @@
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";
export class GameComponents {
constructor(options) {
this.ballsArr = [];
const pos = new VectorInteger;
// Rackets
pos.assign(0 + c.pw, c.h_mid - c.ph / 2);
this.playerLeft = new Racket(pos, c.pw, c.ph, c.racketSpeed);
pos.assign(c.w - c.pw - c.pw, c.h_mid - c.ph / 2);
this.playerRight = new Racket(pos, c.pw, c.ph, c.racketSpeed);
// Balls
let ballsCount = 1;
if (options & en.MatchOptions.multiBalls) {
ballsCount = c.multiBallsCount;
}
pos.assign(-c.ballSize, -c.ballSize); // ball out =)
while (this.ballsArr.length < ballsCount) {
this.ballsArr.push(new Ball(pos, c.ballSize, c.ballSpeed, c.ballSpeedIncrease));
}
this.ballsArr.forEach((ball) => {
ball.dir.x = 1;
if (random() > 0.5) {
ball.dir.x *= -1;
}
ball.dir.y = random(0, 0.2);
if (random() > 0.5) {
ball.dir.y *= -1;
}
ball.dir = ball.dir.normalized();
});
// Walls
if (options & en.MatchOptions.movingWalls) {
pos.assign(0, 0);
this.wallTop = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed);
this.wallTop.dir.y = -1;
pos.assign(0, c.h - c.wallSize);
this.wallBottom = new MovingRectangle(pos, c.w, c.wallSize, c.movingWallSpeed);
this.wallBottom.dir.y = 1;
}
else {
pos.assign(0, 0);
this.wallTop = new Rectangle(pos, c.w, c.wallSize);
pos.assign(0, c.h - c.wallSize);
this.wallBottom = new Rectangle(pos, c.w, c.wallSize);
}
}
}

View File

@@ -0,0 +1,63 @@
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";
export 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);
}
}
}

View File

@@ -0,0 +1,124 @@
import { Vector, VectorInteger } from "./Vector.js";
import * as c from "../constants.js";
export class Rectangle {
constructor(pos, width, height) {
this.pos = new VectorInteger(pos.x, pos.y);
this.width = width;
this.height = height;
}
collision(collider) {
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;
}
}
}
export class MovingRectangle extends Rectangle {
constructor(pos, width, height, baseSpeed) {
super(pos, width, height);
this.dir = new Vector(0, 0);
this.baseSpeed = baseSpeed;
this.speed = baseSpeed;
}
move(delta) {
// 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, colliderArr) {
this._moveAndCollideAlgo(delta, colliderArr);
}
_moveAndCollideAlgo(delta, colliderArr) {
let oldPos = new VectorInteger(this.pos.x, this.pos.y);
this.move(delta);
if (colliderArr.some(this.collision, this)) {
this.pos = oldPos;
}
}
}
export class Racket extends MovingRectangle {
constructor(pos, width, height, baseSpeed) {
super(pos, width, height, baseSpeed);
}
moveAndCollide(delta, colliderArr) {
// let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug
this._moveAndCollideAlgo(delta, colliderArr);
// console.log(`y change: ${this.pos.y - oldPos.y}`);
}
}
export class Ball extends MovingRectangle {
constructor(pos, size, baseSpeed, speedIncrease) {
super(pos, size, size, baseSpeed);
this.ballInPlay = false;
this.speedIncrease = speedIncrease;
}
moveAndBounce(delta, colliderArr) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1) {
this.bounce(colliderArr[i]);
this.move(delta);
}
}
bounce(collider) {
this._bounceAlgo(collider);
}
_bounceAlgo(collider) {
/* 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();
}
}
_bounceWall() {
this.dir.y = this.dir.y * -1;
}
_bounceRacket(racket) {
this._bounceRacketAlgo(racket);
}
_bounceRacketAlgo(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}`);
}
}

View File

@@ -0,0 +1,142 @@
import { Vector, VectorInteger } from "./Vector.js";
import { Component, Moving } from "./interface.js";
import * as c from "../constants.js"
export 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;
}
}
}
export 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;
}
}
}
export 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}`);
}
}
export class Ball extends MovingRectangle {
readonly speedIncrease: number;
ballInPlay: boolean = false;
constructor(pos: VectorInteger, size: number, baseSpeed: number, speedIncrease: number) {
super(pos, size, size, baseSpeed);
this.speedIncrease = speedIncrease;
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
bounce(collider?: Rectangle) {
this._bounceAlgo(collider);
}
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();
}
}
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}`);
}
}

View File

@@ -0,0 +1,40 @@
export class Vector {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
assign(x, y) {
this.x = x;
this.y = y;
}
normalized() {
const normalizationFactor = Math.abs(this.x) + Math.abs(this.y);
return new Vector(this.x / normalizationFactor, this.y / normalizationFactor);
}
}
export class VectorInteger extends Vector {
}
/*
export 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;
// }
}
*/

View File

@@ -0,0 +1,47 @@
export 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);
}
}
export class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
export 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;
// }
}
*/

View File

@@ -0,0 +1 @@
export {};

View File

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

View File

@@ -0,0 +1,23 @@
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);
export const gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER
// for testing, force gameSession.id in wsServer.ts->matchmaking()

View File

@@ -0,0 +1,30 @@
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);
export const gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER
// for testing, force gameSession.id in wsServer.ts->matchmaking()

View File

@@ -0,0 +1,43 @@
export var EventTypes;
(function (EventTypes) {
// Class Implemented
EventTypes[EventTypes["gameUpdate"] = 1] = "gameUpdate";
EventTypes[EventTypes["scoreUpdate"] = 2] = "scoreUpdate";
EventTypes[EventTypes["matchEnd"] = 3] = "matchEnd";
EventTypes[EventTypes["assignId"] = 4] = "assignId";
EventTypes[EventTypes["matchmakingComplete"] = 5] = "matchmakingComplete";
// Generic
EventTypes[EventTypes["matchmakingInProgress"] = 6] = "matchmakingInProgress";
EventTypes[EventTypes["matchStart"] = 7] = "matchStart";
EventTypes[EventTypes["matchAbort"] = 8] = "matchAbort";
EventTypes[EventTypes["matchNewRound"] = 9] = "matchNewRound";
EventTypes[EventTypes["matchPause"] = 10] = "matchPause";
EventTypes[EventTypes["matchResume"] = 11] = "matchResume";
// Client
EventTypes[EventTypes["clientAnnounce"] = 12] = "clientAnnounce";
EventTypes[EventTypes["clientPlayerReady"] = 13] = "clientPlayerReady";
EventTypes[EventTypes["clientInput"] = 14] = "clientInput";
})(EventTypes || (EventTypes = {}));
export var InputEnum;
(function (InputEnum) {
InputEnum[InputEnum["noInput"] = 0] = "noInput";
InputEnum[InputEnum["up"] = 1] = "up";
InputEnum[InputEnum["down"] = 2] = "down";
})(InputEnum || (InputEnum = {}));
export var PlayerSide;
(function (PlayerSide) {
PlayerSide[PlayerSide["left"] = 1] = "left";
PlayerSide[PlayerSide["right"] = 2] = "right";
})(PlayerSide || (PlayerSide = {}));
export var ClientRole;
(function (ClientRole) {
ClientRole[ClientRole["player"] = 1] = "player";
ClientRole[ClientRole["spectator"] = 2] = "spectator";
})(ClientRole || (ClientRole = {}));
export var MatchOptions;
(function (MatchOptions) {
// binary flags, can be mixed
MatchOptions[MatchOptions["noOption"] = 0] = "noOption";
MatchOptions[MatchOptions["multiBalls"] = 1] = "multiBalls";
MatchOptions[MatchOptions["movingWalls"] = 2] = "movingWalls";
})(MatchOptions || (MatchOptions = {}));

View File

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

View File

@@ -0,0 +1,18 @@
export function random(min = 0, max = 1) {
return Math.random() * (max - min) + min;
}
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function clamp(n, min, max) {
if (n < min)
n = min;
else if (n > max)
n = max;
return (n);
}
// Typescript hack, unused
export function assertMovingRectangle(value) {
// if (value !== MovingRectangle) throw new Error("Not a MovingRectangle");
return;
}

View File

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

View File

@@ -0,0 +1,13 @@
import * as c from "./constants.js";
export function wallsMovements(delta, gc) {
const wallTop = gc.wallTop;
const wallBottom = gc.wallBottom;
if (wallTop.pos.y <= 0 || wallTop.pos.y >= c.movingWallPosMax) {
wallTop.dir.y *= -1;
}
if (wallBottom.pos.y >= c.h - c.wallSize || wallBottom.pos.y <= c.h - c.movingWallPosMax) {
wallBottom.dir.y *= -1;
}
wallTop.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
wallBottom.moveAndCollide(delta, [gc.playerLeft, gc.playerRight]);
}

View File

@@ -0,0 +1,18 @@
import * as c from "./constants.js";
import { MovingRectangle } from "../shared_js/class/Rectangle.js";
import { GameComponents } from "./class/GameComponents.js";
export 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]);
}