spectator mode WIP, working so far
This commit is contained in:
4
memo.txt
4
memo.txt
@@ -1,5 +1,5 @@
|
|||||||
TODO:
|
TODO:
|
||||||
- mode spectateur
|
- mode spectateur (WIP, semble fonctionner)
|
||||||
- certaines utilisations de Math.floor() superflu ? Vérifier les appels.
|
- certaines utilisations de Math.floor() superflu ? Vérifier les appels.
|
||||||
(éventuellement Math.round() ?)
|
(éventuellement Math.round() ?)
|
||||||
- un autre mode de jeu alternatif ?
|
- un autre mode de jeu alternatif ?
|
||||||
@@ -29,6 +29,8 @@ Done:
|
|||||||
|
|
||||||
-----------
|
-----------
|
||||||
BUG:
|
BUG:
|
||||||
|
- Uncaught (in promise) DOMException: The element has no supported sources.
|
||||||
|
Bug de son. Peut etre du au CORS. Il faut tester de referencer le son avec u chemin relatif.
|
||||||
- Si la balle va très vite, elle peut ignorer la collision avec une raquette ou mur.
|
- Si la balle va très vite, elle peut ignorer la collision avec une raquette ou mur.
|
||||||
la collision est testée seulement après le mouvement.
|
la collision est testée seulement après le mouvement.
|
||||||
Pour éviter ce bug il faudrait diviser le mouvement pour faire plusieurs tests de collision successifs.
|
Pour éviter ce bug il faudrait diviser le mouvement pour faire plusieurs tests de collision successifs.
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
|
||||||
import * as c from "./constants.js";
|
import * as c from "./constants.js";
|
||||||
import * as en from "../shared_js/enums.js"
|
import * as en from "../shared_js/enums.js"
|
||||||
import { gc, matchOptions, clientInfo } from "./global.js";
|
import { gc, matchOptions, clientInfo, clientInfoSpectator} from "./global.js";
|
||||||
import { wallsMovements } from "../shared_js/wallsMovement.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 actual_time: number = Date.now();
|
||||||
let last_time: number;
|
let last_time: number;
|
||||||
@@ -20,7 +22,7 @@ export function gameLoop()
|
|||||||
// interpolation
|
// interpolation
|
||||||
// console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`);
|
// 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 ) {
|
if (clientInfo.opponent.dir.y != 0 ) {
|
||||||
opponentInterpolation(delta_time);
|
racketInterpolation(delta_time, clientInfo.opponent, clientInfo.opponentNextPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// client prediction
|
// client prediction
|
||||||
@@ -33,15 +35,37 @@ export function gameLoop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function opponentInterpolation(delta: number)
|
export function gameLoopSpectator()
|
||||||
{
|
{
|
||||||
// interpolation
|
delta_time = c.fixedDeltaTime;
|
||||||
clientInfo.opponent.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
|
|
||||||
|
|
||||||
if ((clientInfo.opponent.dir.y > 0 && clientInfo.opponent.pos.y > clientInfo.opponentNextPos.y)
|
// interpolation
|
||||||
|| (clientInfo.opponent.dir.y < 0 && clientInfo.opponent.pos.y < clientInfo.opponentNextPos.y))
|
if (gc.playerLeft.dir.y != 0 ) {
|
||||||
{
|
racketInterpolation(delta_time, gc.playerLeft, clientInfoSpectator.playerLeftNextPos);
|
||||||
clientInfo.opponent.dir.y = 0;
|
}
|
||||||
clientInfo.opponent.pos.y = clientInfo.opponentNextPos.y;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
|
|
||||||
export {pong, gc, matchOptions} from "./pong.js"
|
import * as en from "../shared_js/enums.js";
|
||||||
export {socket, clientInfo} from "./ws.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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import { initAudio } from "./audio.js";
|
|||||||
|
|
||||||
|
|
||||||
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
|
/* TODO: A way to delay the init of variables, but still use "const" not "let" ? */
|
||||||
export let pong: GameArea;
|
import { pong, gc } from "./global.js"
|
||||||
export let gc: GameComponentsClient;
|
import { initPong, initGc, initMatchOptions, initStartFunction } from "./global.js"
|
||||||
export let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
|
|
||||||
|
|
||||||
function init()
|
function init()
|
||||||
{
|
{
|
||||||
@@ -34,22 +34,25 @@ function init()
|
|||||||
}
|
}
|
||||||
initAudio(soundMutedFlag);
|
initAudio(soundMutedFlag);
|
||||||
|
|
||||||
|
let matchOptions: en.MatchOptions = en.MatchOptions.noOption;
|
||||||
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
|
if ( (<HTMLInputElement>document.getElementById("multi_balls")).checked ) {
|
||||||
matchOptions |= en.MatchOptions.multiBalls;
|
matchOptions |= en.MatchOptions.multiBalls;
|
||||||
}
|
}
|
||||||
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
|
if ( (<HTMLInputElement>document.getElementById("moving_walls")).checked ) {
|
||||||
matchOptions |= en.MatchOptions.movingWalls;
|
matchOptions |= en.MatchOptions.movingWalls;
|
||||||
}
|
}
|
||||||
|
initMatchOptions(matchOptions);
|
||||||
|
|
||||||
document.getElementById("div_game_options").remove();
|
document.getElementById("div_game_options").remove();
|
||||||
document.getElementById("div_game_instructions").remove();
|
document.getElementById("div_game_instructions").remove();
|
||||||
|
|
||||||
pong = new GameArea();
|
initPong(new GameArea());
|
||||||
gc = new GameComponentsClient(matchOptions, pong.ctx);
|
initGc(new GameComponentsClient(matchOptions, pong.ctx));
|
||||||
|
initStartFunction(start);
|
||||||
initWebSocket(matchOptions);
|
initWebSocket(matchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start()
|
function start()
|
||||||
{
|
{
|
||||||
gc.text1.pos.assign(c.w*0.5, c.h*0.75);
|
gc.text1.pos.assign(c.w*0.5, c.h*0.75);
|
||||||
countdown(c.matchStartDelay/1000, (count: number) => {
|
countdown(c.matchStartDelay/1000, (count: number) => {
|
||||||
|
|||||||
19
src/client/pongSpectator.html
Normal file
19
src/client/pongSpectator.html
Normal 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>
|
||||||
46
src/client/pongSpectator.ts
Normal file
46
src/client/pongSpectator.ts
Normal 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);
|
||||||
|
}
|
||||||
124
src/client/ws.ts
124
src/client/ws.ts
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
import * as c from "./constants.js"
|
import * as c from "./constants.js"
|
||||||
import { gc, matchOptions } from "./global.js"
|
import { gc, matchOptions, startFunction } from "./global.js"
|
||||||
import * as ev from "../shared_js/class/Event.js"
|
import * as ev from "../shared_js/class/Event.js"
|
||||||
import * as en from "../shared_js/enums.js"
|
import * as en from "../shared_js/enums.js"
|
||||||
import * as msg from "./message.js";
|
import * as msg from "./message.js";
|
||||||
import { start } from "./pong.js";
|
|
||||||
import { RacketClient } from "./class/RectangleClient.js";
|
import { RacketClient } from "./class/RectangleClient.js";
|
||||||
import { repeatInput } from "./handleInput.js";
|
import { repeatInput } from "./handleInput.js";
|
||||||
import { soundRoblox } from "./audio.js"
|
import { soundRoblox } from "./audio.js"
|
||||||
@@ -19,16 +18,24 @@ class ClientInfo {
|
|||||||
opponentNextPos: VectorInteger;
|
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 wsPort = 8042;
|
||||||
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong";
|
const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong";
|
||||||
export let socket: WebSocket; /* TODO: A way to still use "const" not "let" ? */
|
export let socket: WebSocket; /* TODO: A way to still use "const" not "let" ? */
|
||||||
export const clientInfo = new ClientInfo();
|
export const clientInfo = new ClientInfo();
|
||||||
|
export const clientInfoSpectator = new ClientInfoSpectator(); // WIP, could refactor this
|
||||||
|
|
||||||
export function initWebSocket(options: en.MatchOptions)
|
export function initWebSocket(options: en.MatchOptions)
|
||||||
{
|
{
|
||||||
socket = new WebSocket(wsUrl, "json");
|
socket = new WebSocket(wsUrl, "json");
|
||||||
socket.addEventListener("open", (event) => {
|
socket.addEventListener("open", (event) => {
|
||||||
socket.send(JSON.stringify( new ev.ClientAnnounce(en.ClientRole.player, options, clientInfo.id) ));
|
socket.send(JSON.stringify( new ev.ClientAnnouncePlayer(options, clientInfo.id) ));
|
||||||
});
|
});
|
||||||
// socket.addEventListener("message", logListener); // for testing purpose
|
// socket.addEventListener("message", logListener); // for testing purpose
|
||||||
socket.addEventListener("message", preMatchListener);
|
socket.addEventListener("message", preMatchListener);
|
||||||
@@ -68,7 +75,7 @@ function preMatchListener(this: WebSocket, event: MessageEvent)
|
|||||||
case en.EventTypes.matchStart:
|
case en.EventTypes.matchStart:
|
||||||
socket.removeEventListener("message", preMatchListener);
|
socket.removeEventListener("message", preMatchListener);
|
||||||
socket.addEventListener("message", inGameListener);
|
socket.addEventListener("message", inGameListener);
|
||||||
start();
|
startFunction();
|
||||||
break;
|
break;
|
||||||
case en.EventTypes.matchAbort:
|
case en.EventTypes.matchAbort:
|
||||||
socket.removeEventListener("message", preMatchListener);
|
socket.removeEventListener("message", preMatchListener);
|
||||||
@@ -77,7 +84,7 @@ function preMatchListener(this: WebSocket, event: MessageEvent)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inGameListener(event: MessageEvent)
|
function inGameListener(this: WebSocket, event: MessageEvent)
|
||||||
{
|
{
|
||||||
const data: ev.ServerEvent = JSON.parse(event.data);
|
const data: ev.ServerEvent = JSON.parse(event.data);
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
@@ -186,3 +193,110 @@ function matchEnd(data: ev.EventMatchEnd)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// export let matchEnded = false; // 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);
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export class Client {
|
|||||||
id: string; // same as "socket.id"
|
id: string; // same as "socket.id"
|
||||||
isAlive: boolean = true;
|
isAlive: boolean = true;
|
||||||
gameSession: GameSession = null;
|
gameSession: GameSession = null;
|
||||||
matchOptions: en.MatchOptions = 0;
|
|
||||||
constructor(socket: WebSocket, id: string) {
|
constructor(socket: WebSocket, id: string) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -18,6 +17,7 @@ export class Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ClientPlayer extends Client {
|
export class ClientPlayer extends Client {
|
||||||
|
matchOptions: en.MatchOptions = 0;
|
||||||
inputBuffer: ev.EventInput = new ev.EventInput();
|
inputBuffer: ev.EventInput = new ev.EventInput();
|
||||||
lastInputId: number = 0;
|
lastInputId: number = 0;
|
||||||
racket: Racket;
|
racket: Racket;
|
||||||
@@ -27,7 +27,7 @@ export class ClientPlayer extends Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClientSpectator extends Client { // Wip, unused
|
export class ClientSpectator extends Client {
|
||||||
constructor(socket: WebSocket, id: string) {
|
constructor(socket: WebSocket, id: string) {
|
||||||
super(socket, id);
|
super(socket, id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import * as en from "../../shared_js/enums.js"
|
import * as en from "../../shared_js/enums.js"
|
||||||
import * as ev from "../../shared_js/class/Event.js"
|
import * as ev from "../../shared_js/class/Event.js"
|
||||||
import * as c from "../constants.js"
|
import * as c from "../constants.js"
|
||||||
import { ClientPlayer } from "./Client";
|
import { ClientPlayer, ClientSpectator } from "./Client";
|
||||||
import { GameComponentsServer } from "./GameComponentsServer.js";
|
import { GameComponentsServer } from "./GameComponentsServer.js";
|
||||||
import { clientInputListener } from "../wsServer.js";
|
import { clientInputListener } from "../wsServer.js";
|
||||||
import { random } from "../utils.js";
|
import { random } from "../utils.js";
|
||||||
@@ -18,8 +18,10 @@ export class GameSession {
|
|||||||
id: string; // url ?
|
id: string; // url ?
|
||||||
playersMap: Map<string, ClientPlayer> = new Map();
|
playersMap: Map<string, ClientPlayer> = new Map();
|
||||||
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
|
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
|
||||||
|
spectatorsMap: Map<string, ClientSpectator> = new Map();
|
||||||
gameLoopInterval: NodeJS.Timer | number = 0;
|
gameLoopInterval: NodeJS.Timer | number = 0;
|
||||||
clientsUpdateInterval: NodeJS.Timer | number = 0;
|
playersUpdateInterval: NodeJS.Timer | number = 0;
|
||||||
|
spectatorsUpdateInterval: NodeJS.Timer | number = 0;
|
||||||
components: GameComponentsServer;
|
components: GameComponentsServer;
|
||||||
matchOptions: en.MatchOptions;
|
matchOptions: en.MatchOptions;
|
||||||
matchEnded: boolean = false;
|
matchEnded: boolean = false;
|
||||||
@@ -50,7 +52,8 @@ export class GameSession {
|
|||||||
|
|
||||||
s.actual_time = Date.now();
|
s.actual_time = Date.now();
|
||||||
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
|
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
|
||||||
s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s);
|
s.playersUpdateInterval = setInterval(s._playersUpdate, c.playersUpdateIntervalMS, s);
|
||||||
|
s.spectatorsUpdateInterval = setInterval(s._spectatorsUpdate, c.spectatorsUpdateIntervalMS, s);
|
||||||
}
|
}
|
||||||
pause(s: GameSession) {
|
pause(s: GameSession) {
|
||||||
s.playersMap.forEach( (client) => {
|
s.playersMap.forEach( (client) => {
|
||||||
@@ -58,7 +61,8 @@ export class GameSession {
|
|||||||
});
|
});
|
||||||
|
|
||||||
clearInterval(s.gameLoopInterval);
|
clearInterval(s.gameLoopInterval);
|
||||||
clearInterval(s.clientsUpdateInterval);
|
clearInterval(s.playersUpdateInterval);
|
||||||
|
clearInterval(s.spectatorsUpdateInterval);
|
||||||
}
|
}
|
||||||
instantInputDebug(client: ClientPlayer) {
|
instantInputDebug(client: ClientPlayer) {
|
||||||
this._handleInput(c.fixedDeltaTime, client);
|
this._handleInput(c.fixedDeltaTime, client);
|
||||||
@@ -114,24 +118,46 @@ export class GameSession {
|
|||||||
if (this.matchEnded) {
|
if (this.matchEnded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._scoreUpdate(ball);
|
||||||
if (ball.pos.x > c.w) { ++gc.scoreLeft; }
|
|
||||||
else if (ball.pos.x < 0 - ball.width) { ++gc.scoreRight; }
|
|
||||||
|
|
||||||
this.playersMap.forEach( (client) => {
|
|
||||||
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
|
|
||||||
});
|
|
||||||
setTimeout(this._newRound, c.newRoundDelay, this, ball);
|
setTimeout(this._newRound, c.newRoundDelay, this, ball);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private _clientsUpdate(s: GameSession) {
|
private _scoreUpdate(ball: Ball) {
|
||||||
const gc = s.components;
|
const gc = this.components;
|
||||||
const update = new ev.EventGameUpdate();
|
if (ball.pos.x > c.w) {
|
||||||
update.playerLeft.y = gc.playerLeft.pos.y;
|
++gc.scoreLeft;
|
||||||
update.playerRight.y = gc.playerRight.pos.y;
|
}
|
||||||
|
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) => {
|
gc.ballsArr.forEach((ball) => {
|
||||||
update.ballsArr.push({
|
snapshot.ballsArr.push({
|
||||||
x: ball.pos.x,
|
x: ball.pos.x,
|
||||||
y: ball.pos.y,
|
y: ball.pos.y,
|
||||||
dirX: ball.dir.x,
|
dirX: ball.dir.x,
|
||||||
@@ -139,15 +165,11 @@ export class GameSession {
|
|||||||
speed: ball.speed
|
speed: ball.speed
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (s.matchOptions & en.MatchOptions.movingWalls) {
|
if (this.matchOptions & en.MatchOptions.movingWalls) {
|
||||||
update.wallTop.y = gc.wallTop.pos.y;
|
snapshot.wallTop.y = gc.wallTop.pos.y;
|
||||||
update.wallBottom.y = gc.wallBottom.pos.y;
|
snapshot.wallBottom.y = gc.wallBottom.pos.y;
|
||||||
}
|
}
|
||||||
|
return (snapshot);
|
||||||
s.playersMap.forEach( (client) => {
|
|
||||||
update.lastInputId = client.lastInputId;
|
|
||||||
client.socket.send(JSON.stringify(update));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
private _newRound(s: GameSession, ball: Ball) {
|
private _newRound(s: GameSession, ball: Ball) {
|
||||||
if (s._checkDisconnexions()) {
|
if (s._checkDisconnexions()) {
|
||||||
@@ -187,6 +209,9 @@ export class GameSession {
|
|||||||
console.log("Player Right WIN (by forfeit)");
|
console.log("Player Right WIN (by forfeit)");
|
||||||
}
|
}
|
||||||
luckyWinner.socket.send(JSON.stringify(eventEnd));
|
luckyWinner.socket.send(JSON.stringify(eventEnd));
|
||||||
|
this.spectatorsMap.forEach( (client) => {
|
||||||
|
client.socket.send(JSON.stringify(eventEnd));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -209,5 +234,8 @@ export class GameSession {
|
|||||||
s.playersMap.forEach( (client) => {
|
s.playersMap.forEach( (client) => {
|
||||||
client.socket.send(JSON.stringify(eventEnd));
|
client.socket.send(JSON.stringify(eventEnd));
|
||||||
});
|
});
|
||||||
|
s.spectatorsMap.forEach( (client) => {
|
||||||
|
client.socket.send(JSON.stringify(eventEnd));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export const serverGameLoopIntervalMS = 15; // millisecond
|
|||||||
export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
|
export const fixedDeltaTime = serverGameLoopIntervalMS/1000; // second
|
||||||
|
|
||||||
// 33.333ms == 1000/30
|
// 33.333ms == 1000/30
|
||||||
export const clientsUpdateIntervalMS = 1000/30; // millisecond
|
export const playersUpdateIntervalMS = 1000/30; // millisecond
|
||||||
|
export const spectatorsUpdateIntervalMS = 1000/30; // millisecond
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
|
|
||||||
import * as en from "../shared_js/enums.js"
|
import * as en from "../shared_js/enums.js"
|
||||||
import * as ev from "../shared_js/class/Event.js"
|
import * as ev from "../shared_js/class/Event.js"
|
||||||
import { Client, ClientPlayer } from "./class/Client.js"
|
import { Client, ClientPlayer, ClientSpectator } from "./class/Client.js"
|
||||||
import { GameSession } from "./class/GameSession.js"
|
import { GameSession } from "./class/GameSession.js"
|
||||||
import { shortId } from "./utils.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 ?
|
// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ?
|
||||||
const wsPort = 8042;
|
const wsPort = 8042;
|
||||||
@@ -61,16 +62,29 @@ function clientAnnounceListener(this: WebSocket, data: string)
|
|||||||
if (msg.type === en.EventTypes.clientAnnounce)
|
if (msg.type === en.EventTypes.clientAnnounce)
|
||||||
{
|
{
|
||||||
// TODO: reconnection with msg.clientId ?
|
// TODO: reconnection with msg.clientId ?
|
||||||
// TODO: spectator/player distinction with msg.role ?
|
// "/pong" to play, "/pong?ID_OF_A_GAMESESSION" to spectate (or something like that)
|
||||||
// msg.role is probably not a good idea.
|
if (msg.role === en.ClientRole.player)
|
||||||
// something like a different route could be better
|
{
|
||||||
// "/pong" to play, "/ID_OF_A_GAMESESSION" to spectate
|
const announce: ev.ClientAnnouncePlayer = <ev.ClientAnnouncePlayer>msg;
|
||||||
|
const player = clientsMap.get(this.id) as ClientPlayer;
|
||||||
const player = clientsMap.get(this.id) as ClientPlayer;
|
player.matchOptions = announce.matchOptions;
|
||||||
player.matchOptions = msg.matchOptions;
|
this.send(JSON.stringify( new ev.EventAssignId(this.id) ));
|
||||||
this.send(JSON.stringify( new ev.EventAssignId(this.id) ));
|
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
|
||||||
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
|
matchmaking(player);
|
||||||
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 {
|
else {
|
||||||
console.log("Invalid ClientAnnounce");
|
console.log("Invalid ClientAnnounce");
|
||||||
@@ -107,6 +121,7 @@ function matchmaking(player: ClientPlayer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const id = gameSessionIdPLACEHOLDER; // Force ID, TESTING SPECTATOR
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const gameSession = new GameSession(id, matchOptions);
|
const gameSession = new GameSession(id, matchOptions);
|
||||||
gameSessionsMap.set(id, gameSession);
|
gameSessionsMap.set(id, gameSession);
|
||||||
@@ -226,7 +241,8 @@ function clientTerminate(client: Client)
|
|||||||
client.gameSession.playersMap.delete(client.id);
|
client.gameSession.playersMap.delete(client.id);
|
||||||
if (client.gameSession.playersMap.size === 0)
|
if (client.gameSession.playersMap.size === 0)
|
||||||
{
|
{
|
||||||
clearInterval(client.gameSession.clientsUpdateInterval);
|
clearInterval(client.gameSession.playersUpdateInterval);
|
||||||
|
clearInterval(client.gameSession.spectatorsUpdateInterval);
|
||||||
clearInterval(client.gameSession.gameLoopInterval);
|
clearInterval(client.gameSession.gameLoopInterval);
|
||||||
gameSessionsMap.delete(client.gameSession.id);
|
gameSessionsMap.delete(client.gameSession.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,16 +82,30 @@ export class ClientEvent {
|
|||||||
|
|
||||||
export class ClientAnnounce extends ClientEvent {
|
export class ClientAnnounce extends ClientEvent {
|
||||||
role: en.ClientRole;
|
role: en.ClientRole;
|
||||||
clientId: string;
|
constructor(role: en.ClientRole) {
|
||||||
matchOptions: en.MatchOptions;
|
|
||||||
constructor(role: en.ClientRole, matchOptions: en.MatchOptions, clientId: string = "") {
|
|
||||||
super(en.EventTypes.clientAnnounce);
|
super(en.EventTypes.clientAnnounce);
|
||||||
this.role = role;
|
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.clientId = clientId;
|
||||||
this.matchOptions = matchOptions;
|
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 {
|
export class EventInput extends ClientEvent {
|
||||||
input: en.InputEnum;
|
input: en.InputEnum;
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ export const newRoundDelay = 1500; // millisecond
|
|||||||
export const multiBallsCount = 3;
|
export const multiBallsCount = 3;
|
||||||
export const movingWallPosMax = Math.floor(w*0.12);
|
export const movingWallPosMax = Math.floor(w*0.12);
|
||||||
export const movingWallSpeed = Math.floor(w*0.08);
|
export const movingWallSpeed = Math.floor(w*0.08);
|
||||||
|
|
||||||
|
|
||||||
|
export const gameSessionIdPLACEHOLDER = "42"; // TESTING SPECTATOR PLACEHOLDER
|
||||||
|
// for testing, force gameSession.id in wsServer.ts->matchmaking()
|
||||||
Reference in New Issue
Block a user