feat(pong): reworked pong to use sane system as ttt
This commit is contained in:
parent
afd79e334c
commit
0b15fd897b
20 changed files with 637 additions and 707 deletions
|
|
@ -1,19 +0,0 @@
|
|||
import { color } from './pong';
|
||||
|
||||
/**
|
||||
* function adds a message to the frontend pongMessage
|
||||
* ATTENTION send inner HTML ******
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function addPongMessage(text: string) {
|
||||
const pongMessage = document.getElementById("system-box") as HTMLDivElement;
|
||||
if (!pongMessage) return;
|
||||
const messageElement = document.createElement("div-test");
|
||||
messageElement.innerHTML = text;
|
||||
pongMessage.appendChild(messageElement);
|
||||
pongMessage.scrollTop = pongMessage.scrollHeight;
|
||||
console.log(`%c DEBUG LOG: Added PONG new message:%c ${text}`, color.red, color.reset);
|
||||
return ;
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { addPongMessage } from "./addPongMessage";
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { getUser } from "@app/auth";
|
||||
|
||||
/**
|
||||
* function sends socket.emit to the backend to active and a broadcast message to all sockets
|
||||
* echos addPongMessage() the message with addMessage to the sender
|
||||
* @param socket
|
||||
* @param msgCommand
|
||||
*/
|
||||
export function broadcastMsg (socket: Socket, msgCommand: string[]): void {
|
||||
let msgText = msgCommand[1] ?? "";
|
||||
addPongMessage(msgText);
|
||||
const user = getUser();
|
||||
if (user && socket?.connected) {
|
||||
const message = {
|
||||
command: msgCommand,
|
||||
destination: '',
|
||||
type: "chat",
|
||||
user: user.name,
|
||||
token: document.cookie,
|
||||
text: msgText,
|
||||
timestamp: Date.now(),
|
||||
SenderWindowID: socket.id,
|
||||
};
|
||||
socket.emit('message', JSON.stringify(message));
|
||||
}
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { getUser } from "@app/auth";
|
||||
import type { User } from '@app/auth'
|
||||
/**
|
||||
* function checks if logged in
|
||||
* @returns either user | null
|
||||
*/
|
||||
export function isLoggedIn(): User | null {
|
||||
return getUser() || null;
|
||||
};
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<div id="batleft" class="pong-batleft bg-amber-400 top-0"></div>
|
||||
<div class="pong-center-line"></div>
|
||||
<div id="batright" class="pong-batright bg-amber-400 top-0"></div>
|
||||
<div id="ball" class="w-8 h-8 rounded-full border-4 bg-white border-gray-400"></div>
|
||||
<div id="ball" class="rounded-full border-4 bg-white border-gray-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,276 +1,36 @@
|
|||
import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
|
||||
import { showError } from "@app/toast";
|
||||
import authHtml from './pong.html?raw';
|
||||
import client from '@app/api'
|
||||
import { getUser, updateUser } from "@app/auth";
|
||||
import io, { Socket } from 'socket.io-client';
|
||||
import { addPongMessage } from './addPongMessage';
|
||||
import { isLoggedIn } from './isLoggedIn';
|
||||
import type { ClientMessage, ClientProfil } from './types_front';
|
||||
import { isNullish } from "@app/utils";
|
||||
|
||||
export const color = {
|
||||
red: 'color: red;',
|
||||
green: 'color: green;',
|
||||
yellow: 'color: orange;',
|
||||
blue: 'color: blue;',
|
||||
reset: '',
|
||||
};
|
||||
import io from 'socket.io-client';
|
||||
import type { CSocket, GameMove, GameUpdate } from "./socket";
|
||||
import { showError, showInfo } from "@app/toast";
|
||||
|
||||
// TODO: local game (2player -> server -> 2player : current setup)
|
||||
// TODO: tournament via remote (dedicated queu? idk)
|
||||
//
|
||||
|
||||
// get the name of the machine used to connect
|
||||
const machineHostName = window.location.hostname;
|
||||
console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow);
|
||||
declare module 'ft_state' {
|
||||
interface State {
|
||||
pongSock?: CSocket;
|
||||
}
|
||||
}
|
||||
|
||||
export let __socket: Socket | undefined = undefined;
|
||||
|
||||
document.addEventListener('ft:pageChange', () => { // dont regen socket on page change from forward/backward navigation arrows
|
||||
if (__socket !== undefined)
|
||||
__socket.close();
|
||||
__socket = undefined;
|
||||
console.log("Page changed");
|
||||
document.addEventListener("ft:pageChange", () => {
|
||||
if (window.__state.pongSock !== undefined) window.__state.pongSock.close();
|
||||
window.__state.pongSock = undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns the initialized socket
|
||||
*/
|
||||
export function getSocket(): Socket {
|
||||
let addressHost = `wss://${machineHostName}:8888`;
|
||||
|
||||
if (__socket === undefined)
|
||||
__socket = io(addressHost, {
|
||||
path: "/api/pong/socket.io/",
|
||||
secure: false,
|
||||
transports: ["websocket"],
|
||||
});
|
||||
return __socket;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param socket The socket to wait for
|
||||
* @returns voir or a promise<void>
|
||||
*/
|
||||
function waitSocketConnected(socket: Socket): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (socket.connected) return resolve();
|
||||
socket.on("connect", () => resolve());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param socket The socket to communicat
|
||||
* @returns nothing
|
||||
*/
|
||||
async function whoami(socket: Socket) {
|
||||
try {
|
||||
const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement;
|
||||
|
||||
const res = await client.guestLogin();
|
||||
switch (res.kind) {
|
||||
case 'success': {
|
||||
let user = await updateUser();
|
||||
if (chatWindow) {
|
||||
socket.emit('updateClientName', {
|
||||
oldUser: '',
|
||||
user: user?.name
|
||||
});
|
||||
export function getSocket(): CSocket {
|
||||
if (window.__state.pongSock === undefined)
|
||||
window.__state.pongSock = io(window.location.host, { path: "/api/pong/socket.io/" }) as any as CSocket;
|
||||
return window.__state.pongSock;
|
||||
}
|
||||
if (user === null)
|
||||
return showError('Failed to get user: no user ?');
|
||||
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`);
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
showError(`Failed to login: ${res.msg}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Login error:", e);
|
||||
showError('Failed to login: Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
|
||||
let socket = getSocket();
|
||||
|
||||
socket.on("connect", async () => {
|
||||
const systemWindow = document.getElementById('system-box') as HTMLDivElement;
|
||||
await waitSocketConnected(socket);
|
||||
console.log("I AM Connected to the server:", socket.id);
|
||||
// const message = {
|
||||
// command: "",
|
||||
// destination: 'system-info',
|
||||
// type: "chat",
|
||||
// user: getUser()?.name,
|
||||
// token: document.cookie ?? "",
|
||||
// text: " has Just ARRIVED in the chat",
|
||||
// timestamp: Date.now(),
|
||||
// SenderWindowID: socket.id,
|
||||
// };
|
||||
// socket.emit('message', JSON.stringify(message));
|
||||
const messageElement = document.createElement("div");
|
||||
// messageElement.textContent = `${message.user}: is connected au server`;
|
||||
messageElement.textContent = `${getUser()?.name ?? "unkown user"}: is connected au server`;
|
||||
systemWindow.appendChild(messageElement);
|
||||
systemWindow.scrollTop = systemWindow.scrollHeight;
|
||||
});
|
||||
|
||||
// Queu handler
|
||||
async function joinQueu(socket : Socket) {
|
||||
try {
|
||||
const res = await client.guestLogin();
|
||||
|
||||
switch (res.kind) {
|
||||
case 'success': {
|
||||
let user = await updateUser();
|
||||
|
||||
if (user === null)
|
||||
return showError('Failed to get user: no user ?');
|
||||
socket.emit('queuJoin', user.id);
|
||||
console.log('queu join sent for : ', user.id);
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
showError(`Failed to Join Game Queu: ${res.msg}`);
|
||||
console.log('Failed to Join Game Queu');
|
||||
}
|
||||
}
|
||||
} catch (err ) {
|
||||
showError(`Failed to Join Game Queu`);
|
||||
console.log('Failed to Join Game Queu');
|
||||
}
|
||||
}
|
||||
|
||||
// keys handler
|
||||
const keys: Record<string, boolean> = {};
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
});
|
||||
document.addEventListener("keyup", (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
setInterval(() => { // key sender
|
||||
if ((keys['w'] || keys['s']) && !(keys['w'] && keys['s'])) { // exclusive or to filter requests
|
||||
if (keys['w']) {
|
||||
socket.emit("batmove_Left", "up");
|
||||
console.log('north key pressed - emit batmove_Left up');
|
||||
}
|
||||
if (keys['s']) {
|
||||
socket.emit("batmove_Left", "down");
|
||||
console.log('south key pressed - emit batmove_Left down');
|
||||
}
|
||||
}
|
||||
if ((keys['p'] || keys['l']) && !(keys['p'] && keys['l'])) { // exclusive or to filter requests
|
||||
if (keys['p']) {
|
||||
socket.emit("batmove_Right", "up");
|
||||
console.log('north key pressed - emit batmove_Right up');
|
||||
}
|
||||
if (keys['l']) {
|
||||
socket.emit("batmove_Right", "down");
|
||||
console.log('south key pressed - emit batmove_Right down');
|
||||
}
|
||||
}
|
||||
}, 16);
|
||||
|
||||
// Pong Objects updators
|
||||
socket.on("batLeft_update", (y: number) => {
|
||||
console.log('batLeft_update received y: ', y);
|
||||
const bat = document.getElementById("batleft") as HTMLDivElement | null;
|
||||
if (!bat) {
|
||||
console.error("FATAL ERROR: Bat element with ID 'bat-left' not found. Check HTML.");
|
||||
return ;
|
||||
}
|
||||
if (typeof y === 'number' && !isNaN(y)) {
|
||||
bat.style.transform = `translateY(${y}px)`;
|
||||
} else {
|
||||
console.warn(`Received invalid Y value: ${y}`);
|
||||
}
|
||||
});
|
||||
socket.on("batRight_update", (y: number) => {
|
||||
console.log('batRight_update received y: ', y);
|
||||
const bat = document.getElementById("batright") as HTMLDivElement | null;
|
||||
if (!bat) {
|
||||
console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML.");
|
||||
return ;
|
||||
}
|
||||
if (typeof y === 'number' && !isNaN(y)) {
|
||||
bat.style.transform = `translateY(${y}px)`;
|
||||
} else {
|
||||
console.warn(`Received invalid Y value: ${y}`);
|
||||
}
|
||||
});
|
||||
socket.on("ballPos_update", (x:number, y : number) => {
|
||||
console.log('ballPos_update recieved');
|
||||
const ball = document.getElementById("ball") as HTMLDivElement | null;
|
||||
|
||||
if (!ball) {
|
||||
console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML.");
|
||||
return ;
|
||||
}
|
||||
if (typeof y !== 'number' || isNaN(y) || typeof x !== 'number' || isNaN(x)) {
|
||||
console.warn(`Received invalid X/Y value: ${x} / ${y}`);
|
||||
return ;
|
||||
}
|
||||
ball.style.transform = `translateY(${y}px)`;
|
||||
ball.style.transform += `translateX(${x}px)`;
|
||||
});
|
||||
|
||||
// socket.once('welcome', (data) => {
|
||||
// console.log('%cWelcome PONG PAGE', color.yellow );
|
||||
// addPongMessage('socket.once \'Welcome\' called')
|
||||
// });
|
||||
|
||||
// Listen for messages from the server "MsgObjectServer"
|
||||
socket.on("MsgObjectServer", (data: { message: ClientMessage}) => {
|
||||
// Display the message in the chat window
|
||||
console.log("message recieved : ", data.message.text);
|
||||
const systemWindow = document.getElementById('system-box') as HTMLDivElement;
|
||||
const MAX_SYSTEM_MESSAGES = 10;
|
||||
|
||||
if (systemWindow && data.message.destination === "system-info") {
|
||||
const messageElement = document.createElement("div");
|
||||
messageElement.textContent = `${data.message.user}: ${data.message.text}`;
|
||||
systemWindow.appendChild(messageElement);
|
||||
|
||||
// keep only last 10
|
||||
while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) {
|
||||
systemWindow.removeChild(systemWindow.firstChild!);
|
||||
}
|
||||
systemWindow.scrollTop = systemWindow.scrollHeight;
|
||||
}
|
||||
if (systemWindow && data.message.destination === "score-info") {
|
||||
console.log("score update:", data.message.text);
|
||||
const scoreboard = document.getElementById('score-board') as HTMLHeadingElement ;
|
||||
|
||||
if (!scoreboard) {
|
||||
console.log("update score failed :(");
|
||||
return ;
|
||||
}
|
||||
scoreboard.textContent = `${data.message.text}`;
|
||||
}
|
||||
// console.log("Getuser():", getUser());
|
||||
});
|
||||
|
||||
setTitle('Pong Game Page');
|
||||
return {
|
||||
|
||||
html: authHtml, postInsert: async (app) => {
|
||||
const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement;
|
||||
const bqueu = document.getElementById('b-joinQueu') as HTMLButtonElement;
|
||||
|
||||
bwhoami?.addEventListener('click', async () => {
|
||||
whoami(socket);
|
||||
});
|
||||
bqueu?.addEventListener('click', async () => {
|
||||
joinQueu(socket);
|
||||
});
|
||||
|
||||
const checkbox = document.getElementById("modeToggle") as HTMLInputElement;
|
||||
const label = document.getElementById("toggleLabel") as HTMLSpanElement;
|
||||
const track = document.getElementById("toggleTrack") as HTMLDivElement;
|
||||
|
|
@ -287,7 +47,67 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn
|
|||
knob.classList.remove("translate-x-7");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
const batLeft = document.querySelector<HTMLDivElement>("#batleft");
|
||||
const batRight = document.querySelector<HTMLDivElement>("#batright");
|
||||
const ball = document.querySelector<HTMLDivElement>("#ball");
|
||||
const score = document.querySelector<HTMLDivElement>("#score-board");
|
||||
if (!batLeft || !batRight || !ball || !score)
|
||||
return showError('fatal error');
|
||||
|
||||
let socket = getSocket();
|
||||
|
||||
// keys handler
|
||||
const keys: Record<string, boolean> = {};
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
});
|
||||
document.addEventListener("keyup", (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
setInterval(() => { // key sender
|
||||
let packet: GameMove = {
|
||||
move: null,
|
||||
}
|
||||
if ((keys['w'] !== keys['s'])) {
|
||||
packet.move = keys['w'] ? 'up' : 'down';
|
||||
}
|
||||
|
||||
socket.emit('gameMove', packet);
|
||||
}, 1000 / 60);
|
||||
|
||||
const render = (state: GameUpdate) => {
|
||||
//batLeft.style.transform = `translateY(${state.left.paddle.y}px) translateX(${state.left.paddle.x}px)`;
|
||||
batLeft.style.top = `${state.left.paddle.y}px`;
|
||||
batLeft.style.left = `${state.left.paddle.x}px`;
|
||||
batLeft.style.width = `${state.left.paddle.width}px`;
|
||||
batLeft.style.height = `${state.left.paddle.height}px`;
|
||||
|
||||
//batRight.style.transform = `translateY(${state.right.paddle.y}px) translateX(-${state.left.paddle.x}px)`;
|
||||
batRight.style.top = `${state.right.paddle.y}px`;
|
||||
batRight.style.left = `${state.right.paddle.x}px`;
|
||||
batRight.style.width = `${state.right.paddle.width}px`;
|
||||
batRight.style.height = `${state.right.paddle.height}px`;
|
||||
|
||||
ball.style.transform = `translateX(${state.ball.x - state.ball.size}px) translateY(${state.ball.y - state.ball.size}px)`;
|
||||
ball.style.height = `${state.ball.size * 2}px`;
|
||||
ball.style.width = `${state.ball.size * 2}px`;
|
||||
|
||||
|
||||
score.innerText = `${state.left.score} | ${state.right.score}`
|
||||
}
|
||||
|
||||
socket.on('gameUpdate', (state: GameUpdate) => render(state));
|
||||
socket.on('newGame', (state) => render(state));
|
||||
|
||||
socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`));
|
||||
socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`));
|
||||
socket.emit('enqueue');
|
||||
}
|
||||
}
|
||||
};
|
||||
addRoute('/pong', pongClient, { bypass_auth: true });
|
||||
addRoute('/pong', pongClient);
|
||||
|
|
|
|||
47
frontend/src/pages/pong/socket.ts
Normal file
47
frontend/src/pages/pong/socket.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Socket } from 'socket.io-client';
|
||||
|
||||
export type UpdateInfo = {
|
||||
inQueue: number,
|
||||
totalUser: number,
|
||||
}
|
||||
|
||||
export type PaddleData = {
|
||||
x: number,
|
||||
y: number,
|
||||
|
||||
width: number,
|
||||
height: number,
|
||||
};
|
||||
|
||||
export type GameUpdate = {
|
||||
gameId: string;
|
||||
|
||||
left: { id: string, paddle: PaddleData, score: number };
|
||||
right: { id: string, paddle: PaddleData, score: number };
|
||||
|
||||
ball: { x: number, y: number, size: number };
|
||||
}
|
||||
|
||||
export type GameMove = {
|
||||
move: 'up' | 'down' | null,
|
||||
}
|
||||
|
||||
export interface ClientToServer {
|
||||
enqueue: () => void;
|
||||
dequeue: () => void;
|
||||
debugInfo: () => void;
|
||||
gameMove: (up: GameMove) => void;
|
||||
connectedToGame: (gameId: string) => void;
|
||||
};
|
||||
|
||||
export interface ServerToClient {
|
||||
forceDisconnect: (reason: string) => void;
|
||||
queueEvent: (msg: 'registered' | 'unregistered') => void;
|
||||
updateInformation: (info: UpdateInfo) => void,
|
||||
newGame: (initState: GameUpdate) => void,
|
||||
gameUpdate: (state: GameUpdate) => void,
|
||||
gameEnd: () => void;
|
||||
};
|
||||
|
||||
export type SSocket = Socket<ClientToServer, ServerToClient>;
|
||||
export type CSocket = Socket<ServerToClient, ClientToServer>;
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
export type ClientMessage = {
|
||||
command: string
|
||||
destination: string;
|
||||
user: string;
|
||||
text: string;
|
||||
SenderWindowID: string;
|
||||
};
|
||||
|
||||
export type ClientProfil = {
|
||||
command: string,
|
||||
destination: string,
|
||||
type: string,
|
||||
user: string,
|
||||
loginName: string,
|
||||
userID: string,
|
||||
text: string,
|
||||
timestamp: number,
|
||||
SenderWindowID:string,
|
||||
SenderName: string,
|
||||
Sendertext: string,
|
||||
innerHtml?: string,
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -x
|
||||
# do anything here
|
||||
|
||||
# run the CMD [ ... ] from the dockerfile
|
||||
exec "$@"
|
||||
12
src/pong/src/@types/socket.io.d.ts
vendored
Normal file
12
src/pong/src/@types/socket.io.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { type UserId } from '@shared/database/mixin/user';
|
||||
|
||||
declare module 'socket.io'
|
||||
{
|
||||
interface Socket {
|
||||
authUser: {
|
||||
id: UserId;
|
||||
name: string;
|
||||
guest: boolean;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -6,32 +6,11 @@ import * as auth from '@shared/auth';
|
|||
import * as swagger from '@shared/swagger';
|
||||
import * as utils from '@shared/utils';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { broadcast } from './broadcast';
|
||||
import type { ClientProfil, ClientMessage } from './chat_types';
|
||||
import { sendInvite } from './sendInvite';
|
||||
import { setGameLink } from './setGameLink';
|
||||
import { UserId } from '@shared/database/mixin/user';
|
||||
|
||||
// colors for console.log
|
||||
export const color = {
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
import { newState, State } from './state';
|
||||
import { ClientToServer, ServerToClient } from './socket';
|
||||
|
||||
declare const __SERVICE_NAME: string;
|
||||
|
||||
// Global map of clients
|
||||
// key = socket, value = clientname
|
||||
interface ClientInfo {
|
||||
user: string;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export const clientChat = new Map<string, ClientInfo>();
|
||||
|
||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
|
||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||
|
|
@ -60,6 +39,7 @@ const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
|||
|
||||
fastify.ready((err) => {
|
||||
if (err) throw err;
|
||||
newState(fastify);
|
||||
onReady(fastify);
|
||||
});
|
||||
};
|
||||
|
|
@ -69,266 +49,13 @@ export { app };
|
|||
// When using .decorate you have to specify added properties for Typescript
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
io: Server<{
|
||||
inviteGame: (data: ClientProfil) => void;
|
||||
message: (msg: string) => void;
|
||||
batmove_Left: (direction: 'up' | 'down') => void;
|
||||
batmove_Right: (direction: 'up' | 'down') => void;
|
||||
batLeft_update: (y: number) => void;
|
||||
batRight_update: (y: number) => void;
|
||||
ballPos_update: (x: number, y: number) => void;
|
||||
MsgObjectServer: (data: { message: ClientMessage }) => void;
|
||||
queuJoin: (userID: UserId) => void;
|
||||
}>;
|
||||
io: Server<ClientToServer, ServerToClient>;
|
||||
}
|
||||
}
|
||||
|
||||
function isInRange(x: number, low: number, high: number) {
|
||||
if (x >= low && x <= high) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function sendScore(
|
||||
socket: Socket,
|
||||
scoreLeft: number,
|
||||
scoreRight: number,
|
||||
) {
|
||||
// idk why, sometimes... it fails?
|
||||
const msg: ClientMessage = {
|
||||
destination: 'score-info',
|
||||
command: '',
|
||||
user: '',
|
||||
text: scoreLeft.toString() + ':' + scoreRight.toString(),
|
||||
SenderWindowID: '',
|
||||
};
|
||||
|
||||
socket.emit('MsgObjectServer', { message: msg });
|
||||
}
|
||||
|
||||
async function onReady(fastify: FastifyInstance) {
|
||||
// shows address for connection au server transcendance
|
||||
const session = process.env.SESSION_MANAGER ?? '';
|
||||
if (session) {
|
||||
const part = session.split('/')[1];
|
||||
const machineName = part.split('.')[0];
|
||||
console.log(
|
||||
color.yellow,
|
||||
'Connect at : https://' + machineName + ':8888/app/login',
|
||||
);
|
||||
}
|
||||
|
||||
// DRAW AREA
|
||||
// top edge of the field
|
||||
const TOP_EDGE = 0;
|
||||
// bottom edge of the field;
|
||||
const BOTTOM_EDGE = 450;
|
||||
const LEFT_EDGE = 0;
|
||||
const RIGHT_EDGE = 800;
|
||||
|
||||
void LEFT_EDGE;
|
||||
|
||||
// PADDLEs
|
||||
const PADDLE_HEIGHT = 80;
|
||||
const PADDLE_WIDTH = 12;
|
||||
|
||||
const PADDLE_SPEED = 20;
|
||||
const PADDLE_X_OFFSET = 4;
|
||||
|
||||
// 370
|
||||
const MAX_PADDLE_Y = BOTTOM_EDGE - PADDLE_HEIGHT;
|
||||
// 185
|
||||
const PADDLE_START = BOTTOM_EDGE / 2 - PADDLE_HEIGHT / 2;
|
||||
|
||||
// BALL
|
||||
// widht times 2 bc rounded on moth sides + 4 for border
|
||||
const BALL_SIZE = 8 * 2 + 4;
|
||||
const START_BALLX = RIGHT_EDGE / 2 - BALL_SIZE;
|
||||
const START_BALLY = BOTTOM_EDGE / 2 - BALL_SIZE;
|
||||
|
||||
const ACCELERATION_FACTOR = 1.15;
|
||||
const ABS_MAX_BALL_SPEED = 3;
|
||||
|
||||
// val inits
|
||||
// shared start bat position
|
||||
let paddleLeft = PADDLE_START;
|
||||
// shared start bat position
|
||||
let paddleRight = PADDLE_START;
|
||||
|
||||
let ballPosX = START_BALLX;
|
||||
let ballPosY = START_BALLY;
|
||||
let ballSpeedX = -1;
|
||||
let ballSpeedY = -1;
|
||||
let scoreL = 0;
|
||||
let scoreR = 0;
|
||||
|
||||
// uuid, game uid - if not in game empty string
|
||||
const games: Record<UserId, string> = {};
|
||||
|
||||
fastify.io.on('connection', (socket: Socket) => {
|
||||
socket.emit('batLeft_update', paddleLeft);
|
||||
socket.emit('batRight_update', paddleRight);
|
||||
socket.emit('ballPos_update', ballPosX, ballPosY);
|
||||
sendScore(socket, scoreL, scoreR);
|
||||
|
||||
// GAME
|
||||
// paddle handling
|
||||
socket.on('batmove_Left', (direction: 'up' | 'down') => {
|
||||
if (direction === 'up') {
|
||||
paddleLeft -= PADDLE_SPEED;
|
||||
}
|
||||
if (direction === 'down') {
|
||||
paddleLeft += PADDLE_SPEED;
|
||||
}
|
||||
// position of bat leftplokoplpl
|
||||
paddleLeft = Math.max(TOP_EDGE, Math.min(MAX_PADDLE_Y, paddleLeft));
|
||||
console.log('batLeft_update:', paddleLeft);
|
||||
socket.emit('batLeft_update', paddleLeft);
|
||||
});
|
||||
socket.on('batmove_Right', (direction: 'up' | 'down') => {
|
||||
if (direction === 'up') {
|
||||
paddleRight -= PADDLE_SPEED;
|
||||
}
|
||||
if (direction === 'down') {
|
||||
paddleRight += PADDLE_SPEED;
|
||||
}
|
||||
// position of bat left
|
||||
paddleRight = Math.max(
|
||||
TOP_EDGE,
|
||||
Math.min(MAX_PADDLE_Y, paddleRight),
|
||||
);
|
||||
socket.emit('batRight_update', paddleRight);
|
||||
});
|
||||
// ball handling:
|
||||
setInterval(async () => {
|
||||
const new_ballPosX = ballPosX + ballSpeedX;
|
||||
const new_ballPosY = ballPosY + ballSpeedY;
|
||||
|
||||
if (
|
||||
((isInRange(
|
||||
new_ballPosY,
|
||||
paddleLeft,
|
||||
paddleLeft + PADDLE_HEIGHT,
|
||||
) ||
|
||||
isInRange(
|
||||
new_ballPosY + BALL_SIZE * 2,
|
||||
paddleLeft,
|
||||
paddleLeft + PADDLE_HEIGHT,
|
||||
)) &&
|
||||
// y ok ?
|
||||
isInRange(
|
||||
new_ballPosX,
|
||||
PADDLE_X_OFFSET,
|
||||
PADDLE_X_OFFSET + PADDLE_WIDTH,
|
||||
) &&
|
||||
ballSpeedX < 0) ||
|
||||
// x ok? && ball going toward paddle?
|
||||
((isInRange(
|
||||
new_ballPosY,
|
||||
paddleRight,
|
||||
paddleRight + PADDLE_HEIGHT,
|
||||
) ||
|
||||
isInRange(
|
||||
new_ballPosY + BALL_SIZE * 2,
|
||||
paddleRight,
|
||||
paddleRight + PADDLE_HEIGHT,
|
||||
)) &&
|
||||
// right side equations
|
||||
isInRange(
|
||||
new_ballPosX + BALL_SIZE * 2,
|
||||
RIGHT_EDGE - PADDLE_X_OFFSET - PADDLE_WIDTH,
|
||||
RIGHT_EDGE - PADDLE_X_OFFSET,
|
||||
) &&
|
||||
ballSpeedX > 0)
|
||||
) {
|
||||
ballSpeedX *= -1;
|
||||
ballSpeedX *= ACCELERATION_FACTOR;
|
||||
ballSpeedY *= ACCELERATION_FACTOR;
|
||||
console.log('bat colision');
|
||||
}
|
||||
else if (
|
||||
new_ballPosX < 0 ||
|
||||
new_ballPosX + BALL_SIZE * 2 > RIGHT_EDGE
|
||||
) {
|
||||
ballPosX = START_BALLX;
|
||||
ballPosY = START_BALLY;
|
||||
ballSpeedX = Math.random() - 0.5 < 0 ? -1 : 1;
|
||||
|
||||
if (new_ballPosX < 0) {
|
||||
scoreR += 1;
|
||||
ballSpeedY = -1;
|
||||
}
|
||||
else {
|
||||
scoreL += 1;
|
||||
ballSpeedY = 1;
|
||||
}
|
||||
if (scoreL >= 5 || scoreR >= 5) {
|
||||
console.log('game should stop + board reset');
|
||||
// temp solution
|
||||
ballSpeedX = 0;
|
||||
ballSpeedY = 0;
|
||||
// reset board :D
|
||||
}
|
||||
console.log('point scored');
|
||||
sendScore(socket, scoreL, scoreR);
|
||||
// TODO: score point + ball reset + spd reset
|
||||
}
|
||||
else if (
|
||||
new_ballPosY < 0 ||
|
||||
new_ballPosY + BALL_SIZE * 2 > BOTTOM_EDGE
|
||||
) {
|
||||
ballSpeedY *= -1;
|
||||
ballSpeedX *= ACCELERATION_FACTOR;
|
||||
ballSpeedY *= ACCELERATION_FACTOR;
|
||||
}
|
||||
ballSpeedX = Math.max(
|
||||
-ABS_MAX_BALL_SPEED,
|
||||
Math.min(ballSpeedX, ABS_MAX_BALL_SPEED),
|
||||
);
|
||||
ballSpeedY = Math.max(
|
||||
-ABS_MAX_BALL_SPEED,
|
||||
Math.min(ballSpeedY, ABS_MAX_BALL_SPEED),
|
||||
);
|
||||
|
||||
ballPosX += ballSpeedX;
|
||||
ballPosY += ballSpeedY;
|
||||
|
||||
socket.emit('ballPos_update', ballPosX, ballPosY);
|
||||
}, 16);
|
||||
|
||||
// QUEUE HANDL
|
||||
socket.on('queuJoin', async (uuid: UserId) => {
|
||||
console.log('queu join recieved for : ', uuid);
|
||||
if (!(uuid in games.hasOwnProperty)) {
|
||||
console.log('new user in game search queu');
|
||||
games[uuid] = '';
|
||||
}
|
||||
else if (uuid in games && games[uuid] == '') {
|
||||
console.log('already searching for game');
|
||||
}
|
||||
else {
|
||||
// (games.hasOwnProperty(uuid) && games[uuid] != "") {
|
||||
console.log('user alredy in game');
|
||||
return;
|
||||
}
|
||||
// TODO: step2 : sesrch in record<> find guid w/ "" &/ pair them up
|
||||
// TODO: step3 : move game logic to lifecycle of queu'ed game
|
||||
});
|
||||
|
||||
// other:
|
||||
socket.on('message', (message: string) => {
|
||||
const obj: ClientMessage = JSON.parse(message) as ClientMessage;
|
||||
clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() });
|
||||
socket.emit('welcome', { msg: 'Welcome to the chat! : ' });
|
||||
broadcast(fastify, obj, obj.SenderWindowID);
|
||||
});
|
||||
socket.on('inviteGame', async (data: string) => {
|
||||
const clientName: string = clientChat.get(socket.id)?.user || '';
|
||||
const profilInvite: ClientProfil = JSON.parse(data) || '';
|
||||
const inviteHtml: string =
|
||||
'invites you to a game ' + setGameLink('');
|
||||
if (clientName !== null) {
|
||||
sendInvite(fastify, inviteHtml, profilInvite);
|
||||
}
|
||||
});
|
||||
fastify.log.info(`Client connected: ${socket.id}`);
|
||||
State.registerUser(socket);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import type { ClientMessage } from './chat_types';
|
||||
import { clientChat, color } from './app';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
export function broadcast(fastify: FastifyInstance, data: ClientMessage, sender?: string) {
|
||||
fastify.io.fetchSockets().then((sockets) => {
|
||||
for (const socket of sockets) {
|
||||
// Skip sender's own socket
|
||||
if (socket.id === sender) continue;
|
||||
// Get client name from map
|
||||
const clientInfo = clientChat.get(socket.id);
|
||||
if (!clientInfo?.user) {
|
||||
console.log(color.yellow, `Skipping socket ${socket.id} (no user found)`);
|
||||
continue;
|
||||
}
|
||||
// Emit structured JSON object
|
||||
socket.emit('MsgObjectServer', { message: data });
|
||||
// Debug logs
|
||||
// console.log(color.green, `'DEBUG LOG: Broadcast to:', ${data.command} message: ${data.text}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
export type ClientMessage = {
|
||||
command: string
|
||||
destination: string;
|
||||
user: string;
|
||||
text: string;
|
||||
SenderWindowID: string;
|
||||
};
|
||||
|
||||
export type ClientProfil = {
|
||||
command: string,
|
||||
destination: string,
|
||||
type: string,
|
||||
user: string,
|
||||
loginName: string,
|
||||
userID: string,
|
||||
text: string,
|
||||
timestamp: number,
|
||||
SenderWindowID: string,
|
||||
SenderName: string,
|
||||
Sendertext: string,
|
||||
innerHtml?: string,
|
||||
|
||||
};
|
||||
216
src/pong/src/game.ts
Normal file
216
src/pong/src/game.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { UserId } from '@shared/database/mixin/user';
|
||||
|
||||
export class Paddle {
|
||||
public static readonly DEFAULT_SPEED = 20;
|
||||
public static readonly DEFAULT_HEIGHT = 80;
|
||||
public static readonly DEFAULT_WIDTH = 12;
|
||||
|
||||
public height: number = Paddle.DEFAULT_HEIGHT;
|
||||
public width: number = Paddle.DEFAULT_WIDTH;
|
||||
public speed: number = Paddle.DEFAULT_SPEED;
|
||||
|
||||
constructor(
|
||||
// these coordiantes are the topleft corordinates
|
||||
public x: number,
|
||||
public y: number,
|
||||
) { }
|
||||
|
||||
public move(dir: 'up' | 'down') {
|
||||
this.y += (dir === 'up' ? -1 : 1) * this.speed;
|
||||
}
|
||||
|
||||
public clamp(bottom: number, top: number) {
|
||||
if (this.y <= bottom) this.y = bottom;
|
||||
if (this.y + this.height >= top) this.y = top - this.height;
|
||||
}
|
||||
}
|
||||
|
||||
class Ball {
|
||||
public static readonly DEFAULT_SPEED = 1;
|
||||
public static readonly DEFAULT_SIZE = 16;
|
||||
public static readonly DEFAULT_MAX_SPEED = 30;
|
||||
public static readonly DEFAULT_MIN_SPEED = Ball.DEFAULT_SPEED;
|
||||
public static readonly DEFAULT_ACCEL_FACTOR = 1.2;
|
||||
|
||||
public speed: number = Ball.DEFAULT_SPEED;
|
||||
public size: number = Ball.DEFAULT_SIZE;
|
||||
public accel_factor: number = Ball.DEFAULT_ACCEL_FACTOR;
|
||||
|
||||
public max_speed: number = Ball.DEFAULT_MAX_SPEED;
|
||||
public min_speed: number = Ball.DEFAULT_MIN_SPEED;
|
||||
|
||||
constructor(
|
||||
// these coordiantes are the center coordinates
|
||||
public x: number,
|
||||
public y: number,
|
||||
public angle: number,
|
||||
) { }
|
||||
|
||||
public collided(
|
||||
side: 'left' | 'right' | 'top' | 'bottom',
|
||||
walls: { [k in typeof side]: number },
|
||||
) {
|
||||
// this.speed *= this.accel_factor;
|
||||
this.speed = Math.max(
|
||||
Math.min(this.speed, this.max_speed),
|
||||
this.min_speed,
|
||||
);
|
||||
|
||||
let c: 'x' | 'y' = 'x';
|
||||
if (side === 'top' || side === 'bottom') {
|
||||
this.angle = -this.angle;
|
||||
c = 'y';
|
||||
}
|
||||
else {
|
||||
this.angle = -this.angle + Math.PI;
|
||||
c = 'x';
|
||||
}
|
||||
this[c] =
|
||||
walls[side] +
|
||||
this.size * (side === 'right' || side === 'bottom' ? -1 : 1);
|
||||
|
||||
while (this.angle >= Math.PI) {
|
||||
this.angle -= 2 * Math.PI;
|
||||
}
|
||||
while (this.angle < -Math.PI) {
|
||||
this.angle += 2 * Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
public tick() {
|
||||
this.x += Math.cos(this.angle) * this.speed;
|
||||
this.y += Math.sin(this.angle) * this.speed;
|
||||
}
|
||||
}
|
||||
|
||||
function makeAngle(i: number): [number, number, number, number] {
|
||||
return [
|
||||
Math.PI / i,
|
||||
Math.PI / i + Math.PI,
|
||||
-Math.PI / i,
|
||||
-Math.PI / i + Math.PI,
|
||||
];
|
||||
}
|
||||
|
||||
export class Pong {
|
||||
public gameUpdate: NodeJS.Timeout | null = null;
|
||||
|
||||
|
||||
public static readonly BALL_START_ANGLES: number[] = [
|
||||
...makeAngle(4),
|
||||
...makeAngle(6),
|
||||
];
|
||||
|
||||
public ballAngleIdx: number = 0;
|
||||
|
||||
public static readonly GAME_WIDTH: number = 800;
|
||||
public static readonly GAME_HEIGHT: number = 450;
|
||||
|
||||
public static readonly PADDLE_OFFSET: number = 40;
|
||||
|
||||
public leftPaddle: Paddle = new Paddle(
|
||||
Pong.PADDLE_OFFSET,
|
||||
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
|
||||
);
|
||||
public rightPaddle: Paddle = new Paddle(
|
||||
Pong.GAME_WIDTH - Pong.PADDLE_OFFSET - Paddle.DEFAULT_WIDTH,
|
||||
(Pong.GAME_HEIGHT - Paddle.DEFAULT_HEIGHT) / 2,
|
||||
);
|
||||
public ball: Ball = new Ball(Pong.GAME_WIDTH / 2, Pong.GAME_HEIGHT / 2, Pong.BALL_START_ANGLES[this.ballAngleIdx++]);
|
||||
|
||||
public score: [number, number] = [0, 0];
|
||||
|
||||
constructor(
|
||||
public userLeft: UserId,
|
||||
public userRight: UserId,
|
||||
) {
|
||||
}
|
||||
|
||||
public tick() {
|
||||
if (this.paddleCollision(this.leftPaddle, 'left')) {
|
||||
this.ball.collided('left', {
|
||||
left: this.leftPaddle.x + this.leftPaddle.width,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.paddleCollision(this.rightPaddle, 'right')) {
|
||||
this.ball.collided('right', {
|
||||
right: this.rightPaddle.x,
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const wallCollision = this.boxCollision();
|
||||
if (wallCollision === 'top' || wallCollision === 'bottom') {
|
||||
this.ball.collided(wallCollision, {
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: Pong.GAME_HEIGHT,
|
||||
right: Pong.GAME_WIDTH,
|
||||
});
|
||||
}
|
||||
else if (wallCollision !== null) {
|
||||
const idx = wallCollision === 'left' ? 1 : 0;
|
||||
this.score[idx] += 1;
|
||||
this.ball = new Ball(
|
||||
Pong.GAME_WIDTH / 2,
|
||||
Pong.GAME_HEIGHT / 2,
|
||||
Pong.BALL_START_ANGLES[this.ballAngleIdx++],
|
||||
);
|
||||
this.ballAngleIdx %= Pong.BALL_START_ANGLES.length;
|
||||
}
|
||||
this.ball.tick();
|
||||
}
|
||||
|
||||
// This function will return which side the ball collided, if any
|
||||
private boxCollision(): 'top' | 'bottom' | 'left' | 'right' | null {
|
||||
if (this.ball.y - this.ball.size <= 0) return 'top';
|
||||
if (this.ball.y + this.ball.size >= Pong.GAME_HEIGHT) return 'bottom';
|
||||
if (this.ball.x - this.ball.size <= 0) return 'left';
|
||||
if (this.ball.x + this.ball.size >= Pong.GAME_WIDTH) return 'right';
|
||||
return null;
|
||||
}
|
||||
|
||||
private paddleCollision(paddle: Paddle, side: 'left' | 'right'): boolean {
|
||||
// now we check only if the ball is near enought in the y axis to permform the collision
|
||||
if (!(
|
||||
// check if ball is bellow the top of the paddle
|
||||
paddle.y - this.ball.size < this.ball.y &&
|
||||
// check if ball is above the bottom of the paddle
|
||||
this.ball.y < paddle.y + paddle.height + this.ball.size)) return false;
|
||||
|
||||
// so we know that the y is close enougth to be a bit, so we check the X. are we closer than the ball size ? if yes -> hit
|
||||
if (
|
||||
// check if the paddle.x is at most ball.size away from the center of the ball => we have a hit houston
|
||||
// call he pentagon, 9 11
|
||||
Math.abs(
|
||||
paddle.x + paddle.width * (side === 'left' ? 1 : 0)
|
||||
- this.ball.x)
|
||||
< this.ball.size
|
||||
) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public checkWinner(): 'left' | 'right' | null {
|
||||
if (this.score[0] >= 5) return 'left';
|
||||
if (this.score[1] >= 5) return 'right';
|
||||
return null;
|
||||
}
|
||||
|
||||
public movePaddle(user: UserId, dir: 'up' | 'down') {
|
||||
const paddle =
|
||||
user === this.userLeft
|
||||
? this.leftPaddle
|
||||
: user == this.userRight
|
||||
? this.rightPaddle
|
||||
: null;
|
||||
if (paddle === null) return;
|
||||
paddle.move(dir);
|
||||
paddle.clamp(0, Pong.GAME_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { JwtType } from '@shared/auth';
|
||||
import { UserId } from '@shared/database/mixin/user';
|
||||
import { isNullish } from '@shared/utils';
|
||||
import type {
|
||||
FastifyInstance,
|
||||
FastifyPluginAsync,
|
||||
|
|
@ -11,6 +15,21 @@ const F: (
|
|||
) => Omit<FastifyInstance, 'io'> & { io: Server } = (f) =>
|
||||
f as Omit<FastifyInstance, 'io'> & { io: Server };
|
||||
|
||||
function authenticateToken(
|
||||
fastify: FastifyInstance,
|
||||
token: string,
|
||||
): { id: UserId; name: string; guest: boolean } {
|
||||
const tok = fastify.jwt.verify<JwtType>(token);
|
||||
if (tok.kind != 'auth') {
|
||||
throw new Error('Token isn\'t correct type');
|
||||
}
|
||||
const user = fastify.db.getUser(tok.who);
|
||||
if (isNullish(user)) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return { id: user.id, name: user.name, guest: user.guest };
|
||||
}
|
||||
|
||||
const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
||||
function defaultPreClose(done: HookHandlerDoneFunction) {
|
||||
F(fastify).io.local.disconnectSockets(true);
|
||||
|
|
@ -20,6 +39,36 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
|||
'io',
|
||||
new Server(fastify.server, { path: '/api/pong/socket.io' }),
|
||||
);
|
||||
F(fastify).io.use((socket, next) => {
|
||||
const cookieHeader = socket.request.headers.cookie;
|
||||
|
||||
if (!cookieHeader) {
|
||||
throw new Error('Missing token cookie');
|
||||
}
|
||||
|
||||
const cookies = Object.fromEntries(
|
||||
cookieHeader.split(';').map((c) => {
|
||||
const [k, v] = c.trim().split('=');
|
||||
return [k, v];
|
||||
}),
|
||||
);
|
||||
|
||||
if (!cookies.token) {
|
||||
throw new Error('Missing token cookie');
|
||||
}
|
||||
try {
|
||||
socket.authUser = authenticateToken(fastify, cookies.token);
|
||||
next();
|
||||
}
|
||||
catch (e: any) {
|
||||
next({
|
||||
name: 'Unauthorized',
|
||||
message: e.message,
|
||||
data: { status: 401 },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fastify.addHook('preClose', defaultPreClose);
|
||||
fastify.addHook('onClose', (instance: FastifyInstance, done) => {
|
||||
F(instance).io.close();
|
||||
|
|
@ -28,4 +77,3 @@ const fastifySocketIO: FastifyPluginAsync = fp(async (fastify) => {
|
|||
});
|
||||
|
||||
export default fastifySocketIO;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { Static, Type } from 'typebox';
|
||||
import { broadcast } from '../broadcast';
|
||||
|
||||
export const PongReq = Type.Object({
|
||||
message: Type.String(),
|
||||
|
|
@ -19,7 +18,6 @@ const route: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||
config: { requireAuth: false },
|
||||
},
|
||||
async function(req, res) {
|
||||
broadcast(this, { command: '', destination: 'system-info', user: 'CMwaLeSever!!', text: req.body.message, SenderWindowID: 'server' });
|
||||
void res;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
import type { ClientProfil } from './chat_types';
|
||||
import { clientChat, color } from './app';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
|
||||
/**
|
||||
* function looks for the user online in the chat
|
||||
* and sends emit to invite - format HTML to make clickable
|
||||
* message appears in chat window text area
|
||||
* @param fastify
|
||||
* @param innerHtml
|
||||
* @param profil
|
||||
*/
|
||||
|
||||
export function sendInvite(fastify: FastifyInstance, innerHtml: string, profil: ClientProfil) {
|
||||
fastify.io.fetchSockets().then((sockets) => {
|
||||
let targetSocket;
|
||||
for (const socket of sockets) {
|
||||
const clientInfo: string = clientChat.get(socket.id)?.user || '';
|
||||
if (clientInfo === profil.user) {
|
||||
console.log(color.yellow, 'DEBUG LOG: user online found', profil.user);
|
||||
targetSocket = socket || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
profil.innerHtml = innerHtml ?? '';
|
||||
if (targetSocket) {
|
||||
targetSocket.emit('inviteGame', profil);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export function setGameLink(link: string): string {
|
||||
if (!link) {
|
||||
link = '<a href=\'https://google.com\' style=\'color: blue; text-decoration: underline; cursor: pointer;\'>Click me</a>';
|
||||
}
|
||||
return link;
|
||||
};
|
||||
47
src/pong/src/socket.ts
Normal file
47
src/pong/src/socket.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Socket } from 'socket.io';
|
||||
|
||||
export type UpdateInfo = {
|
||||
inQueue: number,
|
||||
totalUser: number,
|
||||
}
|
||||
|
||||
export type PaddleData = {
|
||||
x: number,
|
||||
y: number,
|
||||
|
||||
width: number,
|
||||
height: number,
|
||||
};
|
||||
|
||||
export type GameUpdate = {
|
||||
gameId: string;
|
||||
|
||||
left: { id: string, paddle: PaddleData, score: number };
|
||||
right: { id: string, paddle: PaddleData, score: number };
|
||||
|
||||
ball: { x: number, y: number, size: number };
|
||||
}
|
||||
|
||||
export type GameMove = {
|
||||
move: 'up' | 'down' | null,
|
||||
}
|
||||
|
||||
export interface ClientToServer {
|
||||
enqueue: () => void;
|
||||
dequeue: () => void;
|
||||
debugInfo: () => void;
|
||||
gameMove: (up: GameMove) => void;
|
||||
connectedToGame: (gameId: string) => void;
|
||||
};
|
||||
|
||||
export interface ServerToClient {
|
||||
forceDisconnect: (reason: string) => void;
|
||||
queueEvent: (msg: 'registered' | 'unregistered') => void;
|
||||
updateInformation: (info: UpdateInfo) => void,
|
||||
newGame: (initState: GameUpdate) => void,
|
||||
gameUpdate: (state: GameUpdate) => void,
|
||||
gameEnd: () => void;
|
||||
};
|
||||
|
||||
export type SSocket = Socket<ClientToServer, ServerToClient>;
|
||||
export type CSocket = Socket<ServerToClient, ClientToServer>;
|
||||
182
src/pong/src/state.ts
Normal file
182
src/pong/src/state.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { UserId } from '@shared/database/mixin/user';
|
||||
import { newUUID } from '@shared/utils/uuid';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { Pong } from './game';
|
||||
import { GameMove, GameUpdate, SSocket } from './socket';
|
||||
import { isNullish } from '@shared/utils';
|
||||
|
||||
type PUser = {
|
||||
id: UserId;
|
||||
currentGame: null | GameId;
|
||||
socket: SSocket,
|
||||
windowId: string,
|
||||
updateInterval: NodeJS.Timeout,
|
||||
};
|
||||
|
||||
type GameId = string & { readonly __brand: unique symbol };
|
||||
|
||||
class StateI {
|
||||
public static readonly UPDATE_INTERVAL_FRAMES: number = 60;
|
||||
|
||||
private users: Map<UserId, PUser> = new Map();
|
||||
private queue: Set<UserId> = new Set();
|
||||
private queueInterval: NodeJS.Timeout;
|
||||
private games: Map<GameId, Pong> = new Map();
|
||||
|
||||
public constructor(private fastify: FastifyInstance) {
|
||||
this.queueInterval = setInterval(() => this.queuerFunction());
|
||||
void this.queueInterval;
|
||||
}
|
||||
|
||||
private static getGameUpdateData(id: GameId, g: Pong): GameUpdate {
|
||||
return {
|
||||
gameId: id,
|
||||
left: { id: g.userLeft, score: g.score[0], paddle: { x: g.leftPaddle.x, y: g.leftPaddle.y, width: g.leftPaddle.width, height: g.leftPaddle.height } },
|
||||
right: { id: g.userRight, score: g.score[1], paddle: { x: g.rightPaddle.x, y: g.rightPaddle.y, width: g.rightPaddle.width, height: g.rightPaddle.height } },
|
||||
ball: { x: g.ball.x, y: g.ball.y, size: g.ball.size },
|
||||
};
|
||||
}
|
||||
|
||||
private queuerFunction(): void {
|
||||
const values = Array.from(this.queue.values());
|
||||
while (values.length >= 2) {
|
||||
const id1 = values.pop();
|
||||
const id2 = values.pop();
|
||||
|
||||
if (isNullish(id1) || isNullish(id2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const u1 = this.users.get(id1);
|
||||
const u2 = this.users.get(id2);
|
||||
|
||||
if (isNullish(u1) || isNullish(u2)) {
|
||||
continue;
|
||||
}
|
||||
this.queue.delete(id1);
|
||||
this.queue.delete(id2);
|
||||
|
||||
const gameId = newUUID() as unknown as GameId;
|
||||
const g = new Pong(u1.id, u2.id);
|
||||
const iState: GameUpdate = StateI.getGameUpdateData(gameId, g);
|
||||
|
||||
u1.socket.emit('newGame', iState);
|
||||
u2.socket.emit('newGame', iState);
|
||||
this.games.set(gameId, g);
|
||||
|
||||
u1.currentGame = gameId;
|
||||
u2.currentGame = gameId;
|
||||
|
||||
g.gameUpdate = setInterval(() => {
|
||||
g.tick();
|
||||
this.gameUpdate(gameId, u1.socket);
|
||||
this.gameUpdate(gameId, u2.socket);
|
||||
if (g.checkWinner() !== null) { this.cleanupGame(gameId, g); }
|
||||
}, 1000 / StateI.UPDATE_INTERVAL_FRAMES);
|
||||
}
|
||||
}
|
||||
|
||||
private gameUpdate(id: GameId, sock: SSocket) {
|
||||
// does the game we want to update the client exists ?
|
||||
if (!this.games.has(id)) return;
|
||||
// is the client someone we know ?
|
||||
if (!this.users.has(sock.authUser.id)) return;
|
||||
// is the client associated with that game ?
|
||||
if (this.users.get(sock.authUser.id)!.currentGame !== id) return;
|
||||
sock.emit('gameUpdate', StateI.getGameUpdateData(id, this.games.get(id)!));
|
||||
}
|
||||
|
||||
private gameMove(socket: SSocket, u: GameMove) {
|
||||
// do we know this user ?
|
||||
if (!this.users.has(socket.authUser.id)) return;
|
||||
const user = this.users.get(socket.authUser.id)!;
|
||||
// does the user have a game and do we know such game ?
|
||||
if (user.currentGame === null || !this.games.has(user.currentGame)) return;
|
||||
const game = this.games.get(user.currentGame)!;
|
||||
|
||||
if (u.move !== null) { game.movePaddle(user.id, u.move); }
|
||||
}
|
||||
|
||||
|
||||
public registerUser(socket: SSocket): void {
|
||||
this.fastify.log.info('Registering new user');
|
||||
if (this.users.has(socket.authUser.id)) {
|
||||
socket.emit('forceDisconnect', 'Already Connected');
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
this.users.set(socket.authUser.id, {
|
||||
socket,
|
||||
id: socket.authUser.id,
|
||||
windowId: socket.id,
|
||||
updateInterval: setInterval(() => this.updateClient(socket), 3000),
|
||||
currentGame: null,
|
||||
});
|
||||
this.fastify.log.info('Registered new user');
|
||||
|
||||
socket.on('disconnect', () => this.cleanupUser(socket));
|
||||
socket.on('enqueue', () => this.enqueueUser(socket));
|
||||
socket.on('dequeue', () => this.dequeueUser(socket));
|
||||
|
||||
socket.on('gameMove', (e) => this.gameMove(socket, e));
|
||||
}
|
||||
|
||||
private updateClient(socket: SSocket): void {
|
||||
socket.emit('updateInformation', {
|
||||
inQueue: this.queue.size,
|
||||
totalUser: this.users.size,
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupUser(socket: SSocket): void {
|
||||
if (!this.users.has(socket.authUser.id)) return;
|
||||
|
||||
clearInterval(this.users.get(socket.authUser.id)?.updateInterval);
|
||||
this.users.delete(socket.authUser.id);
|
||||
this.queue.delete(socket.authUser.id);
|
||||
}
|
||||
|
||||
private cleanupGame(gameId: GameId, game: Pong): void {
|
||||
clearInterval(game.gameUpdate ?? undefined);
|
||||
this.games.delete(gameId);
|
||||
let player: PUser | undefined = undefined;
|
||||
if ((player = this.users.get(game.userLeft)) !== undefined) {
|
||||
player.currentGame = null;
|
||||
player.socket.emit('gameEnd');
|
||||
}
|
||||
if ((player = this.users.get(game.userRight)) !== undefined) {
|
||||
player.currentGame = null;
|
||||
player.socket.emit('gameEnd');
|
||||
}
|
||||
// do something here with the game result before deleting the game at the end
|
||||
}
|
||||
|
||||
|
||||
private enqueueUser(socket: SSocket): void {
|
||||
if (!this.users.has(socket.authUser.id)) return;
|
||||
|
||||
if (this.queue.has(socket.authUser.id)) return;
|
||||
|
||||
if (this.users.get(socket.authUser.id)?.currentGame !== null) return;
|
||||
|
||||
this.queue.add(socket.authUser.id);
|
||||
socket.emit('queueEvent', 'registered');
|
||||
}
|
||||
|
||||
private dequeueUser(socket: SSocket): void {
|
||||
if (!this.users.has(socket.authUser.id)) return;
|
||||
|
||||
if (!this.queue.has(socket.authUser.id)) return;
|
||||
|
||||
this.queue.delete(socket.authUser.id);
|
||||
socket.emit('queueEvent', 'unregistered');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export let State: StateI = undefined as unknown as StateI;
|
||||
|
||||
export function newState(f: FastifyInstance) {
|
||||
State = new StateI(f);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue