diff --git a/apps/client/src/app.tsx b/apps/client/src/app.tsx index 08b1b54..a44fa3e 100644 --- a/apps/client/src/app.tsx +++ b/apps/client/src/app.tsx @@ -9,14 +9,15 @@ import { appConsts } from "@repo/shared"; export default function App() { - const { scenePhase, room, myId, joinErrorMessage } = useAppFlow(); + const { scenePhase, room, myId, joinErrorMessage, isJoining, requestJoin } = useAppFlow(); // タイトル画面分岐 if (scenePhase === appConsts.ScenePhase.TITLE) { return ( socketManager.title.joinRoom(payload)} + onJoin={requestJoin} joinErrorMessage={joinErrorMessage} + isJoining={isJoining} /> ); } diff --git a/apps/client/src/hooks/useAppFlow.ts b/apps/client/src/hooks/useAppFlow.ts index 6fe6ae8..db815c9 100644 --- a/apps/client/src/hooks/useAppFlow.ts +++ b/apps/client/src/hooks/useAppFlow.ts @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { socketManager } from "@client/network/SocketManager"; -import { appConsts } from "@repo/shared"; +import { appConsts, config } from "@repo/shared"; import type { appTypes, roomTypes } from "@repo/shared"; type AppFlowState = { @@ -8,6 +8,8 @@ room: roomTypes.Room | null; myId: string | null; joinErrorMessage: string | null; + isJoining: boolean; + requestJoin: (payload: roomTypes.JoinRoomPayload) => void; }; export const useAppFlow = (): AppFlowState => { @@ -15,17 +17,44 @@ const [room, setRoom] = useState(null); const [myId, setMyId] = useState(null); const [joinErrorMessage, setJoinErrorMessage] = useState(null); + const [isJoining, setIsJoining] = useState(false); + const joinTimeoutRef = useRef | null>(null); + + const clearJoinTimeout = () => { + if (!joinTimeoutRef.current) { + return; + } + + clearTimeout(joinTimeoutRef.current); + joinTimeoutRef.current = null; + }; + + const requestJoin = (payload: roomTypes.JoinRoomPayload) => { + clearJoinTimeout(); + setJoinErrorMessage(null); + setIsJoining(true); + joinTimeoutRef.current = setTimeout(() => { + setIsJoining(false); + setJoinErrorMessage("参加要求がタイムアウトしました,もう一度お試しください"); + joinTimeoutRef.current = null; + }, config.GAME_CONFIG.JOIN_REQUEST_TIMEOUT_MS); + socketManager.title.joinRoom(payload); + }; useEffect(() => { const handleConnect = (id: string) => { setMyId(id); }; const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { + clearJoinTimeout(); setRoom(updatedRoom); + setIsJoining(false); setJoinErrorMessage(null); setScenePhase(appConsts.ScenePhase.LOBBY); }; const handleJoinRejected = (payload: roomTypes.JoinRoomRejectedPayload) => { + clearJoinTimeout(); + setIsJoining(false); if (payload.reason === "full") { setJoinErrorMessage(`ルーム ${payload.roomId} は満員です`); } @@ -40,6 +69,7 @@ socketManager.game.onGameStart(handleGameStart); return () => { + clearJoinTimeout(); socketManager.common.offConnect(handleConnect); socketManager.title.offJoinRejected(handleJoinRejected); socketManager.lobby.offRoomUpdate(handleRoomUpdate); @@ -47,5 +77,5 @@ }; }, []); - return { scenePhase, room, myId, joinErrorMessage }; + return { scenePhase, room, myId, joinErrorMessage, isJoining, requestJoin }; }; diff --git a/apps/client/src/scenes/title/TitleScene.tsx b/apps/client/src/scenes/title/TitleScene.tsx index cec2fdc..ffb2328 100644 --- a/apps/client/src/scenes/title/TitleScene.tsx +++ b/apps/client/src/scenes/title/TitleScene.tsx @@ -7,9 +7,11 @@ onJoin: (payload: roomTypes.JoinRoomPayload) => void; // 入室失敗時の表示メッセージ joinErrorMessage: string | null; + // 入室リクエスト送信中フラグ + isJoining: boolean; }; -export const TitleScene = ({ onJoin, joinErrorMessage }: Props) => { +export const TitleScene = ({ onJoin, joinErrorMessage, isJoining }: Props) => { // プレイヤー名入力値 const [playerName, setPlayerName] = useState(""); // ルームID入力値 @@ -46,12 +48,12 @@ {joinErrorMessage && ( diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index bc2a755..eac31d5 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -5,6 +5,7 @@ // UI表示更新設定 TIMER_DISPLAY_UPDATE_MS: 250, // 残り時間表示の更新間隔(ms) + JOIN_REQUEST_TIMEOUT_MS: 8000, // ルーム参加要求の待機タイムアウト(ms) // ネットワーク・描画補間設定 PLAYER_POSITION_UPDATE_MS: 50, // 座標送信間隔(20Hz)