From 0727067b27cc0dfbf62408bb35d4a02b543ca602 Mon Sep 17 00:00:00 2001 From: bgoulard Date: Sun, 4 Jan 2026 15:52:20 +0100 Subject: [PATCH] pong pretty / local rdy in back, need hooking in front --- frontend/src/pages/pong/pong.html | 32 +++--- frontend/src/pages/pong/pong.ts | 169 +++++++++++++++++++++--------- frontend/src/pages/pong/socket.ts | 1 + src/pong/src/game.ts | 25 +++-- 4 files changed, 150 insertions(+), 77 deletions(-) diff --git a/frontend/src/pages/pong/pong.html b/frontend/src/pages/pong/pong.html index 7b896af..005a44e 100644 --- a/frontend/src/pages/pong/pong.html +++ b/frontend/src/pages/pong/pong.html @@ -1,35 +1,35 @@
- -
- +

Pong Box


- + + + + + + + +
System: connecting ...
-

0:0

+
+

+

0:0

+

+
-
+
-
+
diff --git a/frontend/src/pages/pong/pong.ts b/frontend/src/pages/pong/pong.ts index ea7dd2e..6380161 100644 --- a/frontend/src/pages/pong/pong.ts +++ b/frontend/src/pages/pong/pong.ts @@ -1,12 +1,10 @@ -import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; +import { addRoute, navigateTo, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; import authHtml from './pong.html?raw'; 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) -// +import { getUser } from "@app/auth"; +import { isNullish } from "@app/utils"; // get the name of the machine used to connect declare module 'ft_state' { @@ -15,8 +13,15 @@ declare module 'ft_state' { } } + // GameRdyDown = "Ready Up?" + // GameRdyUp = "Ready down?" +enum QueueState { + InQueu = "In Queue", + InGame = "In Game", + Iddle = "Queue Up", +}; + document.addEventListener("ft:pageChange", (newUrl) => { - // we are still on a pong page => keep the socket around ! if (newUrl.detail.startsWith('/app/pong') || newUrl.detail.startsWith('/pong')) return; if (window.__state.pongSock !== undefined) window.__state.pongSock.close(); window.__state.pongSock = undefined; @@ -30,73 +35,83 @@ export function getSocket(): CSocket { function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn { setTitle('Pong Game Page'); + // MAYBE: "queue up" btn : adds timer to page for duration of queue + // TODO: "local play" btn : emit "local new game" evt to server; play game on single computer (maybe need to change keys-handling logic) + return { - html: authHtml, postInsert: async (app) => { - const checkbox = document.getElementById("modeToggle") as HTMLInputElement; - const label = document.getElementById("toggleLabel") as HTMLSpanElement; - const track = document.getElementById("toggleTrack") as HTMLDivElement; - const knob = document.getElementById("toggleKnob") as HTMLSpanElement; - - checkbox.addEventListener("change", () => { // web vs local - if (checkbox.checked) { - label.textContent = "Web"; - track.classList.replace("bg-gray-300", "bg-blue-600"); - knob.classList.add("translate-x-7"); - } else { - label.textContent = "Local"; - track.classList.replace("bg-blue-600", "bg-gray-300"); - knob.classList.remove("translate-x-7"); - } - }); - - + const DEFAULT_COLOR = "white"; + const SELF_COLOR = "red"; + const user = getUser(); + let currentGame: GameUpdate | null = null; const batLeft = document.querySelector("#batleft"); const batRight = document.querySelector("#batright"); const ball = document.querySelector("#ball"); const score = document.querySelector("#score-board"); - if (!batLeft || !batRight || !ball || !score) - return showError('fatal error'); + const playerL = document.querySelector('#player-left'); + const playerR = document.querySelector('#player-right'); + const queueBtn = document.querySelector("#QueueBtn"); let socket = getSocket(); - let currentGame: GameUpdate | null = null; + if (isNullish(user)) { // if no user (no loggin / other) : GTFO + navigateTo("/app"); + return ; + } + if (!batLeft || !batRight || !ball || !score || !queueBtn || !playerL || !playerR) // sanity check + return showError('fatal error'); + + // --- // keys handler + // --- const keys: Record = {}; - document.addEventListener("keydown", (e) => { - keys[e.key.toLowerCase()] = true; - }); - document.addEventListener("keyup", (e) => { - keys[e.key.toLowerCase()] = false; - }); + document.addEventListener("keydown", (e) => {keys[e.key.toLowerCase()] = true;}); + document.addEventListener("keyup", (e) => {keys[e.key.toLowerCase()] = false;}); setInterval(() => { // key sender + if (queueBtn.innerText !== QueueState.InGame)//we're in game ? continue | gtfo + return ; if (currentGame === null) return; + let packet: GameMove = { move: null, moveRight: null, } - if ((keys['w'] !== keys['s'])) { - packet.move = keys['w'] ? 'up' : 'down'; - } - if (currentGame.local && (keys['o'] !== keys['l'])) { - packet.moveRight = keys['o'] ? 'up' : 'down'; - } + if (queueBtn.innerText !== QueueState.InGame)//we're in game ? continue | gtfo + return ; + if (currentGame === null) return; + + if ((keys['w'] !== keys['s'])) + packet.move = keys['w'] ? 'up' : 'down'; + if (currentGame.local && (keys['o'] !== keys['l'])) + packet.moveRight = keys['o'] ? 'up' : 'down'; socket.emit('gameMove', packet); }, 1000 / 60); + // --- + // keys end + // --- + + // --- + // position logic (client) + // --- + const DEFAULT_POSITIONS : GameUpdate = { + gameId:"", + ball:{size:16, x:800/2, y:450/2}, + left:{id:"", paddle:{x:40, y:185, width:12, height:80}, score:0}, + right:{id:"", paddle:{x:748, y:185, width:12, height:80}, score:0}, + local:false + }; const render = (state: GameUpdate) => { currentGame = state; - //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`; @@ -106,19 +121,71 @@ function pongClient(_url: string, _args: RouteHandlerParams): RouteHandlerReturn 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('gameEnd', () => currentGame = null); - socket.on('gameUpdate', (state: GameUpdate) => render(state)); - socket.on('newGame', (state: GameUpdate) => render(state)); + // --- + // position logic (client) end + // --- - socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`)); - socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); - //socket.emit('enqueue'); - socket.emit('localGame'); + // --- + // queue evt + // --- + function set_pretty(batU : HTMLDivElement, txtU : HTMLDivElement, txtO : HTMLDivElement, colorYou : string) { + batU.style.backgroundColor = colorYou; + txtU.style.color = colorYou; + txtU.innerText = "you"; + txtO.innerHTML = "The Mechant"; + } + queueBtn.addEventListener("click", ()=>{ + if (queueBtn.innerText !== QueueState.Iddle) { + if (queueBtn.innerText === QueueState.InQueu) { + socket.emit("dequeue"); + queueBtn.innerText = QueueState.Iddle; + } + return ; + } + queueBtn.innerText = QueueState.InQueu; + socket.emit('enqueue'); + }); + socket.on('newGame', (state) => { + render(state); + queueBtn.innerText = QueueState.InGame; + queueBtn.style.color = 'red'; + batLeft.style.backgroundColor = DEFAULT_COLOR; + batRight.style.backgroundColor = DEFAULT_COLOR; + if (state.left.id === user.id) { + set_pretty(batLeft, playerL, playerR, SELF_COLOR); + } else if (state.right.id === user.id) { + set_pretty(batRight, playerR, playerL, SELF_COLOR); + } else + showError("couldn't find your id in game"); + }); // TODO: notif user of new game w "ready up" btn + + socket.on("gameEnd", () => { + queueBtn.innerHTML = QueueState.Iddle; + queueBtn.style.color = 'white'; + render(DEFAULT_POSITIONS); + batLeft.style.backgroundColor = DEFAULT_COLOR; + batRight.style.backgroundColor = DEFAULT_COLOR; + playerR.style.color = ""; + playerL.style.color = ""; + playerR.innerText = ""; + playerL.innerText = ""; + currentGame = null; + }) + // --- + // queue evt end + // --- + render(DEFAULT_POSITIONS); + batLeft.style.backgroundColor = DEFAULT_COLOR; + batRight.style.backgroundColor = DEFAULT_COLOR; + + socket.on('updateInformation', (e) => showInfo(`UpdateInformation: t=${e.totalUser};q=${e.inQueue}`)); // queue info TODO: delete for final product + socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`)); // queue evt can be left in product + showInfo("butter"); + showInfo("butter-toast"); + // socket.emit('localGame'); } } }; diff --git a/frontend/src/pages/pong/socket.ts b/frontend/src/pages/pong/socket.ts index bf6b971..064e7f4 100644 --- a/frontend/src/pages/pong/socket.ts +++ b/frontend/src/pages/pong/socket.ts @@ -29,6 +29,7 @@ export type GameMove = { moveRight: 'up' | 'down' | null, } +// TODO: add new evt such as "local play", "ready-up" see: ./pong.ts export interface ClientToServer { enqueue: () => void; dequeue: () => void; diff --git a/src/pong/src/game.ts b/src/pong/src/game.ts index abb8e6b..6acfdb3 100644 --- a/src/pong/src/game.ts +++ b/src/pong/src/game.ts @@ -26,9 +26,9 @@ export class Paddle { } class Ball { - public static readonly DEFAULT_SPEED = 1; + public static readonly DEFAULT_SPEED = 3; public static readonly DEFAULT_SIZE = 16; - public static readonly DEFAULT_MAX_SPEED = 30; + public static readonly DEFAULT_MAX_SPEED = 15; public static readonly DEFAULT_MIN_SPEED = Ball.DEFAULT_SPEED; public static readonly DEFAULT_ACCEL_FACTOR = 1.2; @@ -49,6 +49,7 @@ class Ball { public collided( side: 'left' | 'right' | 'top' | 'bottom', walls: { [k in typeof side]: number }, + snap: boolean = true, ) { // this.speed *= this.accel_factor; this.speed = Math.max( @@ -65,9 +66,11 @@ class Ball { this.angle = -this.angle + Math.PI; c = 'x'; } - this[c] = - walls[side] + - this.size * (side === 'right' || side === 'bottom' ? -1 : 1); + if (snap){ + this[c] = + walls[side] + + this.size * (side === 'right' || side === 'bottom' ? -1 : 1); + } while (this.angle >= Math.PI) { this.angle -= 2 * Math.PI; @@ -139,8 +142,8 @@ export class Pong { left: this.leftPaddle.x + this.leftPaddle.width, right: 0, top: 0, - bottom: 0, - }); + bottom: 0}, false + ); return; } if (this.paddleCollision(this.rightPaddle, 'right')) { @@ -148,8 +151,7 @@ export class Pong { right: this.rightPaddle.x, left: 0, top: 0, - bottom: 0, - }); + bottom: 0}, false); return; } const wallCollision = this.boxCollision(); @@ -184,6 +186,10 @@ export class Pong { } private paddleCollision(paddle: Paddle, side: 'left' | 'right'): boolean { + if (Math.abs(this.ball.angle) > Math.PI / 2 && side !== 'left' || + Math.abs(this.ball.angle) < Math.PI / 2 && side !== 'right') + return (false) + // 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 @@ -194,7 +200,6 @@ export class Pong { // 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)