DEVELOPMENT ENVIRONMENT

~liljamo/deck-builder

443c9b0c1e96aadaa1aff06953210c400b7fc0c9 — skye 1 year, 5 months ago cc99931
feat!(client): get in-game, and see some details
M client/src/api/game/join.rs => client/src/api/game/join.rs +2 -2
@@ 9,10 9,10 @@
use crate::{NetworkingOptions, post_request_auth};

// NOTE: this response isn't actually used
type JoinResponse = Result<String, ()>;
pub type JoinResponse = Result<String, ()>;

pub fn join(no: &NetworkingOptions, game_id: &str) -> JoinResponse {
    let res = post_request_auth!(no, &format!("/game/{}/join)", &game_id), &{});
    let res = post_request_auth!(no, &format!("/game/{}/join", &game_id), &{});

    Ok(res.text().unwrap())
}

M client/src/macros/async_task.rs => client/src/macros/async_task.rs +22 -0
@@ 111,4 111,26 @@ macro_rules! async_task_handle_call {
            }
        }
    };

    ($call_type:ty, |$response:ident, $global:ident, $game_data:ident| $handler_func:expr) => {
        pub fn handle_call(
            mut commands: Commands,
            mut tasks: Query<(Entity, &mut $call_type)>,
            mut $game_data: ResMut<GameData>,
        ) {
            match tasks.get_single_mut() {
                Ok((entity, mut task)) => {
                    if let Some($response) = future::block_on(future::poll_once(&mut task.0)) {
                        $handler_func;

                        // remove the task
                        commands.entity(entity).remove::<$call_type>();
                        commands.entity(entity).despawn_recursive();
                    }
                }
                // NOTE: don't do anything if the wanted thingy doesn't exist
                _ => {}
            }
        }
    };
}

