Implémentation du jeu dans svelte HOPE

This commit is contained in:
batche
2022-12-12 12:37:47 +01:00
parent fb8368cc63
commit b04cd0aa48
121 changed files with 570 additions and 1793 deletions

View File

@@ -1,13 +0,0 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"strictNullChecks": true,
"strictFunctionTypes": true
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@@ -1,18 +0,0 @@
#!/bin/bash
npx tsc
mkdir -p www
cp ./src/client/*.html ./www/
cp ./src/client/*.css ./www/
mkdir -p www/js
cp ./src/client/*.js ./www/js/
mkdir -p www/js/class
cp ./src/client/class/*.js ./www/js/class/
mkdir -p www/shared_js/
cp ./src/shared_js/*.js ./www/shared_js/
mkdir -p www/shared_js/class
cp ./src/shared_js/class/*.js ./www/shared_js/class/

View File

@@ -1,45 +0,0 @@
Done:
- Connexion client/serveur via un Websocket
- implémentation basique (authoritative server)
- Matchmaking
- client prediction
- server reconciliation (buffer des inputs côté client + id sur les inputs)
- amélioration collision avec Hugo
- du son (rebonds de la balle, "Oof" de Roblox sur un point)
- init de GameComponents partagé entre serveur et client.
- draw on the canvas "WIN", "LOSE", "MATCHMAKING COMPLETE", ...
- interpolation (mis à jour progressif des mouvements de l'adversaire)
- traitement groupé des inputs clients toutes les x millisecondes
(BUG désynchronisation: revenu à un traitement immédiat en attendant)
- Détruire les GameSession une fois finies.
- mode multi-balles
- mode murs mouvant (la zone de jeu rétréci / agrandi en continu)
- Selection des modes de jeu via HTML
- Selection audio on/off via HTML
TODO:
- Match Abort si tout les joueurs ne sont pas pret assez vite (~15 secondes)
- mode spectateur
- certaines utilisations de Math.floor() superflu ? Vérifier les appels.
(éventuellement Math.round() ?)
- un autre mode de jeu alternatif ?
- changer les "localhost:8080" dans le code.
- sélection couleur des raquettes (your color/opponent color) dans le profil utilisateur.
Enregistrement dans la DB.
init des couleurs dans GameComponentsClient() basé sur les variables de l'utilsateur connecté.
-----------
idées modes de jeu :
- mode 2 raquettes (un joueur haut/gauche et bas/droite)
- skin patate ???
- (prediction de l'avancement de la balle basé sur la latence serveur ?)
- d'autres sons (foule qui applaudi/musique de victoire)
-----------
- BUG: Si la balle va très vite, elle peut ignorer la collision avec une raquette ou mur.
la collision est testée seulement après le mouvement.
Pour éviter ce bug il faudrait diviser le mouvement pour faire plusieurs tests de collision successifs.
- BUG mineur: sur un changement de fenêtre, les touches restent enfoncées et il faut les "décoincer"
en réappuyant. Ce n'est pas grave mais peut-on faire mieux ?
----------
OSEF, rebuts:
- reconnection
- amélioration du protocole, remplacement du JSON (compression. moins de bande passante).

121
JEU2/package-lock.json generated
View File

@@ -1,121 +0,0 @@
{
"name": "ft_transcendence",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
},
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.8.4"
}
},
"node_modules/@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/typescript": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@types/node": {
"version": "18.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz",
"integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"typescript": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"ws": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.10.0.tgz",
"integrity": "sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==",
"requires": {}
}
}
}

View File

@@ -1,13 +0,0 @@
{
"type": "module",
"devDependencies": {
"@types/node": "^18.11.5",
"@types/uuid": "^8.3.4",
"@types/ws": "^8.5.3",
"typescript": "^4.8.4"
},
"dependencies": {
"uuid": "^9.0.0",
"ws": "^8.10.0"
}
}

View File

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

View File

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

View File

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

View File

@@ -1,188 +0,0 @@
import * as en from "../enums.js"
import * as ev from "./Event.js"
import * as c from "../constants.js"
import { ClientPlayer } from "./Client";
import { GameComponentsServer } from "./GameComponentsServer.js";
import { clientInputListener } from "../wsServer.js";
import { random } from "../utils.js";
import { Ball } from "../../shared_js/class/Rectangle.js";
import { wallsMovements } from "../../shared_js/wallsMovement.js";
/*
Arg "s: GameSession" replace "this: GameSession" for use with setTimeout(),
because "this" is equal to "this: Timeout"
*/
class GameSession {
id: string; // url ?
playersMap: Map<string, ClientPlayer> = new Map();
unreadyPlayersMap: Map<string, ClientPlayer> = new Map();
gameLoopInterval: NodeJS.Timer | number = 0;
clientsUpdateInterval: NodeJS.Timer | number = 0;
components: GameComponentsServer;
matchOptions: en.MatchOptions;
matchEnded: boolean = false;
actual_time: number;
last_time: number;
delta_time: number;
constructor(id: string, matchOptions: en.MatchOptions) {
this.id = id;
this.matchOptions = matchOptions;
this.components = new GameComponentsServer(this.matchOptions);
}
start() {
const gc = this.components;
setTimeout(this.resume, c.matchStartDelay, this);
let timeout = c.matchStartDelay + c.newRoundDelay;
gc.ballsArr.forEach((ball) => {
setTimeout(this._newRound, timeout, this, ball);
timeout += c.newRoundDelay*0.5;
});
}
resume(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.on("message", clientInputListener);
});
s.actual_time = Date.now();
s.gameLoopInterval = setInterval(s._gameLoop, c.serverGameLoopIntervalMS, s);
s.clientsUpdateInterval = setInterval(s._clientsUpdate, c.clientsUpdateIntervalMS, s);
}
pause(s: GameSession) {
s.playersMap.forEach( (client) => {
client.socket.off("message", clientInputListener);
});
clearInterval(s.gameLoopInterval);
clearInterval(s.clientsUpdateInterval);
}
instantInputDebug(client: ClientPlayer) {
this._handleInput(c.fixedDeltaTime, client);
}
private _handleInput(delta: number, client: ClientPlayer) {
// if (client.inputBuffer === null) {return;}
const gc = this.components;
const input = client.inputBuffer.input;
if (input === en.InputEnum.up) {
client.racket.dir.y = -1;
}
else if (input === en.InputEnum.down) {
client.racket.dir.y = 1;
}
if (input !== en.InputEnum.noInput) {
client.racket.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
}
client.lastInputId = client.inputBuffer.id;
// client.inputBuffer = null;
}
private _gameLoop(s: GameSession) {
/* s.last_time = s.actual_time;
s.actual_time = Date.now();
s.delta_time = (s.actual_time - s.last_time) / 1000; */
s.delta_time = c.fixedDeltaTime;
// WIP, replaced by instantInputDebug() to prevent desynchro
/* s.playersMap.forEach( (client) => {
s._handleInput(s.delta_time, client);
}); */
const gc = s.components;
gc.ballsArr.forEach((ball) => {
s._ballMovement(s.delta_time, ball);
});
if (s.matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(s.delta_time, gc);
}
}
private _ballMovement(delta: number, ball: Ball) {
const gc = this.components;
if (ball.ballInPlay)
{
ball.moveAndBounce(delta, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
if (ball.pos.x > c.w
|| ball.pos.x < 0 - ball.width)
{
ball.ballInPlay = false;
if (this.matchEnded) {
return;
}
if (ball.pos.x > c.w) { ++gc.scoreLeft; }
else if (ball.pos.x < 0 - ball.width) { ++gc.scoreRight; }
this.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(new ev.EventScoreUpdate(gc.scoreLeft, gc.scoreRight)));
});
setTimeout(this._newRound, c.newRoundDelay, this, ball);
}
}
}
private _clientsUpdate(s: GameSession) {
const gc = s.components;
const update = new ev.EventGameUpdate();
update.playerLeft.y = gc.playerLeft.pos.y;
update.playerRight.y = gc.playerRight.pos.y;
gc.ballsArr.forEach((ball) => {
update.ballsArr.push({
x: ball.pos.x,
y: ball.pos.y,
dirX: ball.dir.x,
dirY: ball.dir.y,
speed: ball.speed
});
});
if (s.matchOptions & en.MatchOptions.movingWalls) {
update.wallTop.y = gc.wallTop.pos.y;
update.wallBottom.y = gc.wallBottom.pos.y;
}
s.playersMap.forEach( (client) => {
update.lastInputId = client.lastInputId;
client.socket.send(JSON.stringify(update));
});
}
private _newRound(s: GameSession, ball: Ball) {
const gc = s.components;
// https://fr.wikipedia.org/wiki/Tennis_de_table#Nombre_de_manches
if (gc.scoreLeft >= 11 || gc.scoreRight >= 11)
// if (gc.scoreLeft >= 2 || gc.scoreRight >= 2) // WIP: for testing
{
if (Math.abs(gc.scoreLeft - gc.scoreRight) >= 2)
{
s._matchEnd(s);
return;
}
}
ball.pos.x = c.w_mid;
ball.pos.y = random(c.h*0.3, c.h*0.7);
ball.speed = ball.baseSpeed;
ball.ballInPlay = true;
}
private _matchEnd(s: GameSession) {
s.matchEnded = true;
const gc = s.components;
let eventEnd: ev.EventMatchEnd;
if (gc.scoreLeft > gc.scoreRight) {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.left);
console.log("Player Left WIN");
}
else {
eventEnd = new ev.EventMatchEnd(en.PlayerSide.right);
console.log("Player Right WIN");
}
s.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify(eventEnd));
});
}
}
export {GameSession}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,238 +0,0 @@
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 "./enums.js"
import * as ev from "./class/Event.js"
import { Client, ClientPlayer } from "./class/Client.js"
import { GameSession } from "./class/GameSession.js"
import { shortId } from "./utils.js";
// pas indispensable d'avoir un autre port si le WebSocket est relié à un serveur http préexistant ?
const wsPort = 8042;
export const wsServer = new WebSocketServer<WebSocket>({port: wsPort, path: "/pong"});
const clientsMap: Map<string, Client> = new Map; // socket.id/Client
const matchmakingPlayersMap: Map<string, ClientPlayer> = new Map; // socket.id/ClientPlayer (duplicates with clientsMap)
const gameSessionsMap: Map<string, GameSession> = new Map; // GameSession.id(url)/GameSession
wsServer.on("connection", connectionListener);
wsServer.on("error", errorListener);
wsServer.on("close", closeListener);
function connectionListener(socket: WebSocket, request: IncomingMessage)
{
const id = uuidv4();
const client = new Client(socket, id);
clientsMap.set(id, client);
socket.id = id;
socket.on("pong", function heartbeat() {
client.isAlive = true;
// console.log(`client ${shortId(client.id)} is alive`);
});
socket.on("message", function log(data: string) {
try {
const event: ev.ClientEvent = JSON.parse(data);
if (event.type === en.EventTypes.clientInput) {
return;
}
}
catch (e) {}
console.log("data: " + data);
});
socket.once("message", clientAnnounceListener);
}
function clientAnnounceListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientAnnounce = JSON.parse(data);
if (msg.type === en.EventTypes.clientAnnounce)
{
// TODO: reconnection with msg.clientId ?
// TODO: spectator/player distinction with msg.role ?
// msg.role is probably not a good idea.
// something like a different route could be better
// "/pong" to play, "/ID_OF_A_GAMESESSION" to spectate
const player = clientsMap.get(this.id) as ClientPlayer;
player.matchOptions = msg.matchOptions;
this.send(JSON.stringify( new ev.EventAssignId(this.id) ));
this.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchmakingInProgress) ));
matchmaking(player);
}
else {
console.log("Invalid ClientAnnounce");
}
return;
}
catch (e) {
console.log("Invalid JSON (clientAnnounceListener)");
}
this.once("message", clientAnnounceListener);
}
function matchmaking(player: ClientPlayer)
{
const minPlayersNumber = 2;
const maxPlayersNumber = 2;
const matchOptions = player.matchOptions;
matchmakingPlayersMap.set(player.id, player);
const compatiblePlayers: ClientPlayer[] = [];
for (const [id, client] of matchmakingPlayersMap)
{
if (client.matchOptions === matchOptions)
{
compatiblePlayers.push(client);
if (compatiblePlayers.length === maxPlayersNumber) {
break;
}
}
}
if (compatiblePlayers.length < minPlayersNumber) {
return;
}
const id = uuidv4();
const gameSession = new GameSession(id, matchOptions);
gameSessionsMap.set(id, gameSession);
compatiblePlayers.forEach((client) => {
matchmakingPlayersMap.delete(client.id);
client.gameSession = gameSession;
gameSession.playersMap.set(client.id, client);
gameSession.unreadyPlayersMap.set(client.id, client);
});
// WIP: Not pretty, hardcoded two players.
// Could be done in gameSession maybe ?
compatiblePlayers[0].racket = gameSession.components.playerRight;
compatiblePlayers[1].racket = gameSession.components.playerLeft;
compatiblePlayers.forEach((client) => {
client.socket.once("message", playerReadyConfirmationListener);
});
compatiblePlayers[0].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.right) ));
compatiblePlayers[1].socket.send(JSON.stringify( new ev.EventMatchmakingComplete(en.PlayerSide.left) ));
}
function playerReadyConfirmationListener(this: WebSocket, data: string)
{
try {
const msg : ev.ClientEvent = JSON.parse(data);
if (msg.type === en.EventTypes.clientPlayerReady)
{
const client = clientsMap.get(this.id);
const gameSession = client.gameSession;
gameSession.unreadyPlayersMap.delete(this.id);
if (gameSession.unreadyPlayersMap.size === 0) {
gameSession.playersMap.forEach( (client) => {
client.socket.send(JSON.stringify( new ev.ServerEvent(en.EventTypes.matchStart) ));
});
gameSession.start();
}
}
else {
console.log("Invalid playerReadyConfirmation");
}
return;
}
catch (e) {
console.log("Invalid JSON (playerReadyConfirmationListener)");
}
this.once("message", playerReadyConfirmationListener);
}
export function clientInputListener(this: WebSocket, data: string)
{
try {
// const input: ev.ClientEvent = JSON.parse(data);
const input: ev.EventInput = JSON.parse(data);
if (input.type === en.EventTypes.clientInput)
{
const client = clientsMap.get(this.id) as ClientPlayer;
client.inputBuffer = input;
client.gameSession.instantInputDebug(client); // wip
}
else {
console.log("Invalid clientInput");
}
}
catch (e) {
console.log("Invalid JSON (clientInputListener)");
}
}
////////////
////////////
const pingInterval = setInterval( () => {
let deleteLog = "";
clientsMap.forEach( (client, key, map) => {
if (!client.isAlive) {
clientTerminate(client, key, map);
deleteLog += ` ${shortId(key)} |`;
}
else {
client.isAlive = false;
client.socket.ping();
}
});
if (deleteLog) {
console.log(`Disconnected:${deleteLog}`);
}
console.log("gameSessionMap size: " + gameSessionsMap.size);
console.log("clientsMap size: " + clientsMap.size);
console.log("matchmakingPlayersMap size: " + matchmakingPlayersMap.size);
console.log("");
}, 4200);
function clientTerminate(client: Client, key: string, map: Map<string, Client>)
{
client.socket.terminate();
if (client.gameSession)
{
client.gameSession.playersMap.delete(key);
if (client.gameSession.playersMap.size === 0)
{
clearInterval(client.gameSession.clientsUpdateInterval);
clearInterval(client.gameSession.gameLoopInterval);
gameSessionsMap.delete(client.gameSession.id);
}
}
map.delete(key);
if (matchmakingPlayersMap.has(key)) {
matchmakingPlayersMap.delete(key);
}
}
function closeListener()
{
clearInterval(pingInterval);
}
function errorListener(error: Error)
{
console.log("Error: " + JSON.stringify(error));
}

