From de519ffce030a9e876aeefd596b28b62cfa82e80 Mon Sep 17 00:00:00 2001 From: Harukume Date: Fri, 24 Apr 2026 15:20:58 +0200 Subject: [PATCH 1/7] feat: add matchmaking logic for private and public games --- client/src/App.tsx | 2 + client/src/lib/room-provider.tsx | 50 +++++++-- client/src/lib/use-room.ts | 9 +- client/src/pages/game.tsx | 4 + client/src/pages/start.tsx | 187 +++++++++++++++++++++++++++++++ server/src/app.config.ts | 1 + server/src/rooms/GameRoom.ts | 25 ++++- 7 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 client/src/pages/start.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 48d3885..5699da3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import { Creator } from "./pages/creator"; import { Game } from "./pages/game"; import { Intro } from "./pages/intro"; import { Playground } from "./pages/playground"; +import { Start } from "./pages/start"; export function App() { return ( @@ -12,6 +13,7 @@ export function App() { } /> + } /> } /> } /> } /> diff --git a/client/src/lib/room-provider.tsx b/client/src/lib/room-provider.tsx index 0152158..0c51768 100644 --- a/client/src/lib/room-provider.tsx +++ b/client/src/lib/room-provider.tsx @@ -3,6 +3,7 @@ import type { Room } from "colyseus.js"; import React, { useEffect, useState } from "react"; import { RoomContext } from "./use-room"; +import type { ConnectOptions } from "./use-room"; const client = new Client("ws://localhost:2567"); @@ -19,20 +20,45 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { const [isConnected, setIsConnected] = useState(false); const [joinError, setJoinError] = useState(false); - const connect = async (playerName: string) => { + const connect = async ({ + playerName, + mode, + roomCode, + isPrivate, + }: ConnectOptions) => { try { - const newRoom = await client.joinOrCreate("game_room", { - name: playerName, - }); - setRoom(newRoom); + setJoinError(false); + let joinedRoom: Room; + + console.warn({playerName, + mode, + roomCode, + isPrivate,}); + + if (mode === "create") { + joinedRoom = await client.create("game_room", { + name: playerName, + isPrivate, + }); + } else if ( + mode === "join" && + roomCode !== undefined && + roomCode.length > 0 + ) { + joinedRoom = await client.joinById(roomCode, { + name: playerName, + //isPrivate: true, + }); + } else { + joinedRoom = await client.joinOrCreate("game_room", { + name: playerName, + isPrivate: false, + }); + } + + setRoom(joinedRoom); setIsConnected(true); - localStorage.setItem( - "reconnection", - JSON.stringify({ - token: newRoom.reconnectionToken, - playerName, - }), - ); + } catch (error) { console.error("Join error", error); setJoinError(true); diff --git a/client/src/lib/use-room.ts b/client/src/lib/use-room.ts index 49f1540..fa9f3cb 100644 --- a/client/src/lib/use-room.ts +++ b/client/src/lib/use-room.ts @@ -1,11 +1,18 @@ import type { Room } from "colyseus.js"; import { createContext, useContext } from "react"; +export interface ConnectOptions { + playerName: string; + mode: "join" | "create" | "random"; + roomCode?: string; + isPrivate?: boolean; +} + interface RoomContextType { room: Room | null; isConnected: boolean; joinError: boolean; - connect: (playerName: string) => Promise; + connect: (options: ConnectOptions) => Promise; disconnect: () => Promise; } diff --git a/client/src/pages/game.tsx b/client/src/pages/game.tsx index 9534200..de8b4af 100644 --- a/client/src/pages/game.tsx +++ b/client/src/pages/game.tsx @@ -54,6 +54,10 @@ export function Game() { return (
+
+ KOD POKOJU:{" "} + {room.roomId} +
); } diff --git a/client/src/pages/start.tsx b/client/src/pages/start.tsx new file mode 100644 index 0000000..83b0622 --- /dev/null +++ b/client/src/pages/start.tsx @@ -0,0 +1,187 @@ +import { useCallback, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { Button } from "../components/button"; +import { ErrorContainer } from "../components/error-container"; +import { IntroContainer } from "../components/intro-container"; +import { TitleHeader } from "../components/title-header"; +import { useRoom } from "../lib/use-room"; +import { CustomInput } from "../components/custom-input"; + +export function Start() { + const navigate = useNavigate(); + const { connect, disconnect } = useRoom(); + + const [status, setStatus] = useState< + "idle" | "loading" | "error" | "success" | "reconnecting" + >(() => { + return localStorage.getItem("reconnection") === null + ? "idle" + : "reconnecting"; + }); + + const [name, setName] = useState(""); + const [mode, setMode] = useState<"join" | "create">("join"); + const [roomCode, setRoomCode] = useState(""); + const [isPrivate, setIsPrivate] = useState(false); + + const [countdown, setCountdown] = useState(3); + const [errorMessage, setErrorMessage] = useState(""); + + const handlePlay = useCallback(async () => { + if (name.trim() === "") { + setErrorMessage("Nazwa gracza nie może być pusta."); + setStatus("error"); + return; + } + + setStatus("loading"); + try { + await connect({ + playerName: name.trim(), + mode, + roomCode: mode === "join" ? roomCode.trim() : undefined, + isPrivate: mode === "create" ? isPrivate : undefined, + }); + await navigate("/game"); + } catch { + setErrorMessage("Nie udało się dołaczyć do gry. Spróbuj ponownie."); + setStatus("error"); + } + }, [connect, isPrivate, mode, name, navigate, roomCode]); + + useEffect(() => { + if (status !== "reconnecting") { + return; + } + + let remaining = 3; + setCountdown(remaining); + + const timerId = window.setInterval(() => { + remaining -= 1; + setCountdown(remaining); + + if (remaining > 0) { + return; + } + + window.clearInterval(timerId); + + const cachedReconnection = localStorage.getItem("reconnection"); + if (cachedReconnection === null) { + setStatus("idle"); + } else { + void navigate("/game"); + } + }, 1000); + + return () => { + window.clearInterval(timerId); + }; + }, [status, navigate]); + + useEffect(() => { + if (status === "reconnecting") { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && status !== "loading") { + void handlePlay(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handlePlay, status]); + + const handleCancelReconnection = async () => { + localStorage.removeItem("reconnection"); + await disconnect(); + setStatus("idle"); + setCountdown(3); + }; + + return ( + + + + {status === "reconnecting" ? ( +
+

+ Próba ponownego połączenia za {countdown}... +
+

+ +
+ ) : ( + <> +
+ + +
+ { + setName(value.toUpperCase()); + }} + disabled={status === "loading"} + /> + {mode === "join" ? ( + { + setRoomCode(error.target.value); + }} + /> + ) : ( + + )} + + + + )} + + {status === "error" && } +
+ ); +} diff --git a/server/src/app.config.ts b/server/src/app.config.ts index 518a729..b5bf75e 100644 --- a/server/src/app.config.ts +++ b/server/src/app.config.ts @@ -13,6 +13,7 @@ export default config({ * Define your room handlers: */ gameServer.define("game_room", GameRoom); + // .filterBy(['isPrivate']); }, initializeExpress: (app) => { diff --git a/server/src/rooms/GameRoom.ts b/server/src/rooms/GameRoom.ts index 7213f88..809424e 100644 --- a/server/src/rooms/GameRoom.ts +++ b/server/src/rooms/GameRoom.ts @@ -7,11 +7,20 @@ import { SpeechBubble } from "../speech-bubbles/SpeechBubble"; // import room from "./json/examples/room3.json"; export class GameRoom extends Room { - maxClients = 4; + maxClients = 2; state = new RoomState(); onCreate(options: any) { + this.setMetadata({ + isPrivate: !!options.isPrivate + }); + this.maxClients = room.maxClients ?? this.maxClients; + + if (options.private) { + this.setPrivate(true); + } + this.state.loadRoomFromJson(room); this.onMessage("move", (client, message) => { const player = this.state.playerState.players.get(client.sessionId); @@ -115,12 +124,22 @@ export class GameRoom extends Room { } onJoin(client: Client, options: any) { - this.state.spawnNewPlayer(client.sessionId, options.name); + const nickname = options.name || `Capy_${client.sessionId.slice(0, 3)}`; + + this.state.spawnNewPlayer(client.sessionId, nickname); + const player = this.state.playerState.players.get(client.sessionId); + // Player index 0 = Sol + // Player index 1 = Vron + const character = player.index === 0 ? "Sol" : "Vron"; + + console.log(`Gracz ${player.name} dołączył jako ${player.index === 0 ? "Sol" : "Vron"}`); + this.broadcast("onAddPlayer", { sessionId: client.sessionId, playerName: player.name, + character: character, position: player.position, index: player.index, }); @@ -134,7 +153,7 @@ export class GameRoom extends Room { } // allow disconnected client to reconnect into this room until 20 seconds - await this.allowReconnection(client, 0); + await this.allowReconnection(client, 20); } catch (e) { this.broadcast("onRemovePlayer", { sessionId: client.sessionId, From 9bd845fb051fa7b128e5dbbc4e51b0d21b19abd0 Mon Sep 17 00:00:00 2001 From: Harukume Date: Fri, 24 Apr 2026 15:20:58 +0200 Subject: [PATCH 2/7] feat: add matchmaking logic for private and public games --- client/src/App.tsx | 2 + client/src/lib/room-provider.tsx | 50 +++++++-- client/src/lib/use-room.ts | 9 +- client/src/pages/game.tsx | 4 + client/src/pages/start.tsx | 187 +++++++++++++++++++++++++++++++ server/src/app.config.ts | 1 + server/src/rooms/GameRoom.ts | 25 ++++- 7 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 client/src/pages/start.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 691dd5d..dce28ca 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,6 +4,7 @@ import { Creator } from "./pages/creator"; import { Game } from "./pages/game"; import { Intro } from "./pages/intro"; import { Playground } from "./pages/playground"; +import { Start } from "./pages/start"; export function App() { return ( @@ -11,6 +12,7 @@ export function App() { } /> + } /> } /> } /> } /> diff --git a/client/src/lib/room-provider.tsx b/client/src/lib/room-provider.tsx index 5948296..7fdecfb 100644 --- a/client/src/lib/room-provider.tsx +++ b/client/src/lib/room-provider.tsx @@ -3,6 +3,7 @@ import type { Room } from "colyseus.js"; import React, { useEffect, useState } from "react"; import { RoomContext } from "./use-room"; +import type { ConnectOptions } from "./use-room"; const host = window.location.hostname; @@ -23,20 +24,45 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { const [isConnected, setIsConnected] = useState(false); const [joinError, setJoinError] = useState(false); - const connect = async (playerName: string) => { + const connect = async ({ + playerName, + mode, + roomCode, + isPrivate, + }: ConnectOptions) => { try { - const newRoom = await client.joinOrCreate("game_room", { - name: playerName, - }); - setRoom(newRoom); + setJoinError(false); + let joinedRoom: Room; + + console.warn({playerName, + mode, + roomCode, + isPrivate,}); + + if (mode === "create") { + joinedRoom = await client.create("game_room", { + name: playerName, + isPrivate, + }); + } else if ( + mode === "join" && + roomCode !== undefined && + roomCode.length > 0 + ) { + joinedRoom = await client.joinById(roomCode, { + name: playerName, + //isPrivate: true, + }); + } else { + joinedRoom = await client.joinOrCreate("game_room", { + name: playerName, + isPrivate: false, + }); + } + + setRoom(joinedRoom); setIsConnected(true); - localStorage.setItem( - "reconnection", - JSON.stringify({ - token: newRoom.reconnectionToken, - playerName, - }), - ); + } catch (error) { console.error("Join error", error); setJoinError(true); diff --git a/client/src/lib/use-room.ts b/client/src/lib/use-room.ts index 49f1540..fa9f3cb 100644 --- a/client/src/lib/use-room.ts +++ b/client/src/lib/use-room.ts @@ -1,11 +1,18 @@ import type { Room } from "colyseus.js"; import { createContext, useContext } from "react"; +export interface ConnectOptions { + playerName: string; + mode: "join" | "create" | "random"; + roomCode?: string; + isPrivate?: boolean; +} + interface RoomContextType { room: Room | null; isConnected: boolean; joinError: boolean; - connect: (playerName: string) => Promise; + connect: (options: ConnectOptions) => Promise; disconnect: () => Promise; } diff --git a/client/src/pages/game.tsx b/client/src/pages/game.tsx index 9534200..de8b4af 100644 --- a/client/src/pages/game.tsx +++ b/client/src/pages/game.tsx @@ -54,6 +54,10 @@ export function Game() { return (
+
+ KOD POKOJU:{" "} + {room.roomId} +
); } diff --git a/client/src/pages/start.tsx b/client/src/pages/start.tsx new file mode 100644 index 0000000..83b0622 --- /dev/null +++ b/client/src/pages/start.tsx @@ -0,0 +1,187 @@ +import { useCallback, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { Button } from "../components/button"; +import { ErrorContainer } from "../components/error-container"; +import { IntroContainer } from "../components/intro-container"; +import { TitleHeader } from "../components/title-header"; +import { useRoom } from "../lib/use-room"; +import { CustomInput } from "../components/custom-input"; + +export function Start() { + const navigate = useNavigate(); + const { connect, disconnect } = useRoom(); + + const [status, setStatus] = useState< + "idle" | "loading" | "error" | "success" | "reconnecting" + >(() => { + return localStorage.getItem("reconnection") === null + ? "idle" + : "reconnecting"; + }); + + const [name, setName] = useState(""); + const [mode, setMode] = useState<"join" | "create">("join"); + const [roomCode, setRoomCode] = useState(""); + const [isPrivate, setIsPrivate] = useState(false); + + const [countdown, setCountdown] = useState(3); + const [errorMessage, setErrorMessage] = useState(""); + + const handlePlay = useCallback(async () => { + if (name.trim() === "") { + setErrorMessage("Nazwa gracza nie może być pusta."); + setStatus("error"); + return; + } + + setStatus("loading"); + try { + await connect({ + playerName: name.trim(), + mode, + roomCode: mode === "join" ? roomCode.trim() : undefined, + isPrivate: mode === "create" ? isPrivate : undefined, + }); + await navigate("/game"); + } catch { + setErrorMessage("Nie udało się dołaczyć do gry. Spróbuj ponownie."); + setStatus("error"); + } + }, [connect, isPrivate, mode, name, navigate, roomCode]); + + useEffect(() => { + if (status !== "reconnecting") { + return; + } + + let remaining = 3; + setCountdown(remaining); + + const timerId = window.setInterval(() => { + remaining -= 1; + setCountdown(remaining); + + if (remaining > 0) { + return; + } + + window.clearInterval(timerId); + + const cachedReconnection = localStorage.getItem("reconnection"); + if (cachedReconnection === null) { + setStatus("idle"); + } else { + void navigate("/game"); + } + }, 1000); + + return () => { + window.clearInterval(timerId); + }; + }, [status, navigate]); + + useEffect(() => { + if (status === "reconnecting") { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && status !== "loading") { + void handlePlay(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handlePlay, status]); + + const handleCancelReconnection = async () => { + localStorage.removeItem("reconnection"); + await disconnect(); + setStatus("idle"); + setCountdown(3); + }; + + return ( + + + + {status === "reconnecting" ? ( +
+

+ Próba ponownego połączenia za {countdown}... +
+

+ +
+ ) : ( + <> +
+ + +
+ { + setName(value.toUpperCase()); + }} + disabled={status === "loading"} + /> + {mode === "join" ? ( + { + setRoomCode(error.target.value); + }} + /> + ) : ( + + )} + + + + )} + + {status === "error" && } +
+ ); +} diff --git a/server/src/app.config.ts b/server/src/app.config.ts index 03c6d21..64176e3 100644 --- a/server/src/app.config.ts +++ b/server/src/app.config.ts @@ -13,6 +13,7 @@ export default config({ * Define your room handlers: */ gameServer.define("game_room", GameRoom); + // .filterBy(['isPrivate']); }, initializeExpress: (app) => { diff --git a/server/src/rooms/GameRoom.ts b/server/src/rooms/GameRoom.ts index 13b28e6..399fe06 100644 --- a/server/src/rooms/GameRoom.ts +++ b/server/src/rooms/GameRoom.ts @@ -9,11 +9,20 @@ import { RoomState } from "./schema/RoomState"; // import room from "./json/examples/room3.json"; export class GameRoom extends Room { - maxClients = 4; + maxClients = 2; state = new RoomState(); onCreate(options: any) { + this.setMetadata({ + isPrivate: !!options.isPrivate + }); + this.maxClients = room.maxClients ?? this.maxClients; + + if (options.private) { + this.setPrivate(true); + } + this.state.loadRoomFromJson(room); this.onMessage("move", (client, message) => { const player = this.state.playerState.players.get(client.sessionId); @@ -117,12 +126,22 @@ export class GameRoom extends Room { } onJoin(client: Client, options: any) { - this.state.spawnNewPlayer(client.sessionId, options.name); + const nickname = options.name || `Capy_${client.sessionId.slice(0, 3)}`; + + this.state.spawnNewPlayer(client.sessionId, nickname); + const player = this.state.playerState.players.get(client.sessionId); + // Player index 0 = Sol + // Player index 1 = Vron + const character = player.index === 0 ? "Sol" : "Vron"; + + console.log(`Gracz ${player.name} dołączył jako ${player.index === 0 ? "Sol" : "Vron"}`); + this.broadcast("onAddPlayer", { sessionId: client.sessionId, playerName: player.name, + character: character, position: player.position, index: player.index, }); @@ -136,7 +155,7 @@ export class GameRoom extends Room { } // allow disconnected client to reconnect into this room until 20 seconds - await this.allowReconnection(client, 0); + await this.allowReconnection(client, 20); } catch (e) { this.broadcast("onRemovePlayer", { sessionId: client.sessionId, From 8b64149827cd2d6449479cb7e9bf32e5fd6bf5e8 Mon Sep 17 00:00:00 2001 From: Harukume Date: Sat, 25 Apr 2026 21:40:58 +0200 Subject: [PATCH 3/7] feat: add lobby view with player ready system --- client/src/App.tsx | 2 + client/src/lib/room-provider.tsx | 11 +- client/src/pages/lobby.tsx | 64 ++++++ client/src/pages/start.tsx | 6 +- server/package-lock.json | 282 ------------------------- server/src/rooms/GameRoom.ts | 52 +++-- server/src/rooms/schema/Player.ts | 3 +- server/src/rooms/schema/PlayerState.ts | 5 + server/src/rooms/schema/RoomState.ts | 13 +- server/test/GameRoom_test.ts | 28 ++- 10 files changed, 145 insertions(+), 321 deletions(-) create mode 100644 client/src/pages/lobby.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index dce28ca..bc57f3c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,6 +3,7 @@ import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; import { Creator } from "./pages/creator"; import { Game } from "./pages/game"; import { Intro } from "./pages/intro"; +import { Lobby } from "./pages/lobby"; import { Playground } from "./pages/playground"; import { Start } from "./pages/start"; @@ -13,6 +14,7 @@ export function App() { } /> } /> + } /> } /> } /> } /> diff --git a/client/src/lib/room-provider.tsx b/client/src/lib/room-provider.tsx index 7fdecfb..fadc1f6 100644 --- a/client/src/lib/room-provider.tsx +++ b/client/src/lib/room-provider.tsx @@ -34,11 +34,7 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { setJoinError(false); let joinedRoom: Room; - console.warn({playerName, - mode, - roomCode, - isPrivate,}); - + console.warn({ playerName, mode, roomCode, isPrivate }); if (mode === "create") { joinedRoom = await client.create("game_room", { name: playerName, @@ -55,14 +51,13 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { }); } else { joinedRoom = await client.joinOrCreate("game_room", { - name: playerName, + name: playerName, isPrivate: false, }); } - + setRoom(joinedRoom); setIsConnected(true); - } catch (error) { console.error("Join error", error); setJoinError(true); diff --git a/client/src/pages/lobby.tsx b/client/src/pages/lobby.tsx new file mode 100644 index 0000000..555cd17 --- /dev/null +++ b/client/src/pages/lobby.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useRoom } from "../lib/use-room"; + +export function Lobby() { + const { room, isConnected } = useRoom(); + const navigate = useNavigate(); + const [players, setPlayers] = useState([]); + + useEffect(() => { + if (!room) return; + + const handleStateChange = (state: any) => { + if (state.playerState?.players) { + setPlayers(Array.from(state.playerState.players.values())); + } + + if (state.gameStarted) { + navigate("/game"); + } + }; + + room.onStateChange(handleStateChange); + + handleStateChange(room.state); + + return () => { + room.onStateChange.remove(handleStateChange); + }; + }, [room, navigate]); + + if (!isConnected || !room) return
Łączenie...
; + + const myPlayer = players.find((p) => p.sessionId === room.sessionId); + + return ( +
+

Pokój: {room.roomId}

+ +
    + {players.map((p) => ( +
  • + {p.name || "Anonim"} - Postać:{" "} + {p.index === 0 ? "Sol" : "Vron"} - + {p.ready ? " ✅ GOTOWY" : " ❌ CZEKA"} +
  • + ))} +
+ + +
+ ); +} diff --git a/client/src/pages/start.tsx b/client/src/pages/start.tsx index 83b0622..1d61319 100644 --- a/client/src/pages/start.tsx +++ b/client/src/pages/start.tsx @@ -2,11 +2,11 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button } from "../components/button"; +import { CustomInput } from "../components/custom-input"; import { ErrorContainer } from "../components/error-container"; import { IntroContainer } from "../components/intro-container"; import { TitleHeader } from "../components/title-header"; import { useRoom } from "../lib/use-room"; -import { CustomInput } from "../components/custom-input"; export function Start() { const navigate = useNavigate(); @@ -43,7 +43,7 @@ export function Start() { roomCode: mode === "join" ? roomCode.trim() : undefined, isPrivate: mode === "create" ? isPrivate : undefined, }); - await navigate("/game"); + await navigate("/lobby"); } catch { setErrorMessage("Nie udało się dołaczyć do gry. Spróbuj ponownie."); setStatus("error"); @@ -72,7 +72,7 @@ export function Start() { if (cachedReconnection === null) { setStatus("idle"); } else { - void navigate("/game"); + void navigate("/lobby"); } }, 1000); diff --git a/server/package-lock.json b/server/package-lock.json index 1ab5064..5808959 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -816,90 +816,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1175,20 +1091,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -1218,14 +1120,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1288,14 +1182,6 @@ "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1664,17 +1550,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -1734,23 +1609,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2299,28 +2157,6 @@ "jws": "^4.0.0" } }, - "node_modules/grant/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/grant/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/grant/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2359,18 +2195,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2393,19 +2217,6 @@ "he": "bin/he" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2679,44 +2490,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwk-to-pem": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", - "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "asn1.js": "^5.3.0", - "elliptic": "^6.6.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2900,22 +2673,6 @@ "node": ">= 0.6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2987,29 +2744,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, "node_modules/nanoid": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", @@ -3026,22 +2760,6 @@ "node": ">= 0.6" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-os-utils": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/node-os-utils/-/node-os-utils-1.3.7.tgz", diff --git a/server/src/rooms/GameRoom.ts b/server/src/rooms/GameRoom.ts index 399fe06..b4b7cba 100644 --- a/server/src/rooms/GameRoom.ts +++ b/server/src/rooms/GameRoom.ts @@ -14,17 +14,46 @@ export class GameRoom extends Room { onCreate(options: any) { this.setMetadata({ - isPrivate: !!options.isPrivate + isPrivate: !!options.isPrivate, }); this.maxClients = room.maxClients ?? this.maxClients; if (options.private) { - this.setPrivate(true); + this.setPrivate(true); } - this.state.loadRoomFromJson(room); + this.onMessage("toggle_ready", (client) => { + const player = this.state.playerState.players.get(client.sessionId); + if (player) { + player.ready = !player.ready; + } + + if (this.clients.length === 2) { + const allReady = Array.from( + this.state.playerState.players.values(), + ).every((p) => p.ready); + if (allReady) { + this.state.gameStarted = true; + this.state.loadRoomFromJson(room); + this.clients.forEach((client) => { + const p = this.state.playerState.players.get(client.sessionId); + const startPos = + this.state.startingPositions[ + p.index % this.state.startingPositions.length + ]; + if (p && startPos) { + p.position.x = startPos.x; + p.position.y = startPos.y; + } + }); + } + } + }); + this.onMessage("move", (client, message) => { + if (!this.state.gameStarted) return; + const player = this.state.playerState.players.get(client.sessionId); if (!player) return; @@ -74,6 +103,8 @@ export class GameRoom extends Room { }); this.setSimulationInterval((deltaTime) => { + if (!this.state.gameStarted) return; + const result = this.state.updateLasers(deltaTime); if (result.length > 0) { this.broadcast("lasersUpdated", { lasers: result }); @@ -126,7 +157,7 @@ export class GameRoom extends Room { } onJoin(client: Client, options: any) { - const nickname = options.name || `Capy_${client.sessionId.slice(0, 3)}`; + const nickname = options.name; this.state.spawnNewPlayer(client.sessionId, nickname); @@ -136,16 +167,9 @@ export class GameRoom extends Room { // Player index 1 = Vron const character = player.index === 0 ? "Sol" : "Vron"; - console.log(`Gracz ${player.name} dołączył jako ${player.index === 0 ? "Sol" : "Vron"}`); - - this.broadcast("onAddPlayer", { - sessionId: client.sessionId, - playerName: player.name, - character: character, - position: player.position, - index: player.index, - }); - console.log(client.sessionId, "joined!"); + console.log( + `Gracz ${player.name} dołączył jako ${player.index === 0 ? "Sol" : "Vron"}`, + ); } async onLeave(client: Client, consented: boolean) { diff --git a/server/src/rooms/schema/Player.ts b/server/src/rooms/schema/Player.ts index a9575ff..dfc0f23 100644 --- a/server/src/rooms/schema/Player.ts +++ b/server/src/rooms/schema/Player.ts @@ -6,5 +6,6 @@ export class Player extends Schema { @type("string") sessionId: string; @type("number") index: number; @type("string") name: string; - @type(Position) position: Position; + @type(Position) position: Position = new Position(); + @type("boolean") ready: boolean = false; } diff --git a/server/src/rooms/schema/PlayerState.ts b/server/src/rooms/schema/PlayerState.ts index 4d4acc8..9322a46 100644 --- a/server/src/rooms/schema/PlayerState.ts +++ b/server/src/rooms/schema/PlayerState.ts @@ -67,4 +67,9 @@ export class PlayerState extends Schema { getSessionIdByName(name: string): string | null { return this.playerSessionIds.get(name) || null; } + + toggleReady(sessionId: string) { + const player = this.players.get(sessionId); + player.ready = !player.ready; + } } diff --git a/server/src/rooms/schema/RoomState.ts b/server/src/rooms/schema/RoomState.ts index 8297a68..8713fec 100644 --- a/server/src/rooms/schema/RoomState.ts +++ b/server/src/rooms/schema/RoomState.ts @@ -15,6 +15,7 @@ export class RoomState extends Schema { @type(["string"]) grid = new ArraySchema(); @type("number") width: number = 10; @type("number") height: number = 7; + @type("boolean") gameStarted: boolean = false; @type([Position]) startingPositions = new ArraySchema(); @@ -282,10 +283,14 @@ export class RoomState extends Schema { spawnNewPlayer(sessionId: string, name: string = null) { this.playerState.createPlayer(sessionId, name); const player = this.playerState.players.get(sessionId); - const startingPos = - this.startingPositions[player.index % this.startingPositions.length]; - player.position.x = startingPos.x; - player.position.y = startingPos.y; + if (this.startingPositions && this.startingPositions.length > 0) { + const startingPos = + this.startingPositions[player.index % this.startingPositions.length]; + if (startingPos) { + player.position.x = startingPos.x; + player.position.y = startingPos.y; + } + } } despawnPlayer(sessionId: string) { diff --git a/server/test/GameRoom_test.ts b/server/test/GameRoom_test.ts index e17eb6a..5445a2c 100644 --- a/server/test/GameRoom_test.ts +++ b/server/test/GameRoom_test.ts @@ -46,22 +46,25 @@ describe("GameRoom (room1)", () => { it("connects a client and syncs room1 state (size, player spawn)", async () => { const room = await colyseus.createRoom("game_room", {}); - const client = await colyseus.connectTo(room, { name: "TestPlayer" }); - registerNoopMessageHandlers(client); + const c1 = await colyseus.connectTo(room, { name: "TestPlayer" }); + const c2 = await colyseus.connectTo(room, { name: "Bot" }); + registerNoopMessageHandlers(c1); assert.strictEqual( - client.sessionId, + c1.sessionId, room.clients[0].sessionId, "Client sessionId matches the first connected client in the room", ); + c1.send("toggle_ready"); + c2.send("toggle_ready"); await room.waitForNextPatch(); const s = room.state; assert.strictEqual(s.width, 10); assert.strictEqual(s.height, 8); - const player = s.playerState.players.get(client.sessionId); + const player = s.playerState.players.get(c1.sessionId); assert.ok(player, "Player exists after join"); assert.strictEqual(player.position.x, 1); assert.strictEqual(player.position.y, 2); @@ -74,6 +77,9 @@ describe("GameRoom (room1)", () => { const c2 = await colyseus.connectTo(room, { name: "Bravo" }); registerNoopMessageHandlers(c2); + c1.send("toggle_ready"); + c2.send("toggle_ready"); + await room.waitForNextPatch(); const s = room.state; @@ -93,20 +99,24 @@ describe("GameRoom (room1)", () => { it("moves the player after a move message (down from spawn)", async () => { const room = await colyseus.createRoom("game_room", {}); - const client = await colyseus.connectTo(room, { name: "Mover" }); - registerNoopMessageHandlers(client); + const c1 = await colyseus.connectTo(room, { name: "Mover" }); + const c2 = await colyseus.connectTo(room, { name: "Bot" }); + registerNoopMessageHandlers(c1); + + c1.send("toggle_ready"); + c2.send("toggle_ready"); await room.waitForNextPatch(); - const before = room.state.playerState.players.get(client.sessionId); + const before = room.state.playerState.players.get(c1.sessionId); assert.deepStrictEqual( { x: before.position.x, y: before.position.y }, { x: 1, y: 2 }, ); - client.send("move", { direction: "down" }); + c1.send("move", { direction: "down" }); await room.waitForNextPatch(); - const after = room.state.playerState.players.get(client.sessionId); + const after = room.state.playerState.players.get(c1.sessionId); assert.deepStrictEqual( { x: after.position.x, y: after.position.y }, { x: 1, y: 3 }, From 9df653e003b171049ebd1254e4d1c564896e055a Mon Sep 17 00:00:00 2001 From: Harukume Date: Sun, 26 Apr 2026 10:13:14 +0200 Subject: [PATCH 4/7] fix: sync package-lock with main to resolve npm ci errors --- server/package-lock.json | 282 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/server/package-lock.json b/server/package-lock.json index 5808959..1ab5064 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -816,6 +816,90 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1091,6 +1175,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/async": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", @@ -1120,6 +1218,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1182,6 +1288,14 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -1550,6 +1664,17 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -1609,6 +1734,23 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2157,6 +2299,28 @@ "jws": "^4.0.0" } }, + "node_modules/grant/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/grant/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/grant/node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2195,6 +2359,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2217,6 +2393,19 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2490,6 +2679,44 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwk-to-pem": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", + "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.6.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2673,6 +2900,22 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2744,6 +2987,29 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/nanoid": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz", @@ -2760,6 +3026,22 @@ "node": ">= 0.6" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-os-utils": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/node-os-utils/-/node-os-utils-1.3.7.tgz", From 05930663326a95c25a8821b57745fa0d5805548d Mon Sep 17 00:00:00 2001 From: Harukume Date: Sun, 26 Apr 2026 10:20:40 +0200 Subject: [PATCH 5/7] chore: format code with prettier --- server/src/app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/app.config.ts b/server/src/app.config.ts index 64176e3..3ac3e7e 100644 --- a/server/src/app.config.ts +++ b/server/src/app.config.ts @@ -13,7 +13,7 @@ export default config({ * Define your room handlers: */ gameServer.define("game_room", GameRoom); - // .filterBy(['isPrivate']); + // .filterBy(['isPrivate']); }, initializeExpress: (app) => { From 486f48a82e6548b03f377c07ba860842ed119a06 Mon Sep 17 00:00:00 2001 From: Harukume Date: Sun, 26 Apr 2026 10:33:08 +0200 Subject: [PATCH 6/7] fix: align connect call with ConnectOptions interface --- client/src/pages/intro.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/pages/intro.tsx b/client/src/pages/intro.tsx index a68d6c2..9e1c29c 100644 --- a/client/src/pages/intro.tsx +++ b/client/src/pages/intro.tsx @@ -34,7 +34,10 @@ export function Intro() { setStatus("loading"); try { - await connect(name.trim()); + await connect({ + playerName: name.trim(), + mode: "create", + }); await navigate("/game"); } catch { setErrorMessage("Nie udało się dołaczyć do gry. Spróbuj ponownie."); From edf2cbdcbd76e94ef1289160e3e08d65963f7159 Mon Sep 17 00:00:00 2001 From: Harukume Date: Tue, 26 May 2026 12:37:45 +0200 Subject: [PATCH 7/7] refactor: apply code review suggestions --- client/src/App.tsx | 8 ++++++- client/src/lib/room-provider.tsx | 14 ++++++++--- client/src/pages/game.tsx | 2 +- client/src/pages/intro.tsx | 2 +- client/src/pages/lobby.tsx | 4 ++-- client/src/pages/start.tsx | 4 ++-- server/src/rooms/GameRoom.ts | 41 +++++++++++++++++--------------- 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index bc57f3c..18d5321 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,9 @@ -import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; +import { + Navigate, + Route, + BrowserRouter as Router, + Routes, +} from "react-router-dom"; import { Creator } from "./pages/creator"; import { Game } from "./pages/game"; @@ -18,6 +23,7 @@ export function App() { } /> } /> } /> + } />
diff --git a/client/src/lib/room-provider.tsx b/client/src/lib/room-provider.tsx index fadc1f6..acb299c 100644 --- a/client/src/lib/room-provider.tsx +++ b/client/src/lib/room-provider.tsx @@ -34,7 +34,6 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { setJoinError(false); let joinedRoom: Room; - console.warn({ playerName, mode, roomCode, isPrivate }); if (mode === "create") { joinedRoom = await client.create("game_room", { name: playerName, @@ -47,7 +46,6 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { ) { joinedRoom = await client.joinById(roomCode, { name: playerName, - //isPrivate: true, }); } else { joinedRoom = await client.joinOrCreate("game_room", { @@ -56,6 +54,16 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { }); } + if (joinedRoom && joinedRoom.reconnectionToken) { + localStorage.setItem( + "reconnection", + JSON.stringify({ + token: joinedRoom.reconnectionToken, + playerName, + }), + ); + } + setRoom(joinedRoom); setIsConnected(true); } catch (error) { @@ -85,7 +93,7 @@ export function RoomProvider({ children }: { children: React.ReactNode }) { isReconnecting = true; try { const parsed = JSON.parse(cached) as CachedReconnection; - const { token } = parsed; + const { token } = JSON.parse(cached); const reconnected = await client.reconnect(token); diff --git a/client/src/pages/game.tsx b/client/src/pages/game.tsx index de8b4af..d3ff869 100644 --- a/client/src/pages/game.tsx +++ b/client/src/pages/game.tsx @@ -38,7 +38,7 @@ export function Game() { )} diff --git a/server/src/rooms/GameRoom.ts b/server/src/rooms/GameRoom.ts index b4b7cba..c954979 100644 --- a/server/src/rooms/GameRoom.ts +++ b/server/src/rooms/GameRoom.ts @@ -19,7 +19,7 @@ export class GameRoom extends Room { this.maxClients = room.maxClients ?? this.maxClients; - if (options.private) { + if (options.isPrivate) { this.setPrivate(true); } @@ -29,24 +29,12 @@ export class GameRoom extends Room { player.ready = !player.ready; } - if (this.clients.length === 2) { + if (this.clients.length >= 1) { const allReady = Array.from( this.state.playerState.players.values(), ).every((p) => p.ready); if (allReady) { - this.state.gameStarted = true; - this.state.loadRoomFromJson(room); - this.clients.forEach((client) => { - const p = this.state.playerState.players.get(client.sessionId); - const startPos = - this.state.startingPositions[ - p.index % this.state.startingPositions.length - ]; - if (p && startPos) { - p.position.x = startPos.x; - p.position.y = startPos.y; - } - }); + this.startGame(); } } }); @@ -156,6 +144,25 @@ export class GameRoom extends Room { }); } + private startGame() { + this.state.gameStarted = true; + this.state.loadRoomFromJson(room); + + this.clients.forEach((client) => { + const p = this.state.playerState.players.get(client.sessionId); + if (p) { + const startPos = + this.state.startingPositions[ + p.index % this.state.startingPositions.length + ]; + if (startPos) { + p.position.x = startPos.x; + p.position.y = startPos.y; + } + } + }); + } + onJoin(client: Client, options: any) { const nickname = options.name; @@ -163,10 +170,6 @@ export class GameRoom extends Room { const player = this.state.playerState.players.get(client.sessionId); - // Player index 0 = Sol - // Player index 1 = Vron - const character = player.index === 0 ? "Sol" : "Vron"; - console.log( `Gracz ${player.name} dołączył jako ${player.index === 0 ? "Sol" : "Vron"}`, );