added ball + bounces; started on queu system for user to join game

This commit is contained in:
bgoulard 2025-12-14 16:12:19 +01:00 committed by Maix0
parent 7571927097
commit 8a6394786c
7 changed files with 258 additions and 182 deletions

View file

@ -1,25 +1,26 @@
<div class="displaybox"> <div class="displaybox">
<div id="mainbox" class="mainboxDisplay"> <div id="mainbox" class="mainboxDisplay">
<button id="b-whoami" class="btn-style absolute top-4 left-6">Who am i</button> <button id="b-whoami" class="btn-style absolute top-4 left-6">Who am i</button>
<h1 class="text-3xl font-bold text-gray-800"> <br>
Pong Box<span id="t-username"></span> <button id="b-joinQueu" class="btn-style absolute top-16 left-6">Queu Up</button>
</h1><br> <br>
<!-- Horizontal Message Box --> <h1 class="text-3xl font-bold text-gray-800">
<div id="system-box" class="system-info">System: connecting ... </div> Pong Box<span id="t-username"></span>
<!-- Pong Box --> </h1><br>
<div class="flex justify-center mt-2"> <!-- Horizontal Message Box -->
<div id="pongspace" class="flex flex-col"> <div id="system-box" class="system-info">System: connecting ... </div>
<div id="pongbox" class="pongbox-style"> <!-- Pong Box -->
<div class="pong-field"> <div class="flex justify-center mt-2">
<div id="batleft" class="pong-batleft bg-amber-400 top-0"></div> <div id="pongspace" class="flex flex-col">
<div class="pong-center-line"></div> <div id="pongbox" class="pongbox-style">
<div id="batright" class="pong-batright bg-amber-400 top-0"></div> <div class="pong-field">
</div> <div id="batleft" class="pong-batleft bg-amber-400 top-0"></div>
</div> <div class="pong-center-line"></div>
</div> <div id="batright" class="pong-batright bg-amber-400 top-0"></div>
</div> <div id="ball" class="w-16 h-16 rounded-full border-4 bg-white border-gray-400"></div>
</div>
</div>
</div>
</div>
</div>
</div> </div>

View file

