import * as c from "./constants.js" import { gc, matchOptions } from "./global.js" import * as ev from "../shared_js/class/Event.js" import * as en from "../shared_js/enums.js" import { matchmaking, matchmakingComplete, matchAbort, matchStart } from "./pong.js"; import { RacketClient } from "./class/RectangleClient.js"; import { repeatInput } from "./handleInput.js"; import { soundRoblox } from "./audio.js" import { sleep } from "./utils.js"; import { Vector, VectorInteger } from "../shared_js/class/Vector.js"; class ClientInfo { id = ""; side: en.PlayerSide; racket: RacketClient; opponent: RacketClient; opponentNextPos: VectorInteger; } const wsPort = 8042; const wsUrl = "ws://" + document.location.hostname + ":" + wsPort + "/pong"; export let socket: WebSocket; /* TODO: A way to still use "const" not "let" ? */ export const clientInfo = new ClientInfo(); export function initWebSocket(options: en.MatchOptions) { socket = new WebSocket(wsUrl, "json"); socket.addEventListener("open", (event) => { socket.send(JSON.stringify( new ev.ClientAnnounce(en.ClientRole.player, options, clientInfo.id) )); }); // socket.addEventListener("message", logListener); // for testing purpose socket.addEventListener("message", preMatchListener); } function logListener(this: WebSocket, event: MessageEvent) { console.log("%i: " + event.data, Date.now()); } function preMatchListener(this: WebSocket, event: MessageEvent) { const data: ev.ServerEvent = JSON.parse(event.data); switch (data.type) { case en.EventTypes.assignId: clientInfo.id = (data).id; break; case en.EventTypes.matchmakingInProgress: matchmaking(); break; case en.EventTypes.matchmakingComplete: clientInfo.side = (data).side; if (clientInfo.side === en.PlayerSide.left) { clientInfo.racket = gc.playerLeft; clientInfo.opponent = gc.playerRight; } else if (clientInfo.side === en.PlayerSide.right) { clientInfo.racket = gc.playerRight; clientInfo.opponent = gc.playerLeft; } clientInfo.opponentNextPos = new VectorInteger(clientInfo.opponent.pos.x, clientInfo.opponent.pos.y); clientInfo.racket.color = "darkgreen"; // for testing purpose socket.send(JSON.stringify( new ev.ClientEvent(en.EventTypes.clientPlayerReady) )); // TODO: set an interval/timeout to resend until matchStart response (in case of network problem) matchmakingComplete(); break; case en.EventTypes.matchStart: socket.removeEventListener("message", preMatchListener); socket.addEventListener("message", inGameListener); matchStart(); break; case en.EventTypes.matchAbort: socket.removeEventListener("message", preMatchListener); matchAbort(); break; } } function inGameListener(event: MessageEvent) { const data: ev.ServerEvent = JSON.parse(event.data); switch (data.type) { case en.EventTypes.gameUpdate: // setTimeout(gameUpdate, 500, data as ev.EventGameUpdate); // artificial latency for testing purpose gameUpdate(data as ev.EventGameUpdate); break; case en.EventTypes.scoreUpdate: scoreUpdate(data as ev.EventScoreUpdate); break; case en.EventTypes.matchEnd: matchEnd(data as ev.EventMatchEnd); break; } } function gameUpdate(data: ev.EventGameUpdate) { console.log("gameUpdate"); if (matchOptions & en.MatchOptions.movingWalls) { gc.wallTop.pos.y = data.wallTop.y; gc.wallBottom.pos.y = data.wallBottom.y; } data.ballsArr.forEach((ball, i) => { gc.ballsArr[i].pos.assign(ball.x, ball.y); gc.ballsArr[i].dir.assign(ball.dirX, ball.dirY); gc.ballsArr[i].speed = ball.speed; }); /* // Equivalent to gc.ballsArr.forEach((ball, i) => { ball.pos.assign(data.ballsArr[i].x, data.ballsArr[i].y); ball.dir.assign(data.ballsArr[i].dirX, data.ballsArr[i].dirY); ball.speed = data.ballsArr[i].speed; }); */ const predictionPos = new VectorInteger(clientInfo.racket.pos.x, clientInfo.racket.pos.y); // debug if (clientInfo.side === en.PlayerSide.left) { clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerLeft.y); } else if (clientInfo.side === en.PlayerSide.right) { clientInfo.racket.pos.assign(clientInfo.racket.pos.x, data.playerRight.y); } // interpolation clientInfo.opponent.pos.assign(clientInfo.opponentNextPos.x, clientInfo.opponentNextPos.y); if (clientInfo.side === en.PlayerSide.left) { clientInfo.opponentNextPos.assign(clientInfo.opponent.pos.x, data.playerRight.y); } else if (clientInfo.side === en.PlayerSide.right) { clientInfo.opponentNextPos.assign(clientInfo.opponent.pos.x, data.playerLeft.y); } clientInfo.opponent.dir = new Vector( clientInfo.opponentNextPos.x - clientInfo.opponent.pos.x, clientInfo.opponentNextPos.y - clientInfo.opponent.pos.y ); if (Math.abs(clientInfo.opponent.dir.x) + Math.abs(clientInfo.opponent.dir.y) !== 0) { clientInfo.opponent.dir = clientInfo.opponent.dir.normalized(); } // server reconciliation repeatInput(data.lastInputId); // debug if (clientInfo.racket.pos.y > predictionPos.y + 1 || clientInfo.racket.pos.y < predictionPos.y - 1) { console.log( `Reconciliation error: server y: ${data.playerLeft.y} reconciliation y: ${clientInfo.racket.pos.y} prediction y: ${predictionPos.y}` ); } } function scoreUpdate(data: ev.EventScoreUpdate) { // console.log("scoreUpdate"); if (clientInfo.side === en.PlayerSide.left && data.scoreRight > gc.scoreRight.value) { soundRoblox.play(); } else if (clientInfo.side === en.PlayerSide.right && data.scoreLeft > gc.scoreLeft.value) { soundRoblox.play(); } gc.scoreLeft.value = data.scoreLeft; gc.scoreRight.value = data.scoreRight; } function matchEnd(data: ev.EventMatchEnd) { if (data.winner === clientInfo.side) { gc.text1.pos.assign(c.w*0.415, c.h*0.5); gc.text1.text = "WIN"; if (data.forfeit) { if (clientInfo.side === 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"; }, 2500); setTimeout(() => { gc.text3.text = "calme ta joie"; }, 5000); } } else { gc.text1.pos.assign(c.w*0.383, c.h*0.5); gc.text1.text = "LOSE"; } // matchEnded = true; // unused } // export let matchEnded = false; // unused