From 458e40ac30d62116a7ef68542b7446967fd52a64 Mon Sep 17 00:00:00 2001 From: Franciszek Kaczmarek Date: Mon, 30 Dec 2024 13:50:56 +0100 Subject: [PATCH 01/19] Auth validates handshake, and verify old and new players, added functions to manage players --- client/src/ConnectionProvider/index.tsx | 11 +- server/index.ts | 4 +- server/src/Player.ts | 11 +- server/src/Room.ts | 43 ++++-- server/src/socketAuth.ts | 74 +++++---- server/src/socketRoutes.ts | 26 ---- server/src/socketRoutes/index.ts | 25 +++ server/src/webRoutes.ts | 2 +- types/Game.ts | 197 +++++++++++++----------- types/Persona.ts | 1 + types/Sockets.ts | 4 +- 11 files changed, 226 insertions(+), 172 deletions(-) delete mode 100644 server/src/socketRoutes.ts create mode 100644 server/src/socketRoutes/index.ts create mode 100644 types/Persona.ts diff --git a/client/src/ConnectionProvider/index.tsx b/client/src/ConnectionProvider/index.tsx index d6d7ad1..efbc420 100644 --- a/client/src/ConnectionProvider/index.tsx +++ b/client/src/ConnectionProvider/index.tsx @@ -8,13 +8,14 @@ function ConnectionProvider(props: { children: React.ReactNode }) { // Create a socket connection useEffect(() => { - if (socket.connected) return; + if (socket.connected || code === undefined) return; - socket.io.opts.query = { code }; + socket.io.opts.query = { code, name: "Player", id: "0" }; socket.on("connect", () => console.log("Connected to server")); socket.on("connect_error", (err) => console.error(err)); socket.on("disconnect", () => console.log("Disconnected from server")); + socket.on("info", (data) => console.info(data)); socket.connect(); return () => { @@ -22,11 +23,7 @@ function ConnectionProvider(props: { children: React.ReactNode }) { }; }, [code]); - return ( - - {props.children} - - ); + return {props.children}; } export default ConnectionProvider; diff --git a/server/index.ts b/server/index.ts index 3045211..f9f4b90 100644 --- a/server/index.ts +++ b/server/index.ts @@ -28,6 +28,4 @@ app.use("/", webRoutes); // Configure the HTTP server // Start servers httpServer.listen(config.PORT); // See https://socket.io/docs/v4/server-initialization/#with-express -console.log( - `Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}` -); +console.log(`Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}`); diff --git a/server/src/Player.ts b/server/src/Player.ts index 554bf74..d8ae661 100644 --- a/server/src/Player.ts +++ b/server/src/Player.ts @@ -1,9 +1,14 @@ +import { Persona } from "@global/Persona"; import { Roles } from "@global/Roles"; export class Player { + online: boolean = true; + persona: Persona = {}; + role: Roles | null = null; + seat: number | null = null; + constructor( - public name: string, - public id: number, - public role: Roles + public id: number, // Unique identifier for the player + public name: string // Real name of the player ) {} } diff --git a/server/src/Room.ts b/server/src/Room.ts index a4ae197..b9e1f6f 100644 --- a/server/src/Room.ts +++ b/server/src/Room.ts @@ -1,27 +1,50 @@ -import { Phases } from "@global/Game"; +import { NON_STRICT_PHASES, Phases } from "@global/Game"; import { Player } from "./Player"; export class Room { code: string; - players: Array = new Array(); + private players = new Map(); phase: Phases = Phases.LOBBY; constructor(code: string) { this.code = code; } - addPlayer(player: Player) { - this.players.push(player); + addPlayer(playerId: number, name: string) { + this.players.set(playerId, new Player(playerId, name)); } - addPlayerAt(playerId: number, player: Player) { - playerId = Math.max(playerId, this.players.length); - playerId = Math.min(playerId, 0); + removePlayer(playerId: number) { + if (NON_STRICT_PHASES.includes(this.phase)) { + this.players.delete(playerId); + } else { + this.players.get(playerId)!.online = false; + } + } - this.players.splice(playerId, 0, player); + hasPlayer(playerId: number) { + return this.players.has(playerId); } - removePlayer(playerId: number) { - delete this.players[playerId]; + getPlayer(playerId: number) { + return this.players.get(playerId); + } + + getPlayers() { + return Array.from(this.players.values()); + } + + setPlayerSeat(playerId: number, seat: number) { + for (const player of this.getPlayers()) { + if (player.seat && player.seat >= seat) player.seat++; + } + + this.players.get(playerId)!.seat = seat; + } + + getPlayersBySeat() { + return this.getPlayers() + .filter((player) => player.seat) + .sort((a, b) => a.seat! - b.seat!); } } diff --git a/server/src/socketAuth.ts b/server/src/socketAuth.ts index 3d03349..3f159d9 100644 --- a/server/src/socketAuth.ts +++ b/server/src/socketAuth.ts @@ -1,36 +1,58 @@ import { ExtendedError } from "socket.io"; import { MASocket } from "@local/Sockets"; import { manager } from "./RoomManager"; -import { Phases } from "@global/Game"; - -// Connection validation -export default function socketAuth( - socket: MASocket, - next: (err?: ExtendedError) => void -) { - // check if code is valid - const code = socket.handshake.query.code; +import { NON_STRICT_PHASES, Phases } from "@global/Game"; + +/* + Validate connection and Join player to room + To establish a connection, the client must provide: + - code - the room code + - name - the player name + - id - old player id or 0 for new player +*/ +export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) => void) { + // VALIDATING - if (code === undefined) return next(new Error("No code provided")); + // Code + const code = socket.handshake.query.code; + if (code === undefined) return next(new Error("Invalud code provided")); if (typeof code !== "string") return next(new Error("Invalid code provided")); - // check if room exists + // Room const room = manager.getRoom(code); if (room === undefined) return next(new Error(`Room ${code} does not exist`)); - // check if player can join - if ( - ![ - Phases.LOBBY, - Phases.POSITION_SELECTION, - Phases.CHARACTER_SELECTION, - Phases.ROLE_ASSIGNMENT, - Phases.WELCOME, - ].includes(room.phase) - ) - return next( - new Error(`ERROR: Room ${code} does not accept new players currently`) - ); - - return next(); + // Player Name + const name = socket.handshake.query.name; + if (name === undefined) return next(new Error("Invalid player name provided")); + if (typeof name !== "string") return next(new Error("Invalid player name provided")); + + // Player Id + const id = socket.handshake.query.id; + if (id === undefined) return next(new Error("Invalid player id provided")); + if (typeof id !== "string") return next(new Error("Invalid player id provided")); + + let playerId = parseInt(id); + if (isNaN(playerId)) return next(new Error("Invalid player id provided")); + + // JOINING ROOM + + // Join as new player + if (NON_STRICT_PHASES.includes(room.phase)) { + // Create new player + playerId = manager.generatePlayerId(); + room.addPlayer(playerId, name); + + socket.data = { playerId, roomCode: code }; + return next(); + } + + // Join as old player + if (room.hasPlayer(playerId)) { + socket.data = { playerId, roomCode: code }; + return next(); + } + + // Can't join + return next(new Error(`Room ${code} does not accept new players currently`)); } diff --git a/server/src/socketRoutes.ts b/server/src/socketRoutes.ts deleted file mode 100644 index ad63a0d..0000000 --- a/server/src/socketRoutes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { MASocket } from "@local/Sockets"; -import { manager } from "./RoomManager"; -import { Player } from "./Player"; -import { Roles } from "@global/Roles"; -import { Phases } from "@global/Game"; - -export default function socketRoutes(socket: MASocket) { - // these variables are valid because of auth middleware - const code = socket.handshake.query.code as string; - const room = manager.getRoom(code)!; - - console.log(`${socket.id} joinRoom ${code}`); - socket.join(code); - - socket.emit("phaseChange", room.phase); - - const playerid = manager.generatePlayerId(); - - room.addPlayer( - new Player(`player-${playerid}`, playerid, Roles.REGULAR_CITIZEN) - ); - - socket.on("vote", (data) => { - socket.emit("info", `You voted for ${data}`); - }); -} diff --git a/server/src/socketRoutes/index.ts b/server/src/socketRoutes/index.ts new file mode 100644 index 0000000..582f3b8 --- /dev/null +++ b/server/src/socketRoutes/index.ts @@ -0,0 +1,25 @@ +import { MASocket } from "@local/Sockets"; +import { manager } from "../RoomManager"; +import { Player } from "../Player"; +import { Roles } from "@global/Roles"; +import { Phases } from "@global/Game"; + +// Here we set up the socket events for the client +export default function socketRoutes(socket: MASocket) { + const room = manager.getRoom(socket.data.roomCode)!; + const playerid = socket.data.playerId; + + socket.emit("phaseChange", room.phase); + + socket.emit( + "info", + JSON.stringify({ + playerId: playerid, + roomCode: room.code, + }) + ); + + socket.on("disconnect", () => { + room.removePlayer(playerid); + }); +} diff --git a/server/src/webRoutes.ts b/server/src/webRoutes.ts index 78cafa9..df6a0c2 100644 --- a/server/src/webRoutes.ts +++ b/server/src/webRoutes.ts @@ -20,7 +20,7 @@ webRouter.get("/room/:code", (req, res) => { return; } - res.json({ roomCode: room.code, playersInRoom: room.players.length }); + res.json({ roomCode: room.code, playersInRoom: room.getPlayers().length }); }); export default webRouter; diff --git a/types/Game.ts b/types/Game.ts index 77280a6..7e72475 100644 --- a/types/Game.ts +++ b/types/Game.ts @@ -1,96 +1,105 @@ export enum Phases { - /** - * Faza LOBBY: Gracze dołączają do gry. Gra czeka, aż wszyscy gracze zgłoszą gotowość. - */ - LOBBY = "LOBBY", - - /** - * Faza POSITION_SELECTION: Gracze wybierają swoje pozycje (np. miejsca przy stole). - * Jeśli wszyscy gracze są gotowi, gra przechodzi do CHARACTER_SELECTION. - */ - POSITION_SELECTION = "POSITION_SELECTION", - - /** - * Faza CHARACTER_SELECTION: Gracze wybierają swoje jawne postacie. - * Jeśli wszyscy gracze są gotowi, gra przechodzi do ROLE_ASSIGNMENT. - */ - CHARACTER_SELECTION = "CHARACTER_SELECTION", - - /** - * Faza ROLE_ASSIGNMENT: Gracze otrzymują swoje tajne role (np. obywatel, mafia). - * Po zakończeniu losowania, gra automatycznie przechodzi do WELCOME. - */ - ROLE_ASSIGNMENT = "ROLE_ASSIGNMENT", - - /** - * Faza WELCOME: Krótkie wprowadzenie do gry. Gracze mogą zapoznać się z zasadami. - * Po określonym czasie gra automatycznie przechodzi do ROUND_START. - */ - WELCOME = "WELCOME", - - /** - * Faza ROUND_START: Oficjalne rozpoczęcie rundy. Trwa przez krótki czas, zanim gra przejdzie do DAY. - */ - ROUND_START = "ROUND_START", - - /** - * Faza DAY: Początek dnia w grze. Gracze mogą rozmawiać i analizować wydarzenia. - * Po określonym czasie gra przechodzi do DEBATE. - */ - DAY = "DAY", - - /** - * Faza DEBATE: Gracze prowadzą debatę na temat swoich podejrzeń. - * Po określonym czasie gra przechodzi do VOTING. - */ - DEBATE = "DEBATE", - - /** - * Faza VOTING: Gracze głosują na osobę, która ich zdaniem powinna zostać wyeliminowana. - * Jeśli dwie lub więcej osób otrzyma taką samą największą liczbę głosów, gra przechodzi do VOTING_OVERTIME. - * W przeciwnym razie, gra przechodzi do NIGHT. - */ - VOTING = "VOTING", - - /** - * Faza VOTING_OVERTIME: Dogrywka głosowania, gdy dwie lub więcej osób otrzymało największą liczbę głosów. - * Po rozstrzygnięciu gra przechodzi do NIGHT. - */ - VOTING_OVERTIME = "VOTING_OVERTIME", - - /** - * Faza NIGHT: Gracze wykonują swoje nocne akcje. - * Gra automatycznie przechodzi do BODYGUARD_DEFENSE. - */ - NIGHT = "NIGHT", - - /** - * Faza BODYGUARD_DEFENSE: Ochroniarz wybiera, kogo chce obronić przed atakiem mafii. - * Jeśli ochroniarz wykona swoją akcję, gra przechodzi do DETECTIVE_CHECK. - */ - BODYGUARD_DEFENSE = "BODYGUARD_DEFENSE", - - /** - * Faza DETECTIVE_CHECK: Detektyw wybiera osobę, której tożsamość chce sprawdzić (obywatel/mafia). - * Po wykonaniu akcji przez detektywa gra przechodzi do MAFIA_VOTING. - */ - DETECTIVE_CHECK = "DETECTIVE_CHECK", - - /** - * Faza MAFIA_VOTING: Mafia wspólnie wybiera osobę, którą chce wyeliminować. - * Jeśli mafia dokona wyboru lub upłynie określony czas, gra przechodzi do ROUND_END. - */ - MAFIA_VOTING = "MAFIA_VOTING", - - /** - * Faza ROUND_END: Podsumowanie rundy. Gra pokazuje wyniki nocnych akcji (np. kto został zabity). - * Jeśli wszyscy mafiozi zostali wyeliminowani lub liczba mafiozów >= liczba obywateli, gra przechodzi do GAME_END. - * W przeciwnym razie gra wraca do DAY. - */ - ROUND_END = "ROUND_END", - - /** - * Faza GAME_END: Zakończenie gry. Gra pokazuje wyniki i zwycięzców (obywatele lub mafia). - */ - GAME_END = "GAME_END" + /** + * Faza LOBBY: Gracze dołączają do gry. Gra czeka, aż wszyscy gracze zgłoszą gotowość. + */ + LOBBY = "LOBBY", + + /** + * Faza POSITION_SELECTION: Gracze wybierają swoje pozycje (np. miejsca przy stole). + * Jeśli wszyscy gracze są gotowi, gra przechodzi do CHARACTER_SELECTION. + */ + POSITION_SELECTION = "POSITION_SELECTION", + + /** + * Faza CHARACTER_SELECTION: Gracze wybierają swoje jawne postacie. + * Jeśli wszyscy gracze są gotowi, gra przechodzi do ROLE_ASSIGNMENT. + */ + CHARACTER_SELECTION = "CHARACTER_SELECTION", + + /** + * Faza ROLE_ASSIGNMENT: Gracze otrzymują swoje tajne role (np. obywatel, mafia). + * Po zakończeniu losowania, gra automatycznie przechodzi do WELCOME. + */ + ROLE_ASSIGNMENT = "ROLE_ASSIGNMENT", + + /** + * Faza WELCOME: Krótkie wprowadzenie do gry. Gracze mogą zapoznać się z zasadami. + * Po określonym czasie gra automatycznie przechodzi do ROUND_START. + */ + WELCOME = "WELCOME", + + /** + * Faza ROUND_START: Oficjalne rozpoczęcie rundy. Trwa przez krótki czas, zanim gra przejdzie do DAY. + */ + ROUND_START = "ROUND_START", + + /** + * Faza DAY: Początek dnia w grze. Gracze mogą rozmawiać i analizować wydarzenia. + * Po określonym czasie gra przechodzi do DEBATE. + */ + DAY = "DAY", + + /** + * Faza DEBATE: Gracze prowadzą debatę na temat swoich podejrzeń. + * Po określonym czasie gra przechodzi do VOTING. + */ + DEBATE = "DEBATE", + + /** + * Faza VOTING: Gracze głosują na osobę, która ich zdaniem powinna zostać wyeliminowana. + * Jeśli dwie lub więcej osób otrzyma taką samą największą liczbę głosów, gra przechodzi do VOTING_OVERTIME. + * W przeciwnym razie, gra przechodzi do NIGHT. + */ + VOTING = "VOTING", + + /** + * Faza VOTING_OVERTIME: Dogrywka głosowania, gdy dwie lub więcej osób otrzymało największą liczbę głosów. + * Po rozstrzygnięciu gra przechodzi do NIGHT. + */ + VOTING_OVERTIME = "VOTING_OVERTIME", + + /** + * Faza NIGHT: Gracze wykonują swoje nocne akcje. + * Gra automatycznie przechodzi do BODYGUARD_DEFENSE. + */ + NIGHT = "NIGHT", + + /** + * Faza BODYGUARD_DEFENSE: Ochroniarz wybiera, kogo chce obronić przed atakiem mafii. + * Jeśli ochroniarz wykona swoją akcję, gra przechodzi do DETECTIVE_CHECK. + */ + BODYGUARD_DEFENSE = "BODYGUARD_DEFENSE", + + /** + * Faza DETECTIVE_CHECK: Detektyw wybiera osobę, której tożsamość chce sprawdzić (obywatel/mafia). + * Po wykonaniu akcji przez detektywa gra przechodzi do MAFIA_VOTING. + */ + DETECTIVE_CHECK = "DETECTIVE_CHECK", + + /** + * Faza MAFIA_VOTING: Mafia wspólnie wybiera osobę, którą chce wyeliminować. + * Jeśli mafia dokona wyboru lub upłynie określony czas, gra przechodzi do ROUND_END. + */ + MAFIA_VOTING = "MAFIA_VOTING", + + /** + * Faza ROUND_END: Podsumowanie rundy. Gra pokazuje wyniki nocnych akcji (np. kto został zabity). + * Jeśli wszyscy mafiozi zostali wyeliminowani lub liczba mafiozów >= liczba obywateli, gra przechodzi do GAME_END. + * W przeciwnym razie gra wraca do DAY. + */ + ROUND_END = "ROUND_END", + + /** + * Faza GAME_END: Zakończenie gry. Gra pokazuje wyniki i zwycięzców (obywatele lub mafia). + */ + GAME_END = "GAME_END", } + +/** + * Fazy gry, do których mogą dołączyć nowi gracze. + */ +export const NON_STRICT_PHASES: ReadonlyArray = [ + Phases.LOBBY, + Phases.POSITION_SELECTION, + Phases.CHARACTER_SELECTION, +]; diff --git a/types/Persona.ts b/types/Persona.ts new file mode 100644 index 0000000..1c82b99 --- /dev/null +++ b/types/Persona.ts @@ -0,0 +1 @@ +export interface Persona {} diff --git a/types/Sockets.ts b/types/Sockets.ts index 302502f..5bf0883 100644 --- a/types/Sockets.ts +++ b/types/Sockets.ts @@ -18,6 +18,6 @@ export interface InterServerEvents { } export interface SocketData { - name: string; - age: number; + playerId: number; + roomCode: string; } From a8a63a554d1caa56ba580d9772d7e147b835b4e0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sun, 5 Jan 2025 20:49:50 +0100 Subject: [PATCH 02/19] refactor: move types directory to src --- client/{ => src}/types/Socket.ts | 0 client/tsconfig.app.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename client/{ => src}/types/Socket.ts (100%) diff --git a/client/types/Socket.ts b/client/src/types/Socket.ts similarity index 100% rename from client/types/Socket.ts rename to client/src/types/Socket.ts diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 9b314bb..be80715 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -28,5 +28,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src", "types"] + "include": ["src"] } From a9a08a54be25b54358d7e3b778d40f2d21f797d4 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sun, 5 Jan 2025 21:01:42 +0100 Subject: [PATCH 03/19] refactor: move socket constant to separate folder --- client/src/ConnectionProvider/Context.ts | 8 ++------ client/src/ConnectionProvider/index.tsx | 3 ++- client/src/constants/index.ts | 3 +++ client/src/constants/socket.ts | 6 ++++++ 4 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 client/src/constants/index.ts create mode 100644 client/src/constants/socket.ts diff --git a/client/src/ConnectionProvider/Context.ts b/client/src/ConnectionProvider/Context.ts index 9748e9f..b419f8b 100644 --- a/client/src/ConnectionProvider/Context.ts +++ b/client/src/ConnectionProvider/Context.ts @@ -1,10 +1,6 @@ import { createContext } from "react"; -import { connect } from "socket.io-client"; -import type { ConnectionContext, CustomSocket } from "types/Socket"; - -export const socket: CustomSocket = connect("ws://localhost:5000", { - autoConnect: false, -}); +import type { ConnectionContext } from "@/types/Socket"; +import { socket } from "@/constants"; export const ConnContext = createContext({ socket, diff --git a/client/src/ConnectionProvider/index.tsx b/client/src/ConnectionProvider/index.tsx index efbc420..3d8f417 100644 --- a/client/src/ConnectionProvider/index.tsx +++ b/client/src/ConnectionProvider/index.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react"; import { useParams } from "react-router"; -import { ConnContext, socket } from "./Context"; +import { ConnContext } from "./Context"; +import { socket } from "@/constants"; function ConnectionProvider(props: { children: React.ReactNode }) { // Get the code from the URL diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts new file mode 100644 index 0000000..b1a69e3 --- /dev/null +++ b/client/src/constants/index.ts @@ -0,0 +1,3 @@ +import { socket } from "./socket"; + +export { socket }; diff --git a/client/src/constants/socket.ts b/client/src/constants/socket.ts new file mode 100644 index 0000000..ce71e4e --- /dev/null +++ b/client/src/constants/socket.ts @@ -0,0 +1,6 @@ +import type { CustomSocket } from "@/types/Socket"; +import { io } from "socket.io-client"; + +export const socket: CustomSocket = io("ws://localhost:5000", { + autoConnect: false, +}); From d0168a9af0bb25d32bc1247f979be07b0cba5f31 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sun, 5 Jan 2025 21:11:17 +0100 Subject: [PATCH 04/19] refactor: move provider component and context to features/connection --- client/src/App.tsx | 2 +- .../Context.ts => features/connection/ConnContext.ts} | 0 .../index.tsx => features/connection/ConnectionProvider.tsx} | 2 +- client/src/features/connection/index.ts | 4 ++++ client/src/pages/Game/index.tsx | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) rename client/src/{ConnectionProvider/Context.ts => features/connection/ConnContext.ts} (100%) rename client/src/{ConnectionProvider/index.tsx => features/connection/ConnectionProvider.tsx} (95%) create mode 100644 client/src/features/connection/index.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 0ad638d..2d65621 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import { BrowserRouter, Route, Routes } from "react-router"; -import ConnectionProvider from "./ConnectionProvider"; +import { ConnectionProvider } from "@/features/connection"; import { GameScreen, MenuScreen } from "@/pages"; function App() { diff --git a/client/src/ConnectionProvider/Context.ts b/client/src/features/connection/ConnContext.ts similarity index 100% rename from client/src/ConnectionProvider/Context.ts rename to client/src/features/connection/ConnContext.ts diff --git a/client/src/ConnectionProvider/index.tsx b/client/src/features/connection/ConnectionProvider.tsx similarity index 95% rename from client/src/ConnectionProvider/index.tsx rename to client/src/features/connection/ConnectionProvider.tsx index 3d8f417..baaa1f8 100644 --- a/client/src/ConnectionProvider/index.tsx +++ b/client/src/features/connection/ConnectionProvider.tsx @@ -1,6 +1,6 @@ import { useEffect } from "react"; import { useParams } from "react-router"; -import { ConnContext } from "./Context"; +import { ConnContext } from "./ConnContext"; import { socket } from "@/constants"; function ConnectionProvider(props: { children: React.ReactNode }) { diff --git a/client/src/features/connection/index.ts b/client/src/features/connection/index.ts new file mode 100644 index 0000000..aabf727 --- /dev/null +++ b/client/src/features/connection/index.ts @@ -0,0 +1,4 @@ +import ConnectionProvider from "./ConnectionProvider"; +import { ConnContext } from "./ConnContext"; + +export { ConnContext, ConnectionProvider }; diff --git a/client/src/pages/Game/index.tsx b/client/src/pages/Game/index.tsx index 0ac700c..0db9a5b 100644 --- a/client/src/pages/Game/index.tsx +++ b/client/src/pages/Game/index.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { ConnContext } from "../../ConnectionProvider/Context"; +import { ConnContext } from "@/features/connection"; import { Phases } from "@global/Game"; import Lobby from "@/pages/Lobby"; From ba7de5436c3c92771f8038fea14f6fa0131cb890 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Mon, 6 Jan 2025 18:01:07 +0100 Subject: [PATCH 05/19] feat(connection): add methods to connect and disconnect --- client/src/features/connection/ConnContext.ts | 3 +++ .../features/connection/ConnectionProvider.tsx | 16 +++++++++------- client/src/features/connection/utils.ts | 15 +++++++++++++++ client/src/types/Socket.ts | 13 +++++++++---- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 client/src/features/connection/utils.ts diff --git a/client/src/features/connection/ConnContext.ts b/client/src/features/connection/ConnContext.ts index b419f8b..5983c62 100644 --- a/client/src/features/connection/ConnContext.ts +++ b/client/src/features/connection/ConnContext.ts @@ -1,7 +1,10 @@ import { createContext } from "react"; import type { ConnectionContext } from "@/types/Socket"; import { socket } from "@/constants"; +import { connect, disconnect } from "./utils"; export const ConnContext = createContext({ socket, + connect, + disconnect, }); diff --git a/client/src/features/connection/ConnectionProvider.tsx b/client/src/features/connection/ConnectionProvider.tsx index baaa1f8..3e6e1f1 100644 --- a/client/src/features/connection/ConnectionProvider.tsx +++ b/client/src/features/connection/ConnectionProvider.tsx @@ -1,7 +1,8 @@ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { useParams } from "react-router"; import { ConnContext } from "./ConnContext"; import { socket } from "@/constants"; +import { connect, disconnect } from "./utils"; function ConnectionProvider(props: { children: React.ReactNode }) { // Get the code from the URL @@ -9,22 +10,23 @@ function ConnectionProvider(props: { children: React.ReactNode }) { // Create a socket connection useEffect(() => { - if (socket.connected || code === undefined) return; - - socket.io.opts.query = { code, name: "Player", id: "0" }; + if (code === undefined) return; socket.on("connect", () => console.log("Connected to server")); socket.on("connect_error", (err) => console.error(err)); socket.on("disconnect", () => console.log("Disconnected from server")); socket.on("info", (data) => console.info(data)); - socket.connect(); + + connect({ code, id: "0", name: "Player" }); return () => { - socket.disconnect(); + disconnect(); }; }, [code]); - return {props.children}; + const contextValue = useMemo(() => ({ socket, connect, disconnect }), []); + + return {props.children}; } export default ConnectionProvider; diff --git a/client/src/features/connection/utils.ts b/client/src/features/connection/utils.ts new file mode 100644 index 0000000..da114cd --- /dev/null +++ b/client/src/features/connection/utils.ts @@ -0,0 +1,15 @@ +import { socket } from "@/constants"; +import type { SocketQueryParams } from "@/types/Socket"; + +const connect = (queryParams: SocketQueryParams) => { + if (socket.connected) return; + + socket.io.opts.query = queryParams; + socket.connect(); +}; + +const disconnect = () => { + socket.disconnect(); +}; + +export { connect, disconnect }; diff --git a/client/src/types/Socket.ts b/client/src/types/Socket.ts index 5afd263..f25e209 100644 --- a/client/src/types/Socket.ts +++ b/client/src/types/Socket.ts @@ -1,11 +1,16 @@ -import { - type Client2ServerEvents, - type Server2ClientEvents, -} from "@global/Sockets"; +import { type Client2ServerEvents, type Server2ClientEvents } from "@global/Sockets"; import { Socket } from "socket.io-client"; export type CustomSocket = Socket; export interface ConnectionContext { socket: CustomSocket; + connect: (queryParams: SocketQueryParams) => void; + disconnect: () => void; +} + +export interface SocketQueryParams { + code: string; + name: string; + id: string; } From 7f656160193b31991b617d8a6097253c37f2a7fd Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Mon, 6 Jan 2025 18:11:50 +0100 Subject: [PATCH 06/19] refactor(ConnectionProvider): extract logic for event handlers into separate hook --- .../{ConnContext.ts => ConnectionContext.ts} | 0 .../connection/ConnectionProvider.tsx | 22 ++++-------------- client/src/features/connection/index.ts | 2 +- .../src/features/connection/useConnection.ts | 23 +++++++++++++++++++ 4 files changed, 28 insertions(+), 19 deletions(-) rename client/src/features/connection/{ConnContext.ts => ConnectionContext.ts} (100%) create mode 100644 client/src/features/connection/useConnection.ts diff --git a/client/src/features/connection/ConnContext.ts b/client/src/features/connection/ConnectionContext.ts similarity index 100% rename from client/src/features/connection/ConnContext.ts rename to client/src/features/connection/ConnectionContext.ts diff --git a/client/src/features/connection/ConnectionProvider.tsx b/client/src/features/connection/ConnectionProvider.tsx index 3e6e1f1..3427b4e 100644 --- a/client/src/features/connection/ConnectionProvider.tsx +++ b/client/src/features/connection/ConnectionProvider.tsx @@ -1,28 +1,14 @@ -import { useEffect, useMemo } from "react"; +import { useMemo } from "react"; import { useParams } from "react-router"; -import { ConnContext } from "./ConnContext"; +import { ConnContext } from "./ConnectionContext"; import { socket } from "@/constants"; import { connect, disconnect } from "./utils"; +import useConnection from "./useConnection"; function ConnectionProvider(props: { children: React.ReactNode }) { // Get the code from the URL const { code } = useParams(); - - // Create a socket connection - useEffect(() => { - if (code === undefined) return; - - socket.on("connect", () => console.log("Connected to server")); - socket.on("connect_error", (err) => console.error(err)); - socket.on("disconnect", () => console.log("Disconnected from server")); - socket.on("info", (data) => console.info(data)); - - connect({ code, id: "0", name: "Player" }); - - return () => { - disconnect(); - }; - }, [code]); + useConnection(code); const contextValue = useMemo(() => ({ socket, connect, disconnect }), []); diff --git a/client/src/features/connection/index.ts b/client/src/features/connection/index.ts index aabf727..75e6fa1 100644 --- a/client/src/features/connection/index.ts +++ b/client/src/features/connection/index.ts @@ -1,4 +1,4 @@ import ConnectionProvider from "./ConnectionProvider"; -import { ConnContext } from "./ConnContext"; +import { ConnContext } from "./ConnectionContext"; export { ConnContext, ConnectionProvider }; diff --git a/client/src/features/connection/useConnection.ts b/client/src/features/connection/useConnection.ts new file mode 100644 index 0000000..b6f1661 --- /dev/null +++ b/client/src/features/connection/useConnection.ts @@ -0,0 +1,23 @@ +import { socket } from "@/constants"; +import { useEffect } from "react"; +import { connect, disconnect } from "./utils"; + +function useConnection(code: string | undefined) { + useEffect(() => { + if (code === undefined) return; + + socket.on("connect", () => console.log("Connected to server")); + socket.on("connect_error", (err) => console.error(err)); + socket.on("disconnect", () => console.log("Disconnected from server")); + socket.on("info", (data) => console.info(data)); + + connect({ code, id: "0", name: "Player" }); + + return () => { + disconnect(); + //TODO: remove registered listeners + }; + }, [code]); +} + +export default useConnection; From 6bf0b1b908b1f5f850b30e8b71227bd42afb8258 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Mon, 6 Jan 2025 19:38:48 +0100 Subject: [PATCH 07/19] chore: add script in package.json to install new packages in root dir, client, server with 1 command --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index deeb9b3..896f036 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "server:dev": "npm --prefix ./server run dev", "all:dev": "npx concurrently \"npm run client:dev\" \"npm run server:dev\"", "format:front": "prettier --write \"./client/src/**/*.{js,jsx,ts,tsx}\"", - "format:back": "prettier --write \"./server/src/**/*.{js,jsx,ts,tsx}\"" + "format:back": "prettier --write \"./server/src/**/*.{js,jsx,ts,tsx}\"", + "instDeps": "npm i & (cd server && npm i) & (cd ../client && npm i)" }, "repository": { "type": "git", From fd5b4d1194ab4a9150b606b347be47ceb4183a9e Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Thu, 9 Jan 2025 22:36:23 +0100 Subject: [PATCH 08/19] refactor: store server url on client in .env --- client/.env | 1 + client/src/constants/socket.ts | 2 +- client/src/vite-env.d.ts | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 client/.env diff --git a/client/.env b/client/.env new file mode 100644 index 0000000..828dd3e --- /dev/null +++ b/client/.env @@ -0,0 +1 @@ +VITE_SERVER_URL="ws://localhost:5000" \ No newline at end of file diff --git a/client/src/constants/socket.ts b/client/src/constants/socket.ts index ce71e4e..b7e890b 100644 --- a/client/src/constants/socket.ts +++ b/client/src/constants/socket.ts @@ -1,6 +1,6 @@ import type { CustomSocket } from "@/types/Socket"; import { io } from "socket.io-client"; -export const socket: CustomSocket = io("ws://localhost:5000", { +export const socket: CustomSocket = io(import.meta.env.VITE_SERVER_URL, { autoConnect: false, }); diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts index 11f02fe..d8fcf94 100644 --- a/client/src/vite-env.d.ts +++ b/client/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_SERVER_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} From afe8a53e47bd2c0b3aa59b3c2cabcc89fd8eb5d5 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 10:49:16 +0100 Subject: [PATCH 09/19] feat: store sid in localStorage and retrieve it on backend (early stage) --- client/src/constants/index.ts | 4 +- .../src/features/connection/useConnection.ts | 9 ++-- client/src/features/connection/utils.ts | 15 ++++-- client/src/pages/Game/index.tsx | 8 ++- client/src/pages/Lobby/Character.tsx | 13 ++++- client/src/types/Socket.ts | 8 +-- server/index.ts | 6 ++- server/src/Player.ts | 4 +- server/src/Room.ts | 14 ++--- server/src/RoomManager.ts | 4 +- server/src/socketAuth.ts | 51 ++++++++++--------- server/src/socketRoutes/index.ts | 30 ++++++----- types/Sockets.ts | 9 ++-- 13 files changed, 102 insertions(+), 73 deletions(-) diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts index b1a69e3..0109a4b 100644 --- a/client/src/constants/index.ts +++ b/client/src/constants/index.ts @@ -1,3 +1,5 @@ import { socket } from "./socket"; -export { socket }; +const SID_KEY_NAME = "session_id"; + +export { socket, SID_KEY_NAME }; diff --git a/client/src/features/connection/useConnection.ts b/client/src/features/connection/useConnection.ts index b6f1661..b5cc6f6 100644 --- a/client/src/features/connection/useConnection.ts +++ b/client/src/features/connection/useConnection.ts @@ -1,4 +1,4 @@ -import { socket } from "@/constants"; +import { SID_KEY_NAME, socket } from "@/constants"; import { useEffect } from "react"; import { connect, disconnect } from "./utils"; @@ -9,9 +9,12 @@ function useConnection(code: string | undefined) { socket.on("connect", () => console.log("Connected to server")); socket.on("connect_error", (err) => console.error(err)); socket.on("disconnect", () => console.log("Disconnected from server")); - socket.on("info", (data) => console.info(data)); - connect({ code, id: "0", name: "Player" }); + socket.on("conn_info_data", ({ playerId }) => { + localStorage.setItem(SID_KEY_NAME, playerId); + }); + + connect(code); return () => { disconnect(); diff --git a/client/src/features/connection/utils.ts b/client/src/features/connection/utils.ts index da114cd..1d4b851 100644 --- a/client/src/features/connection/utils.ts +++ b/client/src/features/connection/utils.ts @@ -1,10 +1,17 @@ -import { socket } from "@/constants"; -import type { SocketQueryParams } from "@/types/Socket"; +import { SID_KEY_NAME, socket } from "@/constants"; -const connect = (queryParams: SocketQueryParams) => { +const connect = (roomCode: string) => { if (socket.connected) return; - socket.io.opts.query = queryParams; + const playerId = localStorage.getItem(SID_KEY_NAME); + + if (playerId) { + socket.auth = { playerId }; + } + + console.log(`socket.auth: ${JSON.stringify(socket.auth)} playerId: ${playerId}`); + + socket.io.opts.query = { roomCode }; socket.connect(); }; diff --git a/client/src/pages/Game/index.tsx b/client/src/pages/Game/index.tsx index 0db9a5b..d0c15d4 100644 --- a/client/src/pages/Game/index.tsx +++ b/client/src/pages/Game/index.tsx @@ -9,7 +9,13 @@ function Game() { const [phase, setPhase] = useState(Phases.GAME_END); useEffect(() => { - socket.on("phaseChange", (data: Phases) => setPhase(data)); + socket.on("phase_updated", ({ err, phase }) => { + if (err) { + console.error("Error in phase_updated occured: " + err); + return; + } + setPhase(phase); + }); }, [socket]); // Render certain components based on the game phase diff --git a/client/src/pages/Lobby/Character.tsx b/client/src/pages/Lobby/Character.tsx index 462477f..f26b26e 100644 --- a/client/src/pages/Lobby/Character.tsx +++ b/client/src/pages/Lobby/Character.tsx @@ -1,6 +1,9 @@ import { Button, Input } from "@/components"; +import { ConnContext } from "@/features/connection"; +import { useContext } from "react"; function Character() { + const { disconnect, connect } = useContext(ConnContext); return (
e.preventDefault()}>
@@ -47,7 +50,15 @@ function Character() {
- +
); } diff --git a/client/src/types/Socket.ts b/client/src/types/Socket.ts index f25e209..97e3be8 100644 --- a/client/src/types/Socket.ts +++ b/client/src/types/Socket.ts @@ -5,12 +5,6 @@ export type CustomSocket = Socket; export interface ConnectionContext { socket: CustomSocket; - connect: (queryParams: SocketQueryParams) => void; + connect: (roomCode: string) => void; disconnect: () => void; } - -export interface SocketQueryParams { - code: string; - name: string; - id: string; -} diff --git a/server/index.ts b/server/index.ts index f9f4b90..18f463f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -27,5 +27,7 @@ socketsServer.use(socketAuth).on("connection", socketRoutes); // Configure the W app.use("/", webRoutes); // Configure the HTTP server // Start servers -httpServer.listen(config.PORT); // See https://socket.io/docs/v4/server-initialization/#with-express -console.log(`Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}`); +httpServer.listen(config.PORT, () => { + console.log(`Server is running on${"\x1b[34m"} http://${config.HOST}:${config.PORT}${"\x1b[0m"}`); +}); +// See https://socket.io/docs/v4/server-initialization/#with-express diff --git a/server/src/Player.ts b/server/src/Player.ts index d8ae661..ad602dc 100644 --- a/server/src/Player.ts +++ b/server/src/Player.ts @@ -8,7 +8,7 @@ export class Player { seat: number | null = null; constructor( - public id: number, // Unique identifier for the player - public name: string // Real name of the player + public id: string // Unique identifier for the player + // public name: string // Real name of the player ) {} } diff --git a/server/src/Room.ts b/server/src/Room.ts index b9e1f6f..5cc37be 100644 --- a/server/src/Room.ts +++ b/server/src/Room.ts @@ -3,18 +3,18 @@ import { Player } from "./Player"; export class Room { code: string; - private players = new Map(); + private players = new Map(); phase: Phases = Phases.LOBBY; constructor(code: string) { this.code = code; } - addPlayer(playerId: number, name: string) { - this.players.set(playerId, new Player(playerId, name)); + addPlayer(playerId: string) { + this.players.set(playerId, new Player(playerId)); } - removePlayer(playerId: number) { + removePlayer(playerId: string) { if (NON_STRICT_PHASES.includes(this.phase)) { this.players.delete(playerId); } else { @@ -22,11 +22,11 @@ export class Room { } } - hasPlayer(playerId: number) { + hasPlayer(playerId: string) { return this.players.has(playerId); } - getPlayer(playerId: number) { + getPlayer(playerId: string): Player | undefined { return this.players.get(playerId); } @@ -34,7 +34,7 @@ export class Room { return Array.from(this.players.values()); } - setPlayerSeat(playerId: number, seat: number) { + setPlayerSeat(playerId: string, seat: number) { for (const player of this.getPlayers()) { if (player.seat && player.seat >= seat) player.seat++; } diff --git a/server/src/RoomManager.ts b/server/src/RoomManager.ts index a17d811..c6971dd 100644 --- a/server/src/RoomManager.ts +++ b/server/src/RoomManager.ts @@ -13,8 +13,8 @@ class RoomManager { return code; } - generatePlayerId(): number { - return this.playerIdCounter++; + generatePlayerId(): string { + return (this.playerIdCounter++).toString(); } create(): string { diff --git a/server/src/socketAuth.ts b/server/src/socketAuth.ts index 3f159d9..4f587f8 100644 --- a/server/src/socketAuth.ts +++ b/server/src/socketAuth.ts @@ -1,7 +1,7 @@ import { ExtendedError } from "socket.io"; import { MASocket } from "@local/Sockets"; import { manager } from "./RoomManager"; -import { NON_STRICT_PHASES, Phases } from "@global/Game"; +import { NON_STRICT_PHASES } from "@global/Game"; /* Validate connection and Join player to room @@ -11,10 +11,8 @@ import { NON_STRICT_PHASES, Phases } from "@global/Game"; - id - old player id or 0 for new player */ export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) => void) { - // VALIDATING - // Code - const code = socket.handshake.query.code; + const code = socket.handshake.query.roomCode; if (code === undefined) return next(new Error("Invalud code provided")); if (typeof code !== "string") return next(new Error("Invalid code provided")); @@ -22,34 +20,37 @@ export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) const room = manager.getRoom(code); if (room === undefined) return next(new Error(`Room ${code} does not exist`)); - // Player Name - const name = socket.handshake.query.name; - if (name === undefined) return next(new Error("Invalid player name provided")); - if (typeof name !== "string") return next(new Error("Invalid player name provided")); - - // Player Id - const id = socket.handshake.query.id; - if (id === undefined) return next(new Error("Invalid player id provided")); - if (typeof id !== "string") return next(new Error("Invalid player id provided")); + console.log(room); - let playerId = parseInt(id); - if (isNaN(playerId)) return next(new Error("Invalid player id provided")); + const playerId = socket.handshake.auth.playerId; + // const session = sessionStore.findSession(sessionId); - // JOINING ROOM - - // Join as new player - if (NON_STRICT_PHASES.includes(room.phase)) { - // Create new player - playerId = manager.generatePlayerId(); - room.addPlayer(playerId, name); + //console.log(`server-side socketAuth. sid: ${sessionId}`); + console.log("hanshake id: " + playerId); + console.log(JSON.stringify(room.getPlayers())); + // Join as old player + if (room.hasPlayer(playerId)) { + console.log("JOIN AS OLD PLAYER"); socket.data = { playerId, roomCode: code }; return next(); } - // Join as old player - if (room.hasPlayer(playerId)) { - socket.data = { playerId, roomCode: code }; + // Join as new player + if (NON_STRICT_PHASES.includes(room.phase)) { + console.log("JOIN AS NEW PLAYER"); + // const newSessionId = manager.genSessionId(); + const newPlayerId = manager.generatePlayerId(); + room.addPlayer(newPlayerId); //creates new player and assigns to room + // console.log(room.hasPlayer(newPlayerId)); + // sessionStore.saveSession(newSessionId, { + // playerId: newPlayerId, + //}); + // console.log(sessionStore.findSession(newSessionId)?.playerId === newPlayerId); + + socket.data = { playerId: newPlayerId, roomCode: code }; + console.log("room: " + JSON.stringify(room.getPlayers())); + return next(); } diff --git a/server/src/socketRoutes/index.ts b/server/src/socketRoutes/index.ts index 582f3b8..922a392 100644 --- a/server/src/socketRoutes/index.ts +++ b/server/src/socketRoutes/index.ts @@ -1,25 +1,27 @@ import { MASocket } from "@local/Sockets"; import { manager } from "../RoomManager"; -import { Player } from "../Player"; -import { Roles } from "@global/Roles"; -import { Phases } from "@global/Game"; +import { NON_STRICT_PHASES } from "@global/Game"; // Here we set up the socket events for the client export default function socketRoutes(socket: MASocket) { - const room = manager.getRoom(socket.data.roomCode)!; - const playerid = socket.data.playerId; + const room = manager.getRoom(socket.data.roomCode); + const playerId = socket.data.playerId; - socket.emit("phaseChange", room.phase); + console.log("conn_info_data", { + sessionId: socket.data.playerId, + }); + + socket.emit("conn_info_data", { + playerId: socket.data.playerId, + }); - socket.emit( - "info", - JSON.stringify({ - playerId: playerid, - roomCode: room.code, - }) - ); + if (!room) { + socket.emit("phase_updated", { err: "Room is not found", phase: NON_STRICT_PHASES[0] }); + } else { + socket.emit("phase_updated", { err: "", phase: room.phase }); + } socket.on("disconnect", () => { - room.removePlayer(playerid); + room?.removePlayer(playerId); }); } diff --git a/types/Sockets.ts b/types/Sockets.ts index 5bf0883..ea30dd8 100644 --- a/types/Sockets.ts +++ b/types/Sockets.ts @@ -1,11 +1,12 @@ import type { Phases } from "./Game"; export interface Server2ClientEvents { - rooms: (rooms: Array) => void; - info: (data: string) => void; - phaseChange: (phase: Phases) => void; + rooms_data: (rooms: Array) => void; + conn_info_data: (data: ConnectionInfoData) => void; + phase_updated: (data: { err: string; phase: Phases }) => void; } +export type ConnectionInfoData = { playerId: string }; export type ResponseHandler = (message: string) => void; export interface Client2ServerEvents { @@ -18,6 +19,6 @@ export interface InterServerEvents { } export interface SocketData { - playerId: number; + playerId: string; roomCode: string; } From 9bea8a34ee8f7ae1853032c2dbdc9ee4db8375c3 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 12:46:13 +0100 Subject: [PATCH 10/19] refactor: remove unnecessary console logs --- client/src/constants/index.ts | 4 ++-- .../src/features/connection/useConnection.ts | 4 ++-- client/src/features/connection/utils.ts | 6 ++---- server/src/socketAuth.ts | 19 +------------------ server/src/socketRoutes/index.ts | 4 ---- 5 files changed, 7 insertions(+), 30 deletions(-) diff --git a/client/src/constants/index.ts b/client/src/constants/index.ts index 0109a4b..d0d9fa1 100644 --- a/client/src/constants/index.ts +++ b/client/src/constants/index.ts @@ -1,5 +1,5 @@ import { socket } from "./socket"; -const SID_KEY_NAME = "session_id"; +const PLAYER_ID_KEY_NAME = "player_id"; -export { socket, SID_KEY_NAME }; +export { socket, PLAYER_ID_KEY_NAME }; diff --git a/client/src/features/connection/useConnection.ts b/client/src/features/connection/useConnection.ts index b5cc6f6..433ab3b 100644 --- a/client/src/features/connection/useConnection.ts +++ b/client/src/features/connection/useConnection.ts @@ -1,4 +1,4 @@ -import { SID_KEY_NAME, socket } from "@/constants"; +import { PLAYER_ID_KEY_NAME, socket } from "@/constants"; import { useEffect } from "react"; import { connect, disconnect } from "./utils"; @@ -11,7 +11,7 @@ function useConnection(code: string | undefined) { socket.on("disconnect", () => console.log("Disconnected from server")); socket.on("conn_info_data", ({ playerId }) => { - localStorage.setItem(SID_KEY_NAME, playerId); + localStorage.setItem(PLAYER_ID_KEY_NAME, playerId); }); connect(code); diff --git a/client/src/features/connection/utils.ts b/client/src/features/connection/utils.ts index 1d4b851..d3f1be4 100644 --- a/client/src/features/connection/utils.ts +++ b/client/src/features/connection/utils.ts @@ -1,16 +1,14 @@ -import { SID_KEY_NAME, socket } from "@/constants"; +import { PLAYER_ID_KEY_NAME, socket } from "@/constants"; const connect = (roomCode: string) => { if (socket.connected) return; - const playerId = localStorage.getItem(SID_KEY_NAME); + const playerId = localStorage.getItem(PLAYER_ID_KEY_NAME); if (playerId) { socket.auth = { playerId }; } - console.log(`socket.auth: ${JSON.stringify(socket.auth)} playerId: ${playerId}`); - socket.io.opts.query = { roomCode }; socket.connect(); }; diff --git a/server/src/socketAuth.ts b/server/src/socketAuth.ts index 4f587f8..0e9df1e 100644 --- a/server/src/socketAuth.ts +++ b/server/src/socketAuth.ts @@ -20,37 +20,20 @@ export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) const room = manager.getRoom(code); if (room === undefined) return next(new Error(`Room ${code} does not exist`)); - console.log(room); - const playerId = socket.handshake.auth.playerId; - // const session = sessionStore.findSession(sessionId); - - //console.log(`server-side socketAuth. sid: ${sessionId}`); - console.log("hanshake id: " + playerId); - console.log(JSON.stringify(room.getPlayers())); // Join as old player if (room.hasPlayer(playerId)) { - console.log("JOIN AS OLD PLAYER"); socket.data = { playerId, roomCode: code }; return next(); } // Join as new player if (NON_STRICT_PHASES.includes(room.phase)) { - console.log("JOIN AS NEW PLAYER"); - // const newSessionId = manager.genSessionId(); const newPlayerId = manager.generatePlayerId(); - room.addPlayer(newPlayerId); //creates new player and assigns to room - // console.log(room.hasPlayer(newPlayerId)); - // sessionStore.saveSession(newSessionId, { - // playerId: newPlayerId, - //}); - // console.log(sessionStore.findSession(newSessionId)?.playerId === newPlayerId); + room.addPlayer(newPlayerId); socket.data = { playerId: newPlayerId, roomCode: code }; - console.log("room: " + JSON.stringify(room.getPlayers())); - return next(); } diff --git a/server/src/socketRoutes/index.ts b/server/src/socketRoutes/index.ts index 922a392..79d990b 100644 --- a/server/src/socketRoutes/index.ts +++ b/server/src/socketRoutes/index.ts @@ -7,10 +7,6 @@ export default function socketRoutes(socket: MASocket) { const room = manager.getRoom(socket.data.roomCode); const playerId = socket.data.playerId; - console.log("conn_info_data", { - sessionId: socket.data.playerId, - }); - socket.emit("conn_info_data", { playerId: socket.data.playerId, }); From dc58bb078e089153d4445eef9a9ed3cb9e85fca0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 12:56:25 +0100 Subject: [PATCH 11/19] refactor(RoomManager): generate player id using node:crypto --- server/src/RoomManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/RoomManager.ts b/server/src/RoomManager.ts index c6971dd..f5e76af 100644 --- a/server/src/RoomManager.ts +++ b/server/src/RoomManager.ts @@ -1,8 +1,8 @@ import { Room } from "./Room"; +import crypto from "node:crypto"; class RoomManager { rooms = new Map([["000000", new Room("000000")]]); - private playerIdCounter = 1; // unique 5 digits room code generateCode(): string { @@ -14,7 +14,7 @@ class RoomManager { } generatePlayerId(): string { - return (this.playerIdCounter++).toString(); + return crypto.randomUUID(); } create(): string { From 5689785fd0c587095e8a92df865c3f97652e137f Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 12:58:51 +0100 Subject: [PATCH 12/19] refactor(Room): change removePlayer method name to disconnectPlayer --- server/src/Room.ts | 2 +- server/src/socketRoutes/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/Room.ts b/server/src/Room.ts index 5cc37be..a515a67 100644 --- a/server/src/Room.ts +++ b/server/src/Room.ts @@ -14,7 +14,7 @@ export class Room { this.players.set(playerId, new Player(playerId)); } - removePlayer(playerId: string) { + disconnectPlayer(playerId: string) { if (NON_STRICT_PHASES.includes(this.phase)) { this.players.delete(playerId); } else { diff --git a/server/src/socketRoutes/index.ts b/server/src/socketRoutes/index.ts index 79d990b..b982841 100644 --- a/server/src/socketRoutes/index.ts +++ b/server/src/socketRoutes/index.ts @@ -18,6 +18,6 @@ export default function socketRoutes(socket: MASocket) { } socket.on("disconnect", () => { - room?.removePlayer(playerId); + room?.disconnectPlayer(playerId); }); } From b368a22b79825915ef1d99450f6b82f80eb7a28d Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 13:11:20 +0100 Subject: [PATCH 13/19] fix: remove err data from phase_updated in Server2ClientEvents --- client/src/pages/Game/index.tsx | 6 +----- server/src/socketRoutes/index.ts | 9 ++------- types/Sockets.ts | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/client/src/pages/Game/index.tsx b/client/src/pages/Game/index.tsx index d0c15d4..b4ef3f1 100644 --- a/client/src/pages/Game/index.tsx +++ b/client/src/pages/Game/index.tsx @@ -9,11 +9,7 @@ function Game() { const [phase, setPhase] = useState(Phases.GAME_END); useEffect(() => { - socket.on("phase_updated", ({ err, phase }) => { - if (err) { - console.error("Error in phase_updated occured: " + err); - return; - } + socket.on("phase_updated", (phase) => { setPhase(phase); }); }, [socket]); diff --git a/server/src/socketRoutes/index.ts b/server/src/socketRoutes/index.ts index b982841..3d8b0f1 100644 --- a/server/src/socketRoutes/index.ts +++ b/server/src/socketRoutes/index.ts @@ -1,21 +1,16 @@ import { MASocket } from "@local/Sockets"; import { manager } from "../RoomManager"; -import { NON_STRICT_PHASES } from "@global/Game"; // Here we set up the socket events for the client export default function socketRoutes(socket: MASocket) { - const room = manager.getRoom(socket.data.roomCode); + const room = manager.getRoom(socket.data.roomCode)!; const playerId = socket.data.playerId; socket.emit("conn_info_data", { playerId: socket.data.playerId, }); - if (!room) { - socket.emit("phase_updated", { err: "Room is not found", phase: NON_STRICT_PHASES[0] }); - } else { - socket.emit("phase_updated", { err: "", phase: room.phase }); - } + socket.emit("phase_updated", room.phase); socket.on("disconnect", () => { room?.disconnectPlayer(playerId); diff --git a/types/Sockets.ts b/types/Sockets.ts index ea30dd8..672978c 100644 --- a/types/Sockets.ts +++ b/types/Sockets.ts @@ -3,7 +3,7 @@ import type { Phases } from "./Game"; export interface Server2ClientEvents { rooms_data: (rooms: Array) => void; conn_info_data: (data: ConnectionInfoData) => void; - phase_updated: (data: { err: string; phase: Phases }) => void; + phase_updated: (phase: Phases) => void; } export type ConnectionInfoData = { playerId: string }; From ae9dd9adae6472cda69e2ddad061c6544f4fa234 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 13:38:24 +0100 Subject: [PATCH 14/19] refactor: organize files in server directory in the appropriate folders --- server/package.json | 2 +- server/{ => src/constants}/config.ts | 0 server/src/constants/index.ts | 4 ++ server/src/constants/manager.ts | 3 + server/{ => src}/index.ts | 12 ++-- server/src/middlewares/index.ts | 3 + server/src/{ => middlewares}/socketAuth.ts | 4 +- server/src/{ => models}/Player.ts | 0 server/src/{ => models}/Room.ts | 0 server/src/{ => models}/RoomManager.ts | 4 +- server/src/models/index.ts | 5 ++ server/src/routes/index.ts | 4 ++ .../index.ts => routes/socketRoutes.ts} | 4 +- server/src/{ => routes}/webRoutes.ts | 2 +- .../{types/Sockets.ts => src/types/index.ts} | 0 types/Roles.ts | 56 +++++++++---------- 16 files changed, 59 insertions(+), 44 deletions(-) rename server/{ => src/constants}/config.ts (100%) create mode 100644 server/src/constants/index.ts create mode 100644 server/src/constants/manager.ts rename server/{ => src}/index.ts (75%) create mode 100644 server/src/middlewares/index.ts rename server/src/{ => middlewares}/socketAuth.ts (93%) rename server/src/{ => models}/Player.ts (100%) rename server/src/{ => models}/Room.ts (100%) rename server/src/{ => models}/RoomManager.ts (91%) create mode 100644 server/src/models/index.ts create mode 100644 server/src/routes/index.ts rename server/src/{socketRoutes/index.ts => routes/socketRoutes.ts} (82%) rename server/src/{ => routes}/webRoutes.ts (93%) rename server/{types/Sockets.ts => src/types/index.ts} (100%) diff --git a/server/package.json b/server/package.json index 6ce321c..ba19074 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.ts", "scripts": { - "dev": "tsx watch index.ts" + "dev": "tsx watch src/index.ts" }, "author": "", "license": "ISC", diff --git a/server/config.ts b/server/src/constants/config.ts similarity index 100% rename from server/config.ts rename to server/src/constants/config.ts diff --git a/server/src/constants/index.ts b/server/src/constants/index.ts new file mode 100644 index 0000000..8f336c3 --- /dev/null +++ b/server/src/constants/index.ts @@ -0,0 +1,4 @@ +import { manager } from "./manager"; +import config from "./config"; + +export { manager, config }; diff --git a/server/src/constants/manager.ts b/server/src/constants/manager.ts new file mode 100644 index 0000000..debc02a --- /dev/null +++ b/server/src/constants/manager.ts @@ -0,0 +1,3 @@ +import { RoomManager } from "@/models"; + +export const manager = new RoomManager(); diff --git a/server/index.ts b/server/src/index.ts similarity index 75% rename from server/index.ts rename to server/src/index.ts index 18f463f..d0cdd50 100644 --- a/server/index.ts +++ b/server/src/index.ts @@ -2,12 +2,10 @@ import express from "express"; import http from "http"; import { Server } from "socket.io"; -import { MAServer } from "@local/Sockets"; -import config from "./config"; - -import socketAuth from "@/socketAuth"; -import socketRoutes from "@/socketRoutes"; -import webRoutes from "@/webRoutes"; +import { MAServer } from "@/types"; +import { config } from "@/constants"; +import { socketAuth } from "@/middlewares"; +import { socketRoutes, webRouter } from "@/routes"; const app = express(); // Create an Express application @@ -24,7 +22,7 @@ const socketsServer: MAServer = new Server(httpServer, { // Configure servers socketsServer.use(socketAuth).on("connection", socketRoutes); // Configure the WebSocket server -app.use("/", webRoutes); // Configure the HTTP server +app.use("/", webRouter); // Configure the HTTP server // Start servers httpServer.listen(config.PORT, () => { diff --git a/server/src/middlewares/index.ts b/server/src/middlewares/index.ts new file mode 100644 index 0000000..c8bb283 --- /dev/null +++ b/server/src/middlewares/index.ts @@ -0,0 +1,3 @@ +import socketAuth from "./socketAuth"; + +export { socketAuth }; diff --git a/server/src/socketAuth.ts b/server/src/middlewares/socketAuth.ts similarity index 93% rename from server/src/socketAuth.ts rename to server/src/middlewares/socketAuth.ts index 0e9df1e..8f0d1c5 100644 --- a/server/src/socketAuth.ts +++ b/server/src/middlewares/socketAuth.ts @@ -1,6 +1,6 @@ import { ExtendedError } from "socket.io"; -import { MASocket } from "@local/Sockets"; -import { manager } from "./RoomManager"; +import { MASocket } from "@/types"; +import { manager } from "@/constants"; import { NON_STRICT_PHASES } from "@global/Game"; /* diff --git a/server/src/Player.ts b/server/src/models/Player.ts similarity index 100% rename from server/src/Player.ts rename to server/src/models/Player.ts diff --git a/server/src/Room.ts b/server/src/models/Room.ts similarity index 100% rename from server/src/Room.ts rename to server/src/models/Room.ts diff --git a/server/src/RoomManager.ts b/server/src/models/RoomManager.ts similarity index 91% rename from server/src/RoomManager.ts rename to server/src/models/RoomManager.ts index f5e76af..b7e81d4 100644 --- a/server/src/RoomManager.ts +++ b/server/src/models/RoomManager.ts @@ -1,7 +1,7 @@ import { Room } from "./Room"; import crypto from "node:crypto"; -class RoomManager { +export class RoomManager { rooms = new Map([["000000", new Room("000000")]]); // unique 5 digits room code @@ -28,5 +28,3 @@ class RoomManager { return this.rooms.get(code); } } - -export const manager = new RoomManager(); diff --git a/server/src/models/index.ts b/server/src/models/index.ts new file mode 100644 index 0000000..8a9a007 --- /dev/null +++ b/server/src/models/index.ts @@ -0,0 +1,5 @@ +import { Player } from "./Player"; +import { Room } from "./Room"; +import { RoomManager } from "./RoomManager"; + +export { Player, Room, RoomManager }; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts new file mode 100644 index 0000000..b82bd20 --- /dev/null +++ b/server/src/routes/index.ts @@ -0,0 +1,4 @@ +import socketRoutes from "./socketRoutes"; +import webRouter from "./webRoutes"; + +export { socketRoutes, webRouter }; diff --git a/server/src/socketRoutes/index.ts b/server/src/routes/socketRoutes.ts similarity index 82% rename from server/src/socketRoutes/index.ts rename to server/src/routes/socketRoutes.ts index 3d8b0f1..d0c0d0e 100644 --- a/server/src/socketRoutes/index.ts +++ b/server/src/routes/socketRoutes.ts @@ -1,5 +1,5 @@ -import { MASocket } from "@local/Sockets"; -import { manager } from "../RoomManager"; +import { MASocket } from "@/types"; +import { manager } from "@/constants"; // Here we set up the socket events for the client export default function socketRoutes(socket: MASocket) { diff --git a/server/src/webRoutes.ts b/server/src/routes/webRoutes.ts similarity index 93% rename from server/src/webRoutes.ts rename to server/src/routes/webRoutes.ts index df6a0c2..1d9ea99 100644 --- a/server/src/webRoutes.ts +++ b/server/src/routes/webRoutes.ts @@ -1,5 +1,5 @@ import express from "express"; -import { manager } from "./RoomManager"; +import { manager } from "@/constants"; const webRouter = express.Router(); diff --git a/server/types/Sockets.ts b/server/src/types/index.ts similarity index 100% rename from server/types/Sockets.ts rename to server/src/types/index.ts diff --git a/types/Roles.ts b/types/Roles.ts index dfcb80f..fe58bd2 100644 --- a/types/Roles.ts +++ b/types/Roles.ts @@ -2,34 +2,34 @@ * Role są tajnymi funkcjami pełnionymi przez wybranych losowo graczy. * Przynależność danego gracza do wybranej roli nie jest powszechnie znana, poza wypisanymi niżej wyjątkami. */ -export enum Roles{ - /** - * Gracze pełniący rolę mafii mogą głosować w trakcie fazy MAFIA_VOTING za eliminacją wybranego obywatela. - * Przynależność do mafii jest tajna poza jej kręgiem członków. - * Rola jest pełniona grupowo. - */ - MAFIOSO = "MAFIOSO", +export enum Roles { + /** + * Gracze pełniący rolę mafii mogą głosować w trakcie fazy MAFIA_VOTING za eliminacją wybranego obywatela. + * Przynależność do mafii jest tajna poza jej kręgiem członków. + * Rola jest pełniona grupowo. + */ + MAFIOSO = "MAFIOSO", - /** - * Detektyw posiada możliwość sprawdzenia tożsamości danego gracza (jego roli) w fazie DETECTIVE_CHECK. - * Nie posiada on jednak ważniejszych zdolności egzekucyjnych poza publiczną debatą (DEBATE) i publicznym głosowaniem (VOTING). - * Z oczywistych względów narażony na ataki mafii w przypadku wyjawienia tożsamości. - * Rola jest pełniona indywidualnie. - */ - DETECTIVE = "DETECTIVE", + /** + * Detektyw posiada możliwość sprawdzenia tożsamości danego gracza (jego roli) w fazie DETECTIVE_CHECK. + * Nie posiada on jednak ważniejszych zdolności egzekucyjnych poza publiczną debatą (DEBATE) i publicznym głosowaniem (VOTING). + * Z oczywistych względów narażony na ataki mafii w przypadku wyjawienia tożsamości. + * Rola jest pełniona indywidualnie. + */ + DETECTIVE = "DETECTIVE", - /** - * Ochroniarz w trakcie fazy BODYGUARD_DEFENSE wybiera jedną osobę, którą chroni przed atakiem mafii. - * Może on także wybrać siebie. - * Rola jest pełniona indywidualnie. - */ - BODYGUARD = "BODYGUARD", + /** + * Ochroniarz w trakcie fazy BODYGUARD_DEFENSE wybiera jedną osobę, którą chroni przed atakiem mafii. + * Może on także wybrać siebie. + * Rola jest pełniona indywidualnie. + */ + BODYGUARD = "BODYGUARD", - /** - * Każdy z graczy ma możliwość uczestniczenia w standardowych fazach dnia (DAY), debaty (DEBATE), głosowania (VOTING, VOTING_OVERTIME). - * - * Rola jest pełniona grupowo i jest domyślnym wariantem, - * której funkcje spełniają także gracze przypisani do wyżej wymienionych ról. - */ - REGULAR_CITIZEN = "REGULAR_CITIZEN" -}; \ No newline at end of file + /** + * Każdy z graczy ma możliwość uczestniczenia w standardowych fazach dnia (DAY), debaty (DEBATE), głosowania (VOTING, VOTING_OVERTIME). + * + * Rola jest pełniona grupowo i jest domyślnym wariantem, + * której funkcje spełniają także gracze przypisani do wyżej wymienionych ról. + */ + REGULAR_CITIZEN = "REGULAR_CITIZEN", +} From 72f5a36a152d5d70afbf03b7d1493d3620290869 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 13:44:43 +0100 Subject: [PATCH 15/19] fix(package.json): use concurrently for instDeps script --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 896f036..ee5525a 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "scripts": { "client:dev": "npm --prefix ./client run dev", "server:dev": "npm --prefix ./server run dev", - "all:dev": "npx concurrently \"npm run client:dev\" \"npm run server:dev\"", + "all:dev": "concurrently \"npm run client:dev\" \"npm run server:dev\"", "format:front": "prettier --write \"./client/src/**/*.{js,jsx,ts,tsx}\"", "format:back": "prettier --write \"./server/src/**/*.{js,jsx,ts,tsx}\"", - "instDeps": "npm i & (cd server && npm i) & (cd ../client && npm i)" + "instDeps": "concurrently \"npm i\" \"cd server && npm i\" \"cd client && npm i\"" }, "repository": { "type": "git", From 5c18ca381a565ea55d291bd12ed0f60d2ee6356a Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 11 Jan 2025 14:39:54 +0100 Subject: [PATCH 16/19] fix(Character): remove unnecessary code that was used for testing --- client/src/pages/Lobby/Character.tsx | 13 +------------ server/src/models/Player.ts | 1 + server/src/routes/socketRoutes.ts | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/client/src/pages/Lobby/Character.tsx b/client/src/pages/Lobby/Character.tsx index f26b26e..462477f 100644 --- a/client/src/pages/Lobby/Character.tsx +++ b/client/src/pages/Lobby/Character.tsx @@ -1,9 +1,6 @@ import { Button, Input } from "@/components"; -import { ConnContext } from "@/features/connection"; -import { useContext } from "react"; function Character() { - const { disconnect, connect } = useContext(ConnContext); return (
e.preventDefault()}>
@@ -50,15 +47,7 @@ function Character() {
- +
); } diff --git a/server/src/models/Player.ts b/server/src/models/Player.ts index ad602dc..6ebe841 100644 --- a/server/src/models/Player.ts +++ b/server/src/models/Player.ts @@ -9,6 +9,7 @@ export class Player { constructor( public id: string // Unique identifier for the player + //TODO: handle player name on client and server // public name: string // Real name of the player ) {} } diff --git a/server/src/routes/socketRoutes.ts b/server/src/routes/socketRoutes.ts index d0c0d0e..5145e8c 100644 --- a/server/src/routes/socketRoutes.ts +++ b/server/src/routes/socketRoutes.ts @@ -3,6 +3,7 @@ import { manager } from "@/constants"; // Here we set up the socket events for the client export default function socketRoutes(socket: MASocket) { + //TODO: Should player be assigned to socket room too? const room = manager.getRoom(socket.data.roomCode)!; const playerId = socket.data.playerId; From 8f12cf30316cfdd55ecedd839088299552915123 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Fri, 14 Feb 2025 22:31:12 +0100 Subject: [PATCH 17/19] feat: add modal component --- client/src/components/Button/index.tsx | 4 +- client/src/components/Input/index.tsx | 10 ++-- client/src/components/Modal/index.tsx | 61 +++++++++++++++++++++++ client/src/components/index.ts | 3 +- client/src/pages/Lobby/index.tsx | 69 ++++++++++++++++---------- 5 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 client/src/components/Modal/index.tsx diff --git a/client/src/components/Button/index.tsx b/client/src/components/Button/index.tsx index 7ec39ea..ab564b4 100644 --- a/client/src/components/Button/index.tsx +++ b/client/src/components/Button/index.tsx @@ -22,10 +22,12 @@ function Button({ "rounded-md font-bold", variant, size, - disabled && "opacity-70", + disabled && "opacity-60", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-black", customClassName )} + disabled={disabled} + aria-disabled={disabled} type="button" {...rest} > diff --git a/client/src/components/Input/index.tsx b/client/src/components/Input/index.tsx index c0ba678..cff54a0 100644 --- a/client/src/components/Input/index.tsx +++ b/client/src/components/Input/index.tsx @@ -1,14 +1,16 @@ +import clsx from "clsx"; import React, { forwardRef } from "react"; type InputProps = React.InputHTMLAttributes; -const Input = forwardRef(({ ...props }, ref) => ( +const Input = forwardRef(({ className, ...props }, ref) => ( )); diff --git a/client/src/components/Modal/index.tsx b/client/src/components/Modal/index.tsx new file mode 100644 index 0000000..054f62c --- /dev/null +++ b/client/src/components/Modal/index.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useRef } from "react"; + +interface ModalProps { + onClose?: () => void; + children: React.ReactNode; + showCloseIcon?: boolean; + canBeDismissed?: boolean; + isOpened: boolean; +} + +function Modal({ onClose, showCloseIcon = true, canBeDismissed = true, children, isOpened }: ModalProps) { + const dialogRef = useRef(null); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape" && !canBeDismissed) e.preventDefault(); + }; + + const handleModalClose = () => { + onClose?.(); + dialogRef.current?.close(); + }; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget && canBeDismissed) { + handleModalClose(); + } + }; + + useEffect(() => { + if (isOpened) { + dialogRef.current?.showModal(); + } else { + dialogRef.current?.close(); + } + }, [isOpened]); + + return ( + +
+ {showCloseIcon && ( + + )} + {children} +
+
+ ); +} + +export default Modal; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 088ea42..5c4f658 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -1,4 +1,5 @@ import Button from "./Button"; import Input from "./Input"; +import Modal from "./Modal"; -export { Button, Input }; +export { Button, Input, Modal }; diff --git a/client/src/pages/Lobby/index.tsx b/client/src/pages/Lobby/index.tsx index 51e4a68..8939d48 100644 --- a/client/src/pages/Lobby/index.tsx +++ b/client/src/pages/Lobby/index.tsx @@ -3,6 +3,7 @@ import { useCallback, useState } from "react"; import Character from "./Character"; import Seat from "./Seat"; import useKeyDown from "@/hooks/useKeyDown"; +import { Button, Input, Modal } from "@/components"; enum Panels { Character = "Character", @@ -54,35 +55,49 @@ function Lobby() { }); }; - return ( -
-
-

MafiAKAI

- -
Options
-
+ const [playerName, setPlayerName] = useState(""); + const [isModalOpened, setIsModalOpened] = useState(true); + const playerNameLength = playerName?.trim().length; -
- {Object.values(Panels).map((panelName, i) => ( - - ))} -
- -
- {panelsValues[panelId] === Panels.Seat ? ( - - ) : ( - <> - )} - {panelsValues[panelId] === Panels.Character ? : <>} + return ( + <> + +

Enter your name

+

This will help other players bind your name with your character.

+ setPlayerName(e.target.value)} className="my-2" /> + +
+ +
+
+

MafiAKAI

+
Options
+
+ +
+ {Object.values(Panels).map((panelName, i) => ( + + ))} +
+ +
+ {panelsValues[panelId] === Panels.Seat ? ( + + ) : ( + <> + )} + {panelsValues[panelId] === Panels.Character ? : <>} +
-
+ ); } From 808a164e7b410911946c61adcc057fa63061af94 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Fri, 14 Feb 2025 23:10:49 +0100 Subject: [PATCH 18/19] feat: emit player name to backend --- client/src/pages/Lobby/index.tsx | 10 +++++++++- server/src/middlewares/socketAuth.ts | 3 +-- server/src/models/Player.ts | 3 +-- server/src/routes/socketRoutes.ts | 4 ++++ types/Sockets.ts | 1 + 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/client/src/pages/Lobby/index.tsx b/client/src/pages/Lobby/index.tsx index 8939d48..e5e59e4 100644 --- a/client/src/pages/Lobby/index.tsx +++ b/client/src/pages/Lobby/index.tsx @@ -4,6 +4,7 @@ import Character from "./Character"; import Seat from "./Seat"; import useKeyDown from "@/hooks/useKeyDown"; import { Button, Input, Modal } from "@/components"; +import { socket } from "@/constants"; enum Panels { Character = "Character", @@ -59,13 +60,20 @@ function Lobby() { const [isModalOpened, setIsModalOpened] = useState(true); const playerNameLength = playerName?.trim().length; + const handleNameConfirmation = () => { + if (playerNameLength > 1) { + socket.emit("send_player_name", playerName); + setIsModalOpened(false); + } + }; + return ( <>

Enter your name

This will help other players bind your name with your character.

setPlayerName(e.target.value)} className="my-2" /> -
diff --git a/server/src/middlewares/socketAuth.ts b/server/src/middlewares/socketAuth.ts index 8f0d1c5..a384d1d 100644 --- a/server/src/middlewares/socketAuth.ts +++ b/server/src/middlewares/socketAuth.ts @@ -7,8 +7,7 @@ import { NON_STRICT_PHASES } from "@global/Game"; Validate connection and Join player to room To establish a connection, the client must provide: - code - the room code - - name - the player name - - id - old player id or 0 for new player + - id - old player id or undefined */ export default function socketAuth(socket: MASocket, next: (err?: ExtendedError) => void) { // Code diff --git a/server/src/models/Player.ts b/server/src/models/Player.ts index 6ebe841..707695d 100644 --- a/server/src/models/Player.ts +++ b/server/src/models/Player.ts @@ -6,10 +6,9 @@ export class Player { persona: Persona = {}; role: Roles | null = null; seat: number | null = null; + name: string | null = null; // Real name of the player constructor( public id: string // Unique identifier for the player - //TODO: handle player name on client and server - // public name: string // Real name of the player ) {} } diff --git a/server/src/routes/socketRoutes.ts b/server/src/routes/socketRoutes.ts index 5145e8c..e1a239b 100644 --- a/server/src/routes/socketRoutes.ts +++ b/server/src/routes/socketRoutes.ts @@ -16,4 +16,8 @@ export default function socketRoutes(socket: MASocket) { socket.on("disconnect", () => { room?.disconnectPlayer(playerId); }); + + socket.on("send_player_name", (playerName) => { + room.getPlayer(playerId)!.name = playerName; + }); } diff --git a/types/Sockets.ts b/types/Sockets.ts index 672978c..fe9ecbe 100644 --- a/types/Sockets.ts +++ b/types/Sockets.ts @@ -12,6 +12,7 @@ export type ResponseHandler = (message: string) => void; export interface Client2ServerEvents { setPosition: (position: number, callback: ResponseHandler) => void; vote: (data: string) => void; + send_player_name: (playerName: string) => void; } export interface InterServerEvents { From 2981208c0f043e68cf81e1fa90778641ed530492 Mon Sep 17 00:00:00 2001 From: Aleksander Nowicki Date: Sat, 15 Feb 2025 13:33:29 +0100 Subject: [PATCH 19/19] refactor: move clsx to client package & minor modal style change --- client/package-lock.json | 10 ++++++++++ client/package.json | 1 + client/src/components/Modal/index.tsx | 4 ++-- package-lock.json | 10 ---------- package.json | 1 - 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 41060e8..67c312f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "mafiakai-client", "version": "0.0.0", "dependencies": { + "clsx": "^2.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.0.2", @@ -1886,6 +1887,15 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/client/package.json b/client/package.json index fc444a0..39f386c 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.0.2", diff --git a/client/src/components/Modal/index.tsx b/client/src/components/Modal/index.tsx index 054f62c..1bf610d 100644 --- a/client/src/components/Modal/index.tsx +++ b/client/src/components/Modal/index.tsx @@ -39,9 +39,9 @@ function Modal({ onClose, showCloseIcon = true, canBeDismissed = true, children, ref={dialogRef} onKeyDown={handleKeyDown} onClick={handleBackdropClick} - className="rounded-md backdrop:bg-black/70 backdrop:backdrop-blur-sm" + className="mx-2 max-w-lg rounded-md bg-white backdrop:bg-black/70 backdrop:backdrop-blur-sm sm:mx-auto" > -
+
{showCloseIcon && (