M client/src/main.rs => client/src/main.rs +4 -0
@@ 60,12 60,14 @@ fn main() {

    app.insert_resource(Global {
        user: None,
        cur_game_id: "".to_string(),
    });

    // has handlers for all async tasks
    app.add_plugin(plugins::AsyncTasksPlugin);

    app.add_plugin(plugins::MenuPlugin);
    app.add_plugin(plugins::GamePlugin);

    app.add_startup_system(setup);
    app.run();


@@ 110,4 112,6 @@ pub struct NetworkingOptions {
pub struct Global {
    /// details of the logged in user
    user: Option<User>,
    /// current game id, set from browse
    cur_game_id: String,
}

M client/src/plugins/async_tasks/mod.rs => client/src/plugins/async_tasks/mod.rs +12 -0
@@ 17,12 17,18 @@ pub use req_register::RegisterCallEvent;
mod req_game_create;
pub use req_game_create::GameCreateCallEvent;

mod req_game_join;
pub use req_game_join::GameJoinCallEvent;

mod req_game_forming;
pub use req_game_forming::GameFormingCallEvent;

mod req_game_mygames;
pub use req_game_mygames::GameMyGamesCallEvent;

mod req_game_details;
pub use req_game_details::GameDetailsCallEvent;

pub struct AsyncTasksPlugin;

impl Plugin for AsyncTasksPlugin {


@@ 31,8 37,10 @@ impl Plugin for AsyncTasksPlugin {
            .add_event::<LoginCallEvent>()
            .add_event::<RegisterCallEvent>()
            .add_event::<GameCreateCallEvent>()
            .add_event::<GameJoinCallEvent>()
            .add_event::<GameFormingCallEvent>()
            .add_event::<GameMyGamesCallEvent>()
            .add_event::<GameDetailsCallEvent>()
            .add_systems(
            (
                req_login::start_call,


@@ 41,10 49,14 @@ impl Plugin for AsyncTasksPlugin {
                req_register::handle_call,
                req_game_create::start_call,
                req_game_create::handle_call,
                req_game_join::start_call,
                req_game_join::handle_call,
                req_game_forming::start_call,
                req_game_forming::handle_call,
                req_game_mygames::start_call,
                req_game_mygames::handle_call,
                req_game_details::start_call,
                req_game_details::handle_call,
            ).chain()
        );
    }

A client/src/plugins/async_tasks/req_game_details.rs => client/src/plugins/async_tasks/req_game_details.rs +40 -0
@@ 0,0 1,40 @@
/*
 * This file is part of laurelin_client
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use crate::{
    async_task_start_call, async_task_handle_call,
    api::{self, game::DetailsResponse}, NetworkingOptions,
    plugins::game::GameData,
};

use bevy::{prelude::*, tasks::{Task, AsyncComputeTaskPool}};
use futures_lite::future;

#[derive(Component)]
pub struct GameDetailsCall(Task<DetailsResponse>);

#[derive(Clone)]
pub struct GameDetailsCallEvent {
    pub game_id: String,
}

async_task_start_call!(GameDetailsCallEvent, GameDetailsCall, |ev, no| {
    let res = api::game::details(&no, &ev.game_id);

    res
});

async_task_handle_call!(GameDetailsCall, |response, _global, game_data| {
    match response {
        Err(_err) => panic!("game details failed, handle me"),
        Ok(resp) => {
            info!("game details for {}", resp.id);
            game_data.game = Some(resp);
        }
    }
});

A client/src/plugins/async_tasks/req_game_join.rs => client/src/plugins/async_tasks/req_game_join.rs +39 -0
@@ 0,0 1,39 @@
/*
 * This file is part of laurelin_client
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use crate::{
    async_task_start_call, async_task_handle_call,
    api::{self, game::JoinResponse}, NetworkingOptions,
    plugins::menu::MenuData,
};

use bevy::{prelude::*, tasks::{Task, AsyncComputeTaskPool}};
use futures_lite::future;

#[derive(Component)]
pub struct GameJoinCall(Task<JoinResponse>);

#[derive(Clone)]
pub struct GameJoinCallEvent {
    pub game_id: String,
}

async_task_start_call!(GameJoinCallEvent, GameJoinCall, |ev, no| {
    let res = api::game::join(&no, &ev.game_id);

    res
});

async_task_handle_call!(GameJoinCall, |response, _menu_data| {
    match response {
        Err(_err) => panic!("game join failed, handle me"),
        Ok(resp) => {
            info!("joined game {}", resp);
        }
    }
});

A client/src/plugins/game/mod.rs => client/src/plugins/game/mod.rs +47 -0
@@ 0,0 1,47 @@
/*
 * This file is part of laurelin_client
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::prelude::*;

use crate::{api::game::Game, Global, AppState};

use super::GameDetailsCallEvent;

mod ui;

pub struct GamePlugin;

impl Plugin for GamePlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(GameData::default())
            .add_system(game_setup.in_schedule(OnEnter(AppState::InGame)))
            .add_system(ui::ui.run_if(in_state(AppState::InGame)));
    }
}

#[derive(Resource)]
pub struct GameData {
    pub game: Option<Game>,
}

impl Default for GameData {
    fn default() -> Self {
        Self {
            game: None,
        }
    }
}

fn game_setup(
    mut details_ev_w: EventWriter<GameDetailsCallEvent>,
    global: Res<Global>,
) {
    details_ev_w.send(GameDetailsCallEvent {
        game_id: global.cur_game_id.clone(),
    });
}

A client/src/plugins/game/ui/mod.rs => client/src/plugins/game/ui/mod.rs +42 -0
@@ 0,0 1,42 @@
/*
 * This file is part of laurelin_client
 * Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
 *
 * Licensed under GPL-3.0-only.
 * See LICENSE for licensing information.
 */

use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts};

use crate::{plugins::GameDetailsCallEvent, Global};

use super::GameData;

pub fn ui(
    mut commands: Commands,
    mut contexts: EguiContexts,
    global: Res<Global>,
    mut game_data: ResMut<GameData>,
    mut details_ev_w: EventWriter<GameDetailsCallEvent>,
) {
    egui::Window::new("Game Details")
        .show(contexts.ctx_mut(), |ui| {
            let Some(game) = &game_data.game else {
                // early return if game is None
                return;
            };
            if ui.button("Refresh").clicked() {
                details_ev_w.send(GameDetailsCallEvent {
                    game_id: game_data.game.as_ref().unwrap().id.clone(),
                });
            }

            ui.separator();

            ui.collapsing("Details", |ui| {
                ui.label(format!("Host: {}", game.host.as_ref().unwrap().username));
                ui.label(format!("Guest: {}", game.guest.as_ref().unwrap().username));
            });
        });
}

M client/src/plugins/menu/ui/browse.rs => client/src/plugins/menu/ui/browse.rs +31 -2
@@ 11,17 11,21 @@ use bevy_egui::egui;

use crate::{plugins::{
    GameCreateCallEvent, BrowseState, 
    GameFormingCallEvent, GameMyGamesCallEvent
}, api::game::{GAMESTATE_INPROGRESS, GAMESTATE_FINISHED}};
    GameFormingCallEvent, GameMyGamesCallEvent,
    GameJoinCallEvent, menu::MenuState
}, api::game::{GAMESTATE_INPROGRESS, GAMESTATE_FINISHED}, Global, AppState};

use super::{MenuData, MenuUIState};

pub fn view(
    mut commands: &mut Commands,
    ui: &mut egui::Ui,
    mut global: &mut Global,
    data: &mut MenuData,
    create_ev_w: &mut EventWriter<GameCreateCallEvent>,
    forming_ev_w: &mut EventWriter<GameFormingCallEvent>,
    mygames_ev_w: &mut EventWriter<GameMyGamesCallEvent>,
    join_ev_w: &mut EventWriter<GameJoinCallEvent>,
) {
    egui::SidePanel::left("browse_side_panel")
        .resizable(false)


@@ 86,6 90,16 @@ pub fn view(
                                        "Host: {}",
                                        game.host.as_ref().unwrap().username
                                    ));

                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    if game.host_id == global.user.as_ref().unwrap().id {
                                        ui.add_enabled(false, egui::Button::new("Host"));
                                    } else if ui.button("Join").clicked() {
                                        join_ev_w.send(GameJoinCallEvent {
                                            game_id: game.id.clone(),
                                        });
                                    }
                                });
                            });
                        });
                }