View File

@@ -1,107 +0,0 @@
{
"include": ["src"],
// "exclude": ["node_modules"],
"compilerOptions": {
// "outDir": "./build",
// "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.gc. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.gc. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
// "module": "commonjs", /* Specify what module code is generated. */
"module": "ES6", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
"strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

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

View File

@@ -1,144 +0,0 @@
import { Vector, VectorInteger } from "./Vector.js";
import { Component, Moving } from "./interface.js";
import * as c from "../constants.js"
class Rectangle implements Component {
pos: VectorInteger;
width: number;
height: number;
constructor(pos: VectorInteger, width: number, height: number) {
this.pos = new VectorInteger(pos.x, pos.y);
this.width = width;
this.height = height;
}
collision(collider: Rectangle): boolean {
const thisLeft = this.pos.x;
const thisRight = this.pos.x + this.width;
const thisTop = this.pos.y;
const thisBottom = this.pos.y + this.height;
const colliderLeft = collider.pos.x;
const colliderRight = collider.pos.x + collider.width;
const colliderTop = collider.pos.y;
const colliderBottom = collider.pos.y + collider.height;
if ((thisBottom < colliderTop)
|| (thisTop > colliderBottom)
|| (thisRight < colliderLeft)
|| (thisLeft > colliderRight)) {
return false;
}
else {
return true;
}
}
}
class MovingRectangle extends Rectangle implements Moving {
dir: Vector = new Vector(0,0);
speed: number;
readonly baseSpeed: number;
constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(pos, width, height);
this.baseSpeed = baseSpeed;
this.speed = baseSpeed;
}
move(delta: number) { // Math.floor WIP until VectorInteger debug
// console.log(`delta: ${delta}, speed: ${this.speed}, speed*delta: ${this.speed * delta}`);
// this.pos.x += Math.floor(this.dir.x * this.speed * delta);
// this.pos.y += Math.floor(this.dir.y * this.speed * delta);
this.pos.x += this.dir.x * this.speed * delta;
this.pos.y += this.dir.y * this.speed * delta;
}
moveAndCollide(delta: number, colliderArr: Rectangle[]) {
this._moveAndCollideAlgo(delta, colliderArr);
}
protected _moveAndCollideAlgo(delta: number, colliderArr: Rectangle[]) {
let oldPos = new VectorInteger(this.pos.x, this.pos.y);
this.move(delta);
if (colliderArr.some(this.collision, this)) {
this.pos = oldPos;
}
}
}
class Racket extends MovingRectangle {
constructor(pos: VectorInteger, width: number, height: number, baseSpeed: number) {
super(pos, width, height, baseSpeed);
}
moveAndCollide(delta: number, colliderArr: Rectangle[]) {
// let oldPos = new VectorInteger(this.pos.x, this.pos.y); // debug
this._moveAndCollideAlgo(delta, colliderArr);
// console.log(`y change: ${this.pos.y - oldPos.y}`);
}
}
class Ball extends MovingRectangle {
readonly speedIncrease: number;
ballInPlay: boolean = false;
constructor(pos: VectorInteger, size: number, baseSpeed: number, speedIncrease: number) {
super(pos, size, size, baseSpeed);
this.speedIncrease = speedIncrease;
}
bounce(collider?: Rectangle) {
this._bounceAlgo(collider);
}
protected _bounceAlgo(collider?: Rectangle) {
/* Could be more generic, but testing only Racket is enough,
because in Pong collider can only be Racket or Wall. */
if (collider instanceof Racket) {
this._bounceRacket(collider);
}
else {
this._bounceWall();
}
}
moveAndBounce(delta: number, colliderArr: Rectangle[]) {
this.move(delta);
let i = colliderArr.findIndex(this.collision, this);
if (i != -1)
{
this.bounce(colliderArr[i]);
this.move(delta);
}
}
protected _bounceWall() { // Should be enough for Wall
this.dir.y = this.dir.y * -1;
}
protected _bounceRacket(racket: Racket) {
this._bounceRacketAlgo(racket);
}
protected _bounceRacketAlgo(racket: Racket) {
this.speed += this.speedIncrease;
let x = this.dir.x * -1;
const angleFactorDegree = 60;
const angleFactor = angleFactorDegree / 90;
const racketHalf = racket.height/2;
const ballMid = this.pos.y + this.height/2;
const racketMid = racket.pos.y + racketHalf;
let impact = ballMid - racketMid;
const horizontalMargin = racketHalf * 0.15;
if (impact < horizontalMargin && impact > -horizontalMargin) {
impact = 0;
}
else if (impact > 0) {
impact = impact - horizontalMargin;
}
else if (impact < 0) {
impact = impact + horizontalMargin;
}
let y = impact / (racketHalf - horizontalMargin) * angleFactor;
this.dir.assign(x, y);
// Normalize Vector (for consistency in speed independent of direction)
if (c.normalizedSpeed) {
this.dir = this.dir.normalized();
}
// console.log(`x: ${this.dir.x}, y: ${this.dir.y}`);
}
}
export {Rectangle, MovingRectangle, Racket, Ball}

View File

@@ -1,49 +0,0 @@
class Vector {
x: number;
y: number;
constructor(x: number = 0, y: number = 0) {
this.x = x;
this.y = y;
}
assign(x: number, y: number) {
this.x = x;
this.y = y;
}
normalized() : Vector {
const normalizationFactor = Math.abs(this.x) + Math.abs(this.y);
return new Vector(this.x/normalizationFactor, this.y/normalizationFactor);
}
}
class VectorInteger extends Vector {
// PLACEHOLDER
// VectorInteger with set/get dont work (No draw on the screen). Why ?
}
/*
class VectorInteger {
// private _x: number = 0;
// private _y: number = 0;
// constructor(x: number = 0, y: number = 0) {
// this._x = x;
// this._y = y;
// }
// get x(): number {
// return this._x;
// }
// set x(v: number) {
// // this._x = Math.floor(v);
// this._x = v;
// }
// get y(): number {
// return this._y;
// }
// set y(v: number) {
// // this._y = Math.floor(v);
// this._y = v;
// }
}
*/
export {Vector, VectorInteger}

View File

@@ -1,26 +0,0 @@
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);

