diff --git a/apps/client/src/network/handlers/LobbyHandler.ts b/apps/client/src/network/handlers/LobbyHandler.ts index 49ff35c..3b7e8a9 100644 --- a/apps/client/src/network/handlers/LobbyHandler.ts +++ b/apps/client/src/network/handlers/LobbyHandler.ts @@ -7,6 +7,8 @@ import { contracts as protocol } from "@repo/shared"; import type { LobbySettingsUpdatePayload, + SelectTeamPayload, + SelectTeamRejectedPayload, ServerToClientPayloadOf, StartGameRequestPayload, } from "@repo/shared"; @@ -31,6 +33,9 @@ ) => void; startGame: (payload?: StartGameRequestPayload) => void; updateLobbySettings: (payload: LobbySettingsUpdatePayload) => void; + selectTeam: (payload: SelectTeamPayload) => void; + onSelectTeamRejected: (callback: (payload: SelectTeamRejectedPayload) => void) => void; + offSelectTeamRejected: (callback: (payload: SelectTeamRejectedPayload) => void) => void; }; /** ロビー画面向けのソケットハンドラを生成する */ @@ -54,6 +59,15 @@ updateLobbySettings: (payload) => { emitEvent(protocol.SocketEvents.LOBBY_SETTINGS_UPDATE, payload); }, + selectTeam: (payload) => { + emitEvent(protocol.SocketEvents.SELECT_TEAM, payload); + }, + onSelectTeamRejected: (callback) => { + onEvent(protocol.SocketEvents.SELECT_TEAM_REJECTED, callback); + }, + offSelectTeamRejected: (callback) => { + offEvent(protocol.SocketEvents.SELECT_TEAM_REJECTED, callback); + }, }; }; diff --git a/apps/client/src/scenes/lobby/LobbyScene.tsx b/apps/client/src/scenes/lobby/LobbyScene.tsx index ee9bdbc..f46abfe 100644 --- a/apps/client/src/scenes/lobby/LobbyScene.tsx +++ b/apps/client/src/scenes/lobby/LobbyScene.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { domain } from "@repo/shared"; -import type { FieldSizePreset, StartGameRequestPayload } from "@repo/shared"; +import type { FieldSizePreset, StartGameRequestPayload, TeamAssignmentMode } from "@repo/shared"; import { config } from "@client/config"; import { socketManager } from "@client/network/SocketManager"; import { OVERLAY_BUTTON_STYLE } from "@client/scenes/shared/styles/overlayStyles"; @@ -27,6 +27,18 @@ LOBBY_WAITING_STYLE, } from "./styles/LobbyScene.styles"; +/** チームIDを1始まりの表示ラベルに変換する */ +const toTeamLabel = (teamId: number): string => `チーム${teamId + 1}`; + +/** チーム選択のオプション(チームID 0〜3 + ランダム) */ +const TEAM_SELECT_OPTIONS: Array<{ value: number | null; label: string }> = [ + { value: null, label: "ランダム" }, + { value: 0, label: toTeamLabel(0) }, + { value: 1, label: toTeamLabel(1) }, + { value: 2, label: toTeamLabel(2) }, + { value: 3, label: toTeamLabel(3) }, +]; + type Props = { room: domain.room.Room | null; myId: string | null; @@ -77,6 +89,9 @@ }, []); const [selectedFieldSizePreset, setSelectedFieldSizePreset] = useState(config.GAME_CONFIG.DEFAULT_FIELD_PRESET); + const [selectedTeamAssignmentMode, setSelectedTeamAssignmentMode] = + useState("random"); + const [teamFullMessage, setTeamFullMessage] = useState(null); const [isRuleModalOpen, setIsRuleModalOpen] = useState(false); const [isStartConfirmVisible, setIsStartConfirmVisible] = useState(false); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); @@ -104,8 +119,9 @@ socketManager.lobby.updateLobbySettings({ targetPlayerCount: selectedStartPlayerCount, fieldSizePreset: selectedFieldSizePreset, + teamAssignmentMode: selectedTeamAssignmentMode, }); - }, [isMeOwner, selectedStartPlayerCount, selectedFieldSizePreset]); + }, [isMeOwner, selectedStartPlayerCount, selectedFieldSizePreset, selectedTeamAssignmentMode]); const handleStartClick = () => { setIsStartConfirmVisible(true); @@ -123,6 +139,19 @@ setIsStartConfirmVisible(false); }; + useEffect(() => { + const handler = () => { + setTeamFullMessage("このチームは満員です。別のチームを選んでください。"); + setTimeout(() => { setTeamFullMessage(null); }, 3000); + }; + socketManager.lobby.onSelectTeamRejected(handler); + return () => { socketManager.lobby.offSelectTeamRejected(handler); }; + }, []); + + const handleSelectTeam = (preferredTeamId: number | null) => { + socketManager.lobby.selectTeam({ preferredTeamId }); + }; + const toFieldPresetLabel = (preset: FieldSizePreset): string => { const range = config.GAME_CONFIG.FIELD_PRESETS[preset].recommendedPlayers; const baseLabel = @@ -137,6 +166,13 @@ return `${baseLabel} (${range.min}-${range.max}人目安)`; }; + // 自分のチーム選択状態を取得する + const myPreferredTeamId = room.players.find((p) => p.id === myId)?.preferredTeamId ?? null; + + // チーム割り当て方式のラベルを取得する + const teamAssignmentModeLabel = + room.teamAssignmentMode === "player_select" ? "プレイヤーが選択" : "ランダム"; + return ( <>