@ -7,6 +7,7 @@ import io, { Socket } from 'socket.io-client';
import { addPongMessage } from './addPongMessage'; import { addPongMessage } from './addPongMessage';
import { isLoggedIn } from './isLoggedIn'; import { isLoggedIn } from './isLoggedIn';
import type { ClientMessage, ClientProfil } from './types_front'; import type { ClientMessage, ClientProfil } from './types_front';
import { isNullish } from "@app/utils";
export const color = { export const color = {
red: 'color: red;', red: 'color: red;',
@ -21,7 +22,8 @@ const machineHostName = window.location.hostname;
console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow); console.log('connect to login at %chttps://' + machineHostName + ':8888/app/login',color.yellow);
export let __socket: Socket | undefined = undefined; export let __socket: Socket | undefined = undefined;
document.addEventListener('ft:pageChange', () => {
document.addEventListener('ft:pageChange', () => { // dont regen socket on page change from forward/backward navigation arrows
if (__socket !== undefined) if (__socket !== undefined)
__socket.close(); __socket.close();
__socket = undefined; __socket = undefined;
@ -30,9 +32,8 @@ document.addEventListener('ft:pageChange', () => {
export function getSocket(): Socket { export function getSocket(): Socket {
let addressHost = `wss://${machineHostName}:8888`; let addressHost = `wss://${machineHostName}:8888`;
// let addressHost = `wss://localhost:8888`;
if (__socket === undefined)
if (__socket === undefined)
__socket = io(addressHost, { __socket = io(addressHost, {
path: "/api/pong/socket.io/", path: "/api/pong/socket.io/",
secure: false, secure: false,
@ -41,19 +42,16 @@ export function getSocket(): Socket {
return __socket; return __socket;
}; };
function waitSocketConnected(socket: Socket): Promise<void> { function waitSocketConnected(socket: Socket): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
if (socket.connected) return resolve(); if (socket.connected) return resolve();
socket.on("connect", () => resolve()); socket.on("connect", () => resolve());
}); });
}; };
async function whoami(socket: Socket) { async function whoami(socket: Socket) {
try { try {
const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement; const chatWindow = document.getElementById("t-chatbox") as HTMLDivElement;
const loggedIn = isLoggedIn();
const res = await client.guestLogin(); const res = await client.guestLogin();
switch (res.kind) { switch (res.kind) {
@ -80,148 +78,172 @@ async function whoami(socket: Socket) {
} }
}; };
function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
function handleChat(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
let socket = getSocket(); let socket = getSocket();
/**
* on connection plays this part
*/
socket.on("connect", async () => { socket.on("connect", async () => {
const systemWindow = document.getElementById('system-box') as HTMLDivElement; const systemWindow = document.getElementById('system-box') as HTMLDivElement;
await waitSocketConnected(socket); await waitSocketConnected(socket);
console.log("I AM Connected to the server:", socket.id); console.log("I AM Connected to the server:", socket.id);
const message = { // const message = {
command: "", // command: "",
destination: 'system-info', // destination: 'system-info',
type: "chat", // type: "chat",
user: getUser()?.name, // user: getUser()?.name,
token: document.cookie ?? "", // token: document.cookie ?? "",
text: " has Just ARRIVED in the chat", // text: " has Just ARRIVED in the chat",
timestamp: Date.now(), // timestamp: Date.now(),
SenderWindowID: socket.id, // SenderWindowID: socket.id,
}; // };
socket.emit('message', JSON.stringify(message)); // socket.emit('message', JSON.stringify(message));
const messageElement = document.createElement("div"); const messageElement = document.createElement("div");
messageElement.textContent = `${message.user}: is connected au server`; // messageElement.textContent = `${message.user}: is connected au server`;
systemWindow.appendChild(messageElement); messageElement.textContent = `${getUser()?.name ?? "unkown user"}: is connected au server`;
systemWindow.appendChild(messageElement);
systemWindow.scrollTop = systemWindow.scrollHeight; 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> = {}; const keys: Record<string, boolean> = {};
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
keys[e.key.toLowerCase()] = true; keys[e.key.toLowerCase()] = true;
}); });
document.addEventListener("keyup", (e) => { document.addEventListener("keyup", (e) => {
keys[e.key.toLowerCase()] = false; keys[e.key.toLowerCase()] = false;
}); });
setInterval(() => { // key sender
setInterval(() => { if ((keys['w'] || keys['s']) && !(keys['w'] && keys['s'])) { // exclusive or to filter requests
if (keys['w']) { if (keys['w']) {
socket.emit("batmove_Left", "up"); socket.emit("batmove_Left", "up");
console.log('north key pressed - 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['s']) { if ((keys['p'] || keys['l']) && !(keys['p'] && keys['l'])) { // exclusive or to filter requests
socket.emit("batmove_Left", "down"); if (keys['p']) {
console.log('south key pressed - emit batmove_Left down'); socket.emit("batmove_Right", "up");
} console.log('north key pressed - emit batmove_Right up');
if (keys['p']) { }
socket.emit("batmove_Right", "up"); if (keys['l']) {
console.log('north key pressed - emit batmove_Right up'); socket.emit("batmove_Right", "down");
} console.log('south key pressed - emit batmove_Right down');
if (keys['l']) { }
socket.emit("batmove_Right", "down");
console.log('south key pressed - emit batmove_Right down');
} }
}, 16); }, 16);
// Pong Objects updators
socket.on("batLeft_update", (y: number) => {
// Listen for Left bat updates console.log('batLeft_update received y: ', y);
socket.on("batLeft_update", (y: number) => {
console.log('batLeft_update received y: ', y);
const bat = document.getElementById("batleft") as HTMLDivElement | null; const bat = document.getElementById("batleft") as HTMLDivElement | null;
if (!bat) { if (!bat) {
console.error("FATAL ERROR: Bat element with ID 'bat-left' not found. Check HTML."); console.error("FATAL ERROR: Bat element with ID 'bat-left' not found. Check HTML.");
return ; return ;
} }
if (typeof y === 'number' && !isNaN(y)) { if (typeof y === 'number' && !isNaN(y)) {
bat.style.transform = `translateY(${y}px)`; bat.style.transform = `translateY(${y}px)`;
} else { } else {
console.warn(`Received invalid Y value: ${y}`); console.warn(`Received invalid Y value: ${y}`);
} }
}); });
socket.on("batRight_update", (y: number) => {
// Listen for Right bat updates console.log('batRight_update received y: ', y);
socket.on("batRight_update", (y: number) => {
console.log('batRight_update received y: ', y);
const bat = document.getElementById("batright") as HTMLDivElement | null; const bat = document.getElementById("batright") as HTMLDivElement | null;
if (!bat) { if (!bat) {
console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML."); console.error("FATAL ERROR: Bat element with ID 'bat-Right' not found. Check HTML.");
return ; return ;
} }
if (typeof y === 'number' && !isNaN(y)) { if (typeof y === 'number' && !isNaN(y)) {
bat.style.transform = `translateY(${y}px)`; bat.style.transform = `translateY(${y}px)`;
} else { } else {
console.warn(`Received invalid Y value: ${y}`); console.warn(`Received invalid Y value: ${y}`);
} }
}); });
socket.on("ballPos_update", (x:number, y : number) => {
console.log('ballPos_update recieved: ', x, ' / ', y);
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 ;
}
socket.once('welcome', (data) => { if (typeof y !== 'number' || isNaN(y) || typeof x !== 'number' || isNaN(x)) {
console.log('%cWelcome PONG PAGE', color.yellow ); console.warn(`Received invalid X/Y value: ${x} / ${y}`);
addPongMessage('socket.once \'Welcome\' called') 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"
// Listen for messages from the server "MsgObjectServer"
socket.on("MsgObjectServer", (data: { message: ClientMessage}) => { socket.on("MsgObjectServer", (data: { message: ClientMessage}) => {
// Display the message in the chat window // Display the message in the chat window
const systemWindow = document.getElementById('system-box') as HTMLDivElement; const systemWindow = document.getElementById('system-box') as HTMLDivElement;
const MAX_SYSTEM_MESSAGES = 10; const MAX_SYSTEM_MESSAGES = 10;
if (systemWindow && data.message.destination === "system-info") { if (systemWindow && data.message.destination === "system-info") {
const messageElement = document.createElement("div"); const messageElement = document.createElement("div");
messageElement.textContent = `${data.message.user}: ${data.message.text}`; messageElement.textContent = `${data.message.user}: ${data.message.text}`;
systemWindow.appendChild(messageElement); systemWindow.appendChild(messageElement);
// keep only last 10 // keep only last 10
while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) { while (systemWindow.children.length > MAX_SYSTEM_MESSAGES) {
systemWindow.removeChild(systemWindow.firstChild!); systemWindow.removeChild(systemWindow.firstChild!);
} }
systemWindow.scrollTop = systemWindow.scrollHeight; systemWindow.scrollTop = systemWindow.scrollHeight;
} }
console.log("Getuser():", getUser()); console.log("Getuser():", getUser());
}); });
setTitle('Pong Game Page');
setTitle('Pong Page');
return { return {
html: authHtml, postInsert: async (app) => { html: authHtml, postInsert: async (app) => {
const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement; const bwhoami = document.getElementById('b-whoami') as HTMLButtonElement;
const bqueu = document.getElementById('b-joinQueu') as HTMLButtonElement;
bwhoami?.addEventListener('click', async () => { bwhoami?.addEventListener('click', async () => {
whoami(socket); whoami(socket);
}); });
bqueu?.addEventListener('click', async () => {
joinQueu(socket);
});
} }
} }
}; };
addRoute('/pong', handleChat, { bypass_auth: true }); addRoute('/pong', pongClient, { bypass_auth: true });

View file

@ -6,7 +6,6 @@ export type ClientMessage = {
SenderWindowID: string; SenderWindowID: string;
}; };
export type ClientProfil = { export type ClientProfil = {
command: string, command: string,
destination: string, destination: string,

View file

@ -100,7 +100,6 @@
justify-center; justify-center;
} }
.text-style { .text-style {
@apply @apply
text-black text-black
@ -123,7 +122,6 @@
@apply absolute right-4 w-[12px] h-[80px] top-[0px]; @apply absolute right-4 w-[12px] h-[80px] top-[0px];
} }
.pong-center-line { .pong-center-line {
@apply @apply
absolute absolute

View file

@ -30,7 +30,7 @@
"typebox": "^1.0.62" "typebox": "^1.0.62"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.2", "@types/node": "^22.19.3",
"rollup-plugin-node-externals": "^8.1.2", "rollup-plugin-node-externals": "^8.1.2",
"vite": "^7.2.7", "vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"

View file

@ -10,6 +10,9 @@ import { broadcast } from './broadcast';
import type { ClientProfil, ClientMessage } from './chat_types'; import type { ClientProfil, ClientMessage } from './chat_types';
import { sendInvite } from './sendInvite'; import { sendInvite } from './sendInvite';
import { setGameLink } from './setGameLink'; import { setGameLink } from './setGameLink';
import { emit } from 'process';
import { Record } from 'typebox/type';
import { UserId } from '@shared/database/mixin/user';
@ -80,7 +83,9 @@ declare module 'fastify' {
batmove_Right: (direction: "up" | "down") => void; batmove_Right: (direction: "up" | "down") => void;
batLeft_update: (y:number) => void; batLeft_update: (y:number) => void;
batRight_update: (y:number) => void; batRight_update: (y:number) => void;
ballPos_update: (x:number, y:number) => void;
MsgObjectServer: (data: { message: ClientMessage }) => void; MsgObjectServer: (data: { message: ClientMessage }) => void;
queuJoin: (userID : UserId) => void;
}>; }>;
} }
} }
@ -96,66 +101,116 @@ async function onReady(fastify: FastifyInstance) {
} }
const SPEED = 20; // bat speed const SPEED = 20; // bat speed
const BOTTOM_EDGE = 370; // bottom edge of the field;
const TOP_EDGE = 0; // top edge of the field const TOP_EDGE = 0; // top edge of the field
const BOTTOM_EDGE = 450; // bottom edge of the field;
const LEFT_EDGE = 0;
const RIGHT_EDGE = 800;
const MAX_PADDLE_Y = 370; // BOTTOM_EDGE - padle_height
const START_POS_Y = 178; // bat y in the middle of the field const START_POS_Y = 178; // bat y in the middle of the field
let batLeft = START_POS_Y; //shared bat position
let batRight = START_POS_Y; //shared bat position const START_BALLX = 364; //(RIGHT_EDGE / 2) - (ballradius*2 + ballBorder);
const START_BALLY = 189; //(BOTTOM_EDGE / 2) - (ballradius*2 + ballBorder);
const ACCELERATION_FACTOR = 1.15;
let batLeft = START_POS_Y; //shared start bat position
let batRight = START_POS_Y; //shared start bat position
let ballPosX = START_BALLX;
let ballPosY = START_BALLY;
let ballSpeedX = 1;
let ballSpeedY = 1;
let games : Record<UserId, string> = {}; // uuid, game uid - if not in game empty string
fastify.io.on('connection', (socket: Socket) => { fastify.io.on('connection', (socket: Socket) => {
socket.emit("batLeft_update", batLeft); socket.emit("batLeft_update", batLeft);
socket.emit("batRight_update", batRight); socket.emit("batRight_update", batRight);
socket.emit("ballPos_update", ballPosX, ballPosY);
socket.on('batmove_Left', (direction: "up" | "down") => { // GAME
if (direction === "up") { // paddle handling
batLeft -= SPEED; socket.on('batmove_Left', (direction: "up" | "down") => {
console.log('w pressed UP'); if (direction === "up") {
} batLeft -= SPEED;
if (direction === "down") { console.log('w pressed UP');
console.log('s pressed DOWN'); }
if (direction === "down") {
batLeft += SPEED; console.log('s pressed DOWN');
}
// position of bat leftplokoplpl
batLeft = Math.max(TOP_EDGE, Math.min(BOTTOM_EDGE, batLeft));
console.log('batLeft_update is called y:',batLeft);
socket.emit("batLeft_update", batLeft);
});
socket.on('batmove_Right', (direction: "up" | "down") => {
if (direction === "up") {
batRight -= SPEED;
console.log('p pressed UP');
}
if (direction === "down") {
console.log('l pressed DOWN');
batRight += SPEED;
}
// position of bat left
batRight = Math.max(TOP_EDGE, Math.min(BOTTOM_EDGE, batRight));
console.log('batRight_update is called y:',batRight);
socket.emit("batRight_update", batRight);
});
batLeft += SPEED;
}
// position of bat leftplokoplpl
batLeft = Math.max(TOP_EDGE, Math.min(MAX_PADDLE_Y, batLeft));
console.log('batLeft_update is called y:',batLeft);
socket.emit("batLeft_update", batLeft);
});
socket.on('batmove_Right', (direction: "up" | "down") => {
if (direction === "up") {
batRight -= SPEED;
console.log('p pressed UP');
}
if (direction === "down") {
console.log('l pressed DOWN');
batRight += SPEED;
}
// position of bat left
batRight = Math.max(TOP_EDGE, Math.min(MAX_PADDLE_Y, batRight));
console.log('batRight_update is called y:',batRight);
socket.emit("batRight_update", batRight);
});
// ball handling:
// TODO 1: l/r bat hit
// TODO 2: l/r wall hit : score
setInterval(async () => {
const new_ballPosX = ballPosX + ballSpeedX;
const new_ballPosY = ballPosY + ballSpeedY;
if (new_ballPosX < 0 || new_ballPosX + 36*2 > RIGHT_EDGE) {
ballSpeedX *= -1;
ballSpeedX *= ACCELERATION_FACTOR;
ballSpeedY *= ACCELERATION_FACTOR;
// TODO: score point + ball reset + spd reset
}
if (new_ballPosY < 0 || new_ballPosY + 36*2 > BOTTOM_EDGE) {
ballSpeedY *= -1;
ballSpeedX *= ACCELERATION_FACTOR;
ballSpeedY *= ACCELERATION_FACTOR;
}
ballSpeedX = Math.max(-10, Math.min(ballSpeedX, 10));
ballSpeedY = Math.max(-10, Math.min(ballSpeedY, 10));
ballPosX += ballSpeedX;
ballPosY += ballSpeedY;
socket.emit("ballPos_update", ballPosX, ballPosY);
}, 1024)
// QUEU HANDL
socket.on('queuJoin', async (uuid: UserId) => {
console.log('queu join recieved for : ', uuid);
if (!games.hasOwnProperty(uuid)) {
console.log("new user in game search queu");
games[uuid] = "";
} else if (games.hasOwnProperty(uuid) && 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) => { socket.on('message', (message: string) => {
const obj: ClientMessage = JSON.parse(message) as ClientMessage; const obj: ClientMessage = JSON.parse(message) as ClientMessage;
clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() }); clientChat.set(socket.id, { user: obj.user, lastSeen: Date.now() });
socket.emit('welcome', {msg: 'Welcome to the chat! : '}); socket.emit('welcome', {msg: 'Welcome to the chat! : '});
broadcast(fastify, obj, obj.SenderWindowID); broadcast(fastify, obj, obj.SenderWindowID);
}); });
socket.on('inviteGame', async (data: string) => { socket.on('inviteGame', async (data: string) => {
const clientName: string = clientChat.get(socket.id)?.user || ''; const clientName: string = clientChat.get(socket.id)?.user || '';
const profilInvite: ClientProfil = JSON.parse(data) || ''; const profilInvite: ClientProfil = JSON.parse(data) || '';
@ -164,5 +219,6 @@ async function onReady(fastify: FastifyInstance) {
sendInvite(fastify, inviteHtml, profilInvite); sendInvite(fastify, inviteHtml, profilInvite);
} }
}); });
}); });
} }