View File

@@ -1,49 +0,0 @@
import * as c from "./constants.js";
import * as en from "../shared_js/enums.js"
import { gc, matchOptions, clientInfo } from "./global.js";
import { wallsMovements } from "../shared_js/wallsMovement.js";
let actual_time: number = Date.now();
let last_time: number;
let delta_time: number;
function gameLoop()
{
/* last_time = actual_time;
actual_time = Date.now();
delta_time = (actual_time - last_time) / 1000; */
delta_time = c.fixedDeltaTime;
// console.log(`delta_gameLoop: ${delta_time}`);
// interpolation
// console.log(`dir.y: ${clientInfo.opponent.dir.y}, pos.y: ${clientInfo.opponent.pos.y}, opponentNextPos.y: ${clientInfo.opponentNextPos.y}`);
if (clientInfo.opponent.dir.y != 0 ) {
opponentInterpolation(delta_time);
}
// client prediction
gc.ballsArr.forEach((ball) => {
ball.moveAndBounce(delta_time, [gc.wallTop, gc.wallBottom, gc.playerLeft, gc.playerRight]);
});
if (matchOptions & en.MatchOptions.movingWalls) {
wallsMovements(delta_time, gc);
}
}
function opponentInterpolation(delta: number)
{
// interpolation
clientInfo.opponent.moveAndCollide(delta, [gc.wallTop, gc.wallBottom]);
if ((clientInfo.opponent.dir.y > 0 && clientInfo.opponent.pos.y > clientInfo.opponentNextPos.y)
|| (clientInfo.opponent.dir.y < 0 && clientInfo.opponent.pos.y < clientInfo.opponentNextPos.y))
{
clientInfo.opponent.dir.y = 0;
clientInfo.opponent.pos.y = clientInfo.opponentNextPos.y;
}
}
export {gameLoop}

View File

@@ -1,3 +0,0 @@
export {pong, gc, matchOptions} from "./pong.js"
export {socket, clientInfo} from "./ws.js"

View File

@@ -4,7 +4,7 @@
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'> <meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte app</title> <title>Potato Pong</title>
<link rel='icon' type='image/png' href='/favicon.png'> <link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/global.css'>

View File

@@ -0,0 +1,9 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
// name: 'world'
}
});
export default app;
//# sourceMappingURL=main.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAC;AAE/B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC;IACnB,MAAM,EAAE,QAAQ,CAAC,IAAI;IACrB,KAAK,EAAE;IACN,gBAAgB;KAChB;CACD,CAAC,CAAC;AAEH,eAAe,GAAG,CAAC"}

View File

@@ -1,45 +1,104 @@
<script> <script lang="ts">
import "public/game/pong.js" import * as enumeration from './shared_js/enums'
import * as constants from './client/constants'
import { initPong, initGc, initMatchOptions, initStartFunction } from './client/global'
import { GameArea } from './client/class/GameArea';
import { GameComponentsClient } from './client/class/GameComponentsClient';
import { handleInput } from './client/handleInput';
import {gameLoop} from './client/gameLoop'
import { drawLoop } from './client/draw';
import {countdown} from './client/utils'
import { initWebSocket } from './client/ws';
import { initAudio } from './client/audio';
import { pong, gc} from './client/global'
let optionsAreNotSet = true
let sound = false;
let multi_balls = false;
let moving_walls = false;
let matchOption : enumeration.MatchOptions = enumeration.MatchOptions.noOption;
// En async au cas où pour la suite mais apriori inutile, les check se feront sûrement au onMount
const init = async() => {
if (multi_balls === true)
matchOption |= enumeration.MatchOptions.multiBalls
if (moving_walls === true )
matchOption |= enumeration.MatchOptions.movingWalls
initAudio(sound)
initMatchOptions(matchOption)
optionsAreNotSet = false
initPong(new GameArea())
initGc(new GameComponentsClient(matchOption, pong.ctx))
initStartFunction(start)
initWebSocket(matchOption)
}
function start() : void {
gc.text1.pos.assign(constants.w*0.5, constants.h*0.75);
countdown(constants.matchStartDelay/1000, (count: number) => {
gc.text1.clear();
gc.text1.text = `${count}`;
gc.text1.update();
}, resume);
}
function resume(): void {
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, constants.handleInputIntervalMS);
// pong.handleInputInterval = window.setInterval(sendLoop, c.sendLoopIntervalMS);
pong.gameLoopInterval = window.setInterval(gameLoop, constants.gameLoopIntervalMS);
pong.drawLoopInterval = window.setInterval(drawLoop, constants.drawLoopIntervalMS);
}
</script> </script>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head> </head>
<body> <body>
<div id="preload_font">.</div> <div id="preload_font">.</div>
{#if optionsAreNotSet}
<div id="div_game_options"> <form on:submit|preventDefault={init}>
<fieldset> <div id="div_game_options">
<legend>game options</legend> <fieldset>
<div> <legend>game options</legend>
<input type="checkbox" id="multi_balls" name="multi_balls"> <div>
<label for="multi_balls">multiples balls</label> <input type="checkbox" id="multi_balls" name="multi_balls" bind:checked={multi_balls}>
</div> <label for="multi_balls">Multiples balls</label>
<div> </div>
<input type="checkbox" id="moving_walls" name="moving_walls"> <div>
<label for="moving_walls">moving walls</label> <input type="checkbox" id="moving_walls" name="moving_walls" bind:checked={moving_walls}>
</div> <label for="moving_walls">Moving walls</label>
<div> </div>
<label>sound :</label> <div>
<input type="radio" id="sound_on" name="sound_selector" checked> <input type="checkbox" id="moving_walls" name="moving_walls" bind:checked={sound}>
<label for="sound_on">on</label> <label for="moving_walls">Sound</label>
<input type="radio" id="sound_off" name="sound_selector"> </div>
<label for="sound_off">off</label> <div>
</div> <button id="play_pong_button" >PLAY</button>
<div> </div>
<button id="play_pong_button">PLAY</button> </fieldset>
</div> </div>
</fieldset> </form>
<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>
{/if}
<div id="canvas_container"> <div id="canvas_container">
<!-- <p> =) </p> --> <!-- <p> =) </p> -->
</div> </div>
<script src="public/game/pong.js" type="module" defer></script>
</body> </body>
<style> <style>

