lightyear_examples_common 0.26.4

Common harness for the lightyear examples
Documentation
use bevy::picking::prelude::{Click, Pointer};
use bevy::prelude::*;
#[cfg(feature = "bevygap_client")]
use bevygap_client_plugin::prelude::*;
use lightyear::connection::client::ClientState;
use lightyear::prelude::client::*;
use lightyear::prelude::*;

pub struct ExampleClientRendererPlugin {
    /// The name of the example, which must also match the edgegap application name.
    pub name: String,
}

impl ExampleClientRendererPlugin {
    pub fn new(name: String) -> Self {
        Self { name }
    }
}

#[derive(Resource)]
struct GameName(String);

impl Plugin for ExampleClientRendererPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(GameName(self.name.clone()));
        app.insert_resource(ClearColor::default());
        app.add_systems(Startup, set_window_title);
        spawn_connect_button(app);
        app.add_systems(Update, update_button_text);
        app.add_observer(on_update_status_message);
        app.add_observer(handle_connection);
        app.add_observer(handle_disconnection);
    }
}

fn set_window_title(mut window: Query<&mut Window>, game_name: Res<GameName>) {
    let mut window = window.single_mut().unwrap();
    window.title = format!("Lightyear Example: {}", game_name.0);
}

#[derive(Event, Debug)]
pub struct UpdateStatusMessage(pub String);

fn on_update_status_message(
    trigger: On<UpdateStatusMessage>,
    mut q: Query<&mut Text, With<StatusMessageMarker>>,
) {
    for mut text in &mut q {
        text.0 = trigger.event().0.clone();
    }
}

#[derive(Component)]
struct StatusMessageMarker;

#[derive(Component)]
pub(crate) struct ClientButton;

/// Create a button that allow you to connect/disconnect to a server
pub(crate) fn spawn_connect_button(app: &mut App) {
    app.world_mut()
        .spawn(Node {
            width: Val::Percent(30.0),
            height: Val::Percent(30.0),
            position_type: PositionType::Absolute,
            bottom: Val::Px(0.0),
            align_items: AlignItems::FlexEnd,
            justify_content: JustifyContent::FlexEnd,
            justify_self: JustifySelf::End,
            flex_direction: FlexDirection::Row,
            ..default()
        })
        .with_children(|parent| {
            parent.spawn((
                Text("[Client]".to_string()),
                TextColor(Color::srgb(0.9, 0.9, 0.9).with_alpha(0.4)),
                TextFont::from_font_size(18.0),
                StatusMessageMarker,
                Node {
                    padding: UiRect::all(Val::Px(10.0)),
                    justify_content: JustifyContent::Center,
                    align_items: AlignItems::Center,
                    ..default()
                },
            ));
            parent
                .spawn((
                    Text("Connect".to_string()),
                    TextColor(Color::srgb(0.9, 0.9, 0.9)),
                    TextFont::from_font_size(20.0),
                    ClientButton,
                    Node {
                        width: Val::Px(150.0),
                        height: Val::Px(65.0),
                        border: UiRect::all(Val::Px(5.0)),
                        // horizontally center child text
                        justify_content: JustifyContent::Center,
                        // vertically center child text
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    Button,
                ))
                .observe(
                    |_: On<Pointer<Click>>,
                     mut commands: Commands,
                     query: Query<(Entity, &Client)>| {
                        let Ok((entity, client)) = query.single() else {
                            return;
                        };
                        match client.state {
                            ClientState::Disconnected => {
                                commands.trigger(Connect { entity });
                            }
                            _ => {
                                commands.trigger(Disconnect { entity });
                            }
                        };
                    },
                );
        });
}

pub(crate) fn update_button_text(
    client: Single<&Client>,
    mut text_query: Query<&mut Text, (With<Button>, With<ClientButton>)>,
) {
    if let Ok(mut text) = text_query.single_mut() {
        match client.state {
            ClientState::Disconnecting => {
                text.0 = "Disconnecting".to_string();
            }
            ClientState::Disconnected => {
                text.0 = "Connect".to_string();
            }
            ClientState::Connecting => {
                text.0 = "Connecting".to_string();
            }
            ClientState::Connected => {
                text.0 = "Disconnect".to_string();
            }
        }
    }
}

/// Component to identify the text displaying the client id

#[derive(Component)]
pub struct ClientIdText;

/// Listen for events to know when the client is connected, and spawn a text entity
/// to display the client id
pub(crate) fn handle_connection(
    trigger: On<Add, Connected>,
    query: Query<&LocalId, Or<((With<LinkOf>, With<Client>), Without<LinkOf>)>>,
    mut commands: Commands,
) {
    if let Ok(client_id) = query.get(trigger.entity) {
        commands.spawn((
            Text(format!("Client {}", client_id.0)),
            TextFont::from_font_size(30.0),
            ClientIdText,
        ));
    }
}

/// Listen for events to know when the client is disconnected, and print out the reason
/// of the disconnection
pub(crate) fn handle_disconnection(
    trigger: On<Add, Disconnected>,
    mut commands: Commands,
    debug_text: Query<Entity, With<ClientIdText>>,
    disconnected: Query<(Entity, &Disconnected)>,
) {
    commands.trigger(UpdateStatusMessage(format!(
        "Disconnected ({})",
        disconnected
            .get(trigger.entity)
            .map(|d| d.1.reason.as_ref())
            .unwrap_or(None)
            .unwrap_or(&"Unknown".to_string())
    )));
    for entity in debug_text.iter() {
        commands.entity(entity).despawn();
    }
}