Multiplayer Lobby Detail UI
This document outlines the user interface for the lobby detail screen in the multiplayer system. This screen is displayed after a player joins a specific lobby and is where players prepare for the game, interact with each other, and manage their participation.
Table of Contents
- UI Layout
- Components
- Player Management
- Player Actions
- Host Controls
- Handling Player Departures
- Implementation
UI Layout
The lobby detail screen is divided into three main panels:
- Left Panel: Lobby information and player list
- Center Panel: Chat system
- Right Panel: Deck viewer
┌───────────────────────────────────────────────────────┐
│ Lobby Name Host: PlayerName │
├───────────────┬───────────────────┬───────────────────┤
│ │ │ │
│ PLAYERS │ │ DECK VIEWER │
│ │ │ │
│ ○ Player1 │ │ ┌─────────────┐ │
│ (Host) │ │ │ │ │
│ │ │ │ Commander │ │
│ ○ Player2 │ CHAT │ │ Card │ │
│ [Ready] │ │ │ │ │
│ │ │ └─────────────┘ │
│ ○ Player3 │ │ │
│ [Selecting] │ │ Deck Statistics │
│ │ │ │
│ ○ Player4 │ │ │
│ │ │ │
├───────────────┴───────────────────┴───────────────────┤
│ [Leave Lobby] [Select Deck] [Ready Up] │
└───────────────────────────────────────────────────────┘
Components
Lobby Header
The header displays essential information about the lobby:
- Lobby name
- Host name
- Game format (Standard Commander, cEDH, etc.)
- Player count (current/maximum)
- Password protection indicator
Player List
The player list shows all players in the lobby with their current status:
- Player name
- Ready status
- Selected deck (if ready)
- Host indicator
- Color identity of selected commander (if ready)
Ready Controls
Players can indicate their readiness through:
- Deck selection button
- Ready/Unready toggle
- Current status indicator
Player Management
The lobby implements a robust player management system to handle various player actions and states.
Player States
Players can be in various states while in the lobby:
#![allow(unused)] fn main() { /// Player state in the lobby #[derive(Component, Clone, Debug, PartialEq, Eq)] pub enum PlayerLobbyState { /// Just joined, not ready Joined, /// Selecting a deck SelectingDeck, /// Ready with deck selected Ready, /// Temporarily disconnected (can reconnect) Disconnected, /// In the process of joining Joining, } }
Player Actions
Players can perform several actions in the lobby:
Selecting a Deck
Players need to select a deck before they can ready up:
#![allow(unused)] fn main() { fn handle_deck_selection( mut interaction_query: Query<&Interaction, (Changed<Interaction>, With<DeckSelectButton>)>, mut commands: Commands, mut next_state: ResMut<NextState<DeckSelectionState>>, ) { for interaction in &mut interaction_query { if *interaction == Interaction::Pressed { // Transition to deck selection screen next_state.set(DeckSelectionState::Selecting); } } } }
Ready Status
Players indicate they are ready to play:
#![allow(unused)] fn main() { fn handle_ready_button( mut interaction_query: Query<&Interaction, (Changed<Interaction>, With<ReadyButton>)>, mut player_query: Query<&mut PlayerLobbyState, With<LocalPlayer>>, mut lobby_connection: ResMut<LobbyConnection>, ) { for interaction in &mut interaction_query { if *interaction == Interaction::Pressed { // Toggle ready status if let Ok(mut state) = player_query.get_single_mut() { match *state { PlayerLobbyState::Ready => { *state = PlayerLobbyState::Joined; lobby_connection.send_status_update(PlayerLobbyState::Joined); } _ => { if lobby_connection.has_deck_selected() { *state = PlayerLobbyState::Ready; lobby_connection.send_status_update(PlayerLobbyState::Ready); } } } } } } } }
Leaving a Lobby
Players can voluntarily leave a lobby:
#![allow(unused)] fn main() { fn handle_leave_button( mut interaction_query: Query<&Interaction, (Changed<Interaction>, With<LeaveButton>)>, mut lobby_connection: ResMut<LobbyConnection>, mut next_state: ResMut<NextState<GameMenuState>>, ) { for interaction in &mut interaction_query { if *interaction == Interaction::Pressed { // Send leave notification to server lobby_connection.leave_lobby(); // Return to lobby browser next_state.set(GameMenuState::MultiplayerBrowser); } } } }
Host Controls
The host player has additional controls for managing the lobby:
Start Game
The host can start the game when all players are ready:
#![allow(unused)] fn main() { fn handle_start_game_button( mut interaction_query: Query<&Interaction, (Changed<Interaction>, With<StartGameButton>)>, mut lobby_connection: ResMut<LobbyConnection>, player_query: Query<&PlayerLobbyState>, ) { for interaction in &mut interaction_query { if *interaction == Interaction::Pressed { // Check if all players are ready let all_ready = player_query .iter() .all(|state| *state == PlayerLobbyState::Ready); if all_ready { lobby_connection.start_game(); } } } } }
Kick Player
The host can remove players from the lobby:
#![allow(unused)] fn main() { fn handle_kick_button( mut interaction_query: Query<(&Interaction, &KickButtonTarget), (Changed<Interaction>, With<KickButton>)>, mut lobby_connection: ResMut<LobbyConnection>, ) { for (interaction, target) in &mut interaction_query { if *interaction == Interaction::Pressed { // Send kick request to server lobby_connection.kick_player(target.player_id.clone()); } } } }
Handling Player Departures
The system handles various scenarios for player departures from the lobby:
Voluntary Departure
When a player chooses to leave a lobby:
- The player initiates departure through the Leave button
- A leave message is sent to the server
- The server broadcasts the player's departure to all other players
- The player's UI transitions to the lobby browser
- Other players receive a notification of the departure
#![allow(unused)] fn main() { /// Message sent when a player leaves a lobby #[derive(Serialize, Deserialize, Clone, Debug)] pub struct LeaveLobbyMessage { /// ID of the lobby being left pub lobby_id: String, /// Reason for leaving pub reason: LeaveLobbyReason, } /// Reasons a player might leave a lobby #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum LeaveLobbyReason { /// Player chose to leave Voluntary, /// Player was kicked by the host Kicked, /// Player disconnected unexpectedly Disconnected, /// Player was idle for too long Timeout, } }
Being Kicked
When a host kicks a player:
- The host selects a player and clicks the Kick button
- A kick message is sent to the server
- The server validates the request (ensures sender is host)
- The server sends a departure notification to the kicked player
- The kicked player's UI transitions to the lobby browser with a message
- Other players receive a notification that the player was kicked
#![allow(unused)] fn main() { /// System to handle being kicked from a lobby fn handle_being_kicked( mut kick_events: EventReader<KickedFromLobbyEvent>, mut commands: Commands, mut next_state: ResMut<NextState<GameMenuState>>, ) { for event in kick_events.read() { // Display kicked message commands.spawn(KickedNotificationUI { reason: event.reason.clone(), }); // Return to lobby browser next_state.set(GameMenuState::MultiplayerBrowser); } } }
Disconnections
The system also handles unexpected disconnections:
- When a player disconnects, the server detects the dropped connection
- The server broadcasts a player disconnection to all remaining players
- The server keeps the player's slot reserved for a period of time
- If the player reconnects within the time window, they rejoin seamlessly
- If the reconnection window expires, the slot is freed and other players are notified
#![allow(unused)] fn main() { /// System to handle player disconnections fn handle_player_disconnection( mut disconnection_events: EventReader<PlayerDisconnectedEvent>, mut player_query: Query<(&mut PlayerLobbyState, &PlayerName)>, mut lobby_state: ResMut<LobbyState>, ) { for event in disconnection_events.read() { // Update the disconnected player's state for (mut state, name) in &mut player_query { if name.0 == event.player_name { *state = PlayerLobbyState::Disconnected; // Start reconnection timer lobby_state.disconnection_timers.insert( event.player_name.clone(), ReconnectionTimer { time_remaining: RECONNECTION_WINDOW, player_id: event.player_id.clone(), } ); break; } } } } }
Host Migration
If the host leaves or disconnects, the system can migrate host privileges to another player:
- When the host leaves, the server selects the next player (typically by join time)
- The server broadcasts a host migration message to all players
- The new host receives additional UI controls
- All players are notified of the host change
#![allow(unused)] fn main() { /// Message for host migration #[derive(Serialize, Deserialize, Clone, Debug)] pub struct HostMigrationMessage { /// ID of the new host pub new_host_id: String, /// Name of the new host pub new_host_name: String, } /// System to handle host migration fn handle_host_migration( mut migration_events: EventReader<HostMigrationEvent>, mut player_query: Query<(&mut PlayerLobbyState, &PlayerName, &mut PlayerUI)>, mut lobby_state: ResMut<LobbyState>, ) { for event in migration_events.read() { // Update host status lobby_state.host_id = event.new_host_id.clone(); // Update UI to reflect new host for (state, name, mut ui) in &mut player_query { if name.0 == event.new_host_name { ui.show_host_controls(); } } } } }
Implementation
The lobby detail UI is implemented using Bevy's UI system:
#![allow(unused)] fn main() { /// Set up the lobby detail screen pub fn setup_lobby_detail( mut commands: Commands, asset_server: Res<AssetServer>, lobby_info: Res<CurrentLobbyInfo>, ) { // Main container commands .spawn(( Node { width: Val::Percent(100.0), height: Val::Percent(100.0), flex_direction: FlexDirection::Column, ..default() }, LobbyDetailUI, )) .with_children(|parent| { // Header setup_lobby_header(parent, &asset_server, &lobby_info); // Main content area parent .spawn(Node { width: Val::Percent(100.0), height: Val::Percent(85.0), flex_direction: FlexDirection::Row, ..default() }) .with_children(|content| { // Left panel (player list) setup_player_list_panel(content, &asset_server, &lobby_info); // Center panel (chat) setup_chat_panel(content, &asset_server); // Right panel (deck viewer) setup_deck_viewer_panel(content, &asset_server); }); // Footer with action buttons setup_action_buttons(parent, &asset_server, &lobby_info); }); } }
This UI adapts based on the player's role (host or regular player) and provides appropriate controls for each state.