@@ 105,6 119,12 @@ pub fn view(
                                        "Host: {}",
                                        game.host.as_ref().unwrap().username
                                    ));

                                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                                    if ui.button("Enter").clicked() {
                                        enter_game(&mut commands, &mut global, &game.id);
                                    }
                                });
                            });
                        });
                }


@@ 132,3 152,12 @@ pub fn view(
        );
    });
}

fn enter_game(commands: &mut Commands, global: &mut Global, game_id: &str) {
    global.cur_game_id = game_id.to_string();
    
    commands.insert_resource(NextState(Some(MenuState::None)));
    commands.insert_resource(NextState(Some(AppState::InGame)));

    // upon entering the InGame state, fetch details of the game
}

M client/src/plugins/menu/ui/mod.rs => client/src/plugins/menu/ui/mod.rs +7 -1
@@ 9,7 9,7 @@
use bevy::prelude::*;
use bevy_egui::{EguiContexts, egui};

use crate::plugins::{LoginCallEvent, RegisterCallEvent, GameCreateCallEvent, GameFormingCallEvent, GameMyGamesCallEvent};
use crate::{plugins::{LoginCallEvent, RegisterCallEvent, GameCreateCallEvent, GameFormingCallEvent, GameMyGamesCallEvent, GameJoinCallEvent}, Global};

pub use super::{MenuData, MenuUIState};



@@ 19,13 19,16 @@ mod register;
mod browse;

pub fn ui(
    mut commands: Commands,
    mut contexts: EguiContexts,
    mut global: ResMut<Global>,
    mut data: ResMut<MenuData>,
    mut login_ev_w: EventWriter<LoginCallEvent>,
    mut register_ev_w: EventWriter<RegisterCallEvent>,
    mut create_ev_w: EventWriter<GameCreateCallEvent>,
    mut forming_ev_w: EventWriter<GameFormingCallEvent>,
    mut mygames_ev_w: EventWriter<GameMyGamesCallEvent>,
    mut join_ev_w: EventWriter<GameJoinCallEvent>,
) {
    egui::Window::new(format!("{:?}", data.ui_state))
        .collapsible(false)


@@ 47,11 50,14 @@ pub fn ui(
                }
            }
            MenuUIState::Browse => browse::view(
                &mut commands,
                ui,
                &mut global,
                &mut data,
                &mut create_ev_w,
                &mut forming_ev_w,
                &mut mygames_ev_w,
                &mut join_ev_w,
            ),
        }
    });

M client/src/plugins/mod.rs => client/src/plugins/mod.rs +3 -0
@@ 9,5 9,8 @@
mod menu;
pub use menu::*;

mod game;
pub use game::*;

mod async_tasks;
pub use async_tasks::*;