View File

@@ -2,12 +2,12 @@
import * as c from "./constants.js" import * as c from "./constants.js"
export const soundPongArr: HTMLAudioElement[] = []; export const soundPongArr: HTMLAudioElement[] = [];
export const soundRoblox = new Audio("http://localhost:8080/sound/roblox-oof.ogg"); export const soundRoblox = new Audio("http://transcendance:8080/sound/roblox-oof.ogg");
export function initAudio(muteFlag: boolean) export function initAudio(muteFlag: boolean)
{ {
for (let i = 0; i <= 32; i++) { for (let i = 0; i <= 32; i++) {
soundPongArr.push(new Audio("http://localhost:8080/sound/pong/"+i+".ogg")); soundPongArr.push(new Audio("http://transcendance:8080/sound/pong/"+i+".ogg"));
soundPongArr[i].volume = c.soundPongVolume; soundPongArr[i].volume = c.soundPongVolume;
soundPongArr[i].muted = muteFlag; soundPongArr[i].muted = muteFlag;
} }

View File

@@ -1,7 +1,7 @@
import * as c from ".././constants.js" import * as c from ".././constants.js"
class GameArea { export class GameArea {
keys: string[] = []; keys: string[] = [];
handleInputInterval: number = 0; handleInputInterval: number = 0;
gameLoopInterval: number = 0; gameLoopInterval: number = 0;
@@ -34,5 +34,3 @@ class GameArea {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
} }
} }
export {GameArea}

View File

@@ -62,12 +62,13 @@ class GameComponentsExtensionForClient extends GameComponents {
} }
} }
export class GameComponentsClient extends GameComponentsExtensionForClient {
class GameComponentsClient extends GameComponentsExtensionForClient {
midLine: Line; midLine: Line;
scoreLeft: TextNumericValue; scoreLeft: TextNumericValue;
scoreRight: TextNumericValue; scoreRight: TextNumericValue;
text1: TextElem; text1: TextElem;
text2: TextElem;
text3: TextElem;
w_grid_mid: RectangleClient; w_grid_mid: RectangleClient;
w_grid_u1: RectangleClient; w_grid_u1: RectangleClient;
@@ -90,6 +91,8 @@ class GameComponentsClient extends GameComponentsExtensionForClient {
// Text // Text
pos.assign(0, c.h_mid); pos.assign(0, c.h_mid);
this.text1 = new TextElem(pos, Math.floor(c.w/8), ctx, "white"); 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 // Dotted Midline
pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize); pos.assign(c.w_mid-c.midLineSize/2, 0+c.wallSize);
@@ -110,5 +113,3 @@ class GameComponentsClient extends GameComponentsExtensionForClient {
this.h_grid_d1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen"); this.h_grid_d1 = new RectangleClient(pos, c.gridSize, c.h, ctx, "darkgreen");
} }
} }
export {GameComponentsClient}

View File

@@ -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"
class InputHistory { export class InputHistory {
input: en.InputEnum; input: en.InputEnum;
id: number; id: number;
deltaTime: number; deltaTime: number;
@@ -12,5 +12,3 @@ class InputHistory {
this.deltaTime = deltaTime; this.deltaTime = deltaTime;
} }
} }
export {InputHistory}

View File

@@ -17,7 +17,7 @@ function clearRectangle(this: RectangleClient, pos?: VectorInteger) {
this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height); this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height);
} }
class RectangleClient extends Rectangle implements GraphicComponent { export class RectangleClient extends Rectangle implements GraphicComponent {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
color: string; color: string;
update: () => void; update: () => void;
@@ -31,19 +31,9 @@ class RectangleClient extends Rectangle implements GraphicComponent {
this.update = updateRectangle; this.update = updateRectangle;
this.clear = clearRectangle; this.clear = clearRectangle;
} }
// update() {
// this.ctx.fillStyle = this.color;
// this.ctx.fillRect(this.pos.x, this.pos.y, this.width, this.height);
// }
// clear(pos?: VectorInteger) {
// if (pos)
// this.ctx.clearRect(pos.x, pos.y, this.width, this.height);
// else
// this.ctx.clearRect(this.pos.x, this.pos.y, this.width, this.height);
// }
} }
class MovingRectangleClient extends MovingRectangle implements GraphicComponent { export class MovingRectangleClient extends MovingRectangle implements GraphicComponent {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
color: string; color: string;
update: () => void; update: () => void;
@@ -59,7 +49,7 @@ class MovingRectangleClient extends MovingRectangle implements GraphicComponent
} }
} }
class RacketClient extends Racket implements GraphicComponent { export class RacketClient extends Racket implements GraphicComponent {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
color: string; color: string;
update: () => void; update: () => void;
@@ -75,7 +65,7 @@ class RacketClient extends Racket implements GraphicComponent {
} }
} }
class BallClient extends Ball implements GraphicComponent { export class BallClient extends Ball implements GraphicComponent {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
color: string; color: string;
update: () => void; update: () => void;
@@ -93,10 +83,6 @@ class BallClient extends Ball implements GraphicComponent {
this._bounceAlgo(collider); this._bounceAlgo(collider);
soundPongArr[ Math.floor(random(0, soundPongArr.length)) ].play(); soundPongArr[ Math.floor(random(0, soundPongArr.length)) ].play();
} }
/* protected _bounceRacket(collider: Racket) {
this._bounceRacketAlgo(collider);
soundRoblox.play();
} */
} }
function updateLine(this: Line) { function updateLine(this: Line) {
@@ -105,17 +91,20 @@ function updateLine(this: Line) {
let i = 0; let i = 0;
while (i < this.segmentCount) while (i < this.segmentCount)
{ {
// for Horizontal Line /* Horizontal Line */
// pos.y = this.pos.y; // pos.y = this.pos.y;
// pos.x = this.pos.x + this.segmentWidth * i; // pos.x = this.pos.x + this.segmentWidth * i;
/* Vertical Line */
pos.x = this.pos.x; pos.x = this.pos.x;
pos.y = this.pos.y + this.segmentHeight * i; pos.y = this.pos.y + this.segmentHeight * i;
this.ctx.fillRect(pos.x, pos.y, this.segmentWidth, this.segmentHeight); this.ctx.fillRect(pos.x, pos.y, this.segmentWidth, this.segmentHeight);
i += 2; i += 2;
} }
} }
class Line extends RectangleClient { export class Line extends RectangleClient {
gapeCount: number = 0; gapeCount: number = 0;
segmentCount: number; segmentCount: number;
segmentWidth: number; segmentWidth: number;
@@ -129,13 +118,12 @@ class Line extends RectangleClient {
this.gapeCount = gapeCount; this.gapeCount = gapeCount;
this.segmentCount = this.gapeCount * 2 + 1; this.segmentCount = this.gapeCount * 2 + 1;
/* Vertical Line */
this.segmentWidth = this.width; this.segmentWidth = this.width;
this.segmentHeight = this.height / this.segmentCount; this.segmentHeight = this.height / this.segmentCount;
// for Horizontal Line /* Horizontal Line */
// this.segmentWidth = this.width / this.segmentCount; // this.segmentWidth = this.width / this.segmentCount;
// this.segmentHeight = this.height; // this.segmentHeight = this.height;
} }
} }
export {RectangleClient, MovingRectangleClient, RacketClient, BallClient, Line}

View File

@@ -3,7 +3,7 @@ import { Vector, VectorInteger } from "../../shared_js/class/Vector.js";
import { Component } from "../../shared_js/class/interface.js"; import { Component } from "../../shared_js/class/interface.js";
// conflict with Text // conflict with Text
class TextElem implements Component { export class TextElem implements Component {
ctx: CanvasRenderingContext2D; ctx: CanvasRenderingContext2D;
pos: VectorInteger; pos: VectorInteger;
color: string; color: string;
@@ -39,7 +39,7 @@ class TextElem implements Component {
} }
} }
class TextNumericValue extends TextElem { export class TextNumericValue extends TextElem {
private _value: number = 0; private _value: number = 0;
constructor(pos: VectorInteger, size: number, constructor(pos: VectorInteger, size: number,
ctx: CanvasRenderingContext2D, color: string, font?: string) ctx: CanvasRenderingContext2D, color: string, font?: string)
@@ -54,5 +54,3 @@ class TextNumericValue extends TextElem {
this.text = v.toString(); this.text = v.toString();
} }
} }
export {TextElem, TextNumericValue}

Some files were not shown because too many files have changed in this diff Show More