Skip to content

Commit

Permalink
Merge pull request DigitalExtinction#347 from Indy2222/feature/create…
Browse files Browse the repository at this point in the history
…-game
  • Loading branch information
Indy2222 authored Jan 29, 2023
2 parents 496a4f6 + 5c91b95 commit df501a5
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 12 deletions.
32 changes: 31 additions & 1 deletion crates/gui/src/button.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use bevy::{ecs::system::EntityCommands, prelude::*};
use bevy::{
ecs::system::{EntityCommands, SystemParam},
prelude::*,
};

use crate::{GuiCommands, OuterStyle};

Expand Down Expand Up @@ -48,6 +51,33 @@ impl<'w, 's> ButtonCommands<'w, 's> for GuiCommands<'w, 's> {
}
}

#[derive(SystemParam)]
pub struct ButtonOps<'w, 's> {
button_query: Query<'w, 's, &'static Children, With<Button>>,
text_query: Query<'w, 's, &'static mut Text>,
}

impl<'w, 's> ButtonOps<'w, 's> {
/// This method changes text (e.g. caption) of a UI button.
///
/// The entity must have [`Button`] component and at least one child with
/// [`Text`] component. The text must consist of a single section. If such
/// a child is found, its text is changed.
pub fn set_text(&mut self, entity: Entity, text: String) -> Result<(), &'static str> {
let Ok(children) = self.button_query.get(entity) else { return Err("Button does not exist.") };
for &child in children.iter() {
if let Ok(mut text_component) = self.text_query.get_mut(child) {
if text_component.sections.len() == 1 {
text_component.sections[0].value = text;
return Ok(());
}
}
}

Err("Button does not have a child with a single-section text..")
}
}

type ButtonInteractions<'w, 'q> = Query<
'w,
'q,
Expand Down
2 changes: 1 addition & 1 deletion crates/gui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//! game.
use bevy::{app::PluginGroupBuilder, prelude::PluginGroup};
pub use button::ButtonCommands;
use button::ButtonPlugin;
pub use button::{ButtonCommands, ButtonOps};
pub use commands::GuiCommands;
use focus::FocusPlugin;
pub use focus::SetFocusEvent;
Expand Down
6 changes: 3 additions & 3 deletions crates/lobby/src/games/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async fn create(

let game = Game::new(game_config, claims.username().to_owned());
match games.create(game).await {
Ok(_) => HttpResponse::Ok().finish(),
Ok(_) => HttpResponse::Ok().json(()),
Err(CreationError::NameTaken) => {
warn!("Game creation error: game name is already taken.");
HttpResponse::Conflict().json("Game name is already taken.")
Expand Down Expand Up @@ -66,7 +66,7 @@ async fn join(
let name = path.into_inner();

match games.add_player(claims.username(), name.as_str()).await {
Ok(_) => HttpResponse::Ok().finish(),
Ok(_) => HttpResponse::Ok().json(()),
Err(AdditionError::AlreadyInAGame) => {
warn!("Game joining error: a user is already in a different game.");
HttpResponse::Forbidden().json("User is already in a different game.")
Expand All @@ -91,7 +91,7 @@ async fn leave(
let name = path.into_inner();

match games.remove_player(claims.username(), name.as_str()).await {
Ok(_) => HttpResponse::Ok().finish(),
Ok(_) => HttpResponse::Ok().json(()),
Err(RemovalError::NotInTheGame) => {
warn!("Game leaving error: the user is not in the game.");
HttpResponse::Forbidden().json("The user is not in the game.")
Expand Down
2 changes: 1 addition & 1 deletion crates/lobby_client/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl LobbyRequestCreator for CreateGameRequest {

fn create(&self, url: Url) -> Request {
let mut request = Request::new(Method::POST, url);
*request.body_mut() = Some(serde_json::to_string(&self.0).unwrap().into());
json(&mut request, &self.0);
request
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/map/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl MapHash {
}

/// Converts the map hash into a hexadecimal string.
fn to_hex(&self) -> String {
pub fn to_hex(&self) -> String {
let mut hex = String::with_capacity(64);
for &byte in self.0.iter() {
write!(&mut hex, "{byte:02x}").unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/map/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub const MAX_MAP_NAME_LEN: usize = 16;

/// General information about a map. It does not hold full content of the map
/// (i.e. location of objects on the map).
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct MapMetadata {
name: String,
bounds: MapBounds,
Expand Down
233 changes: 233 additions & 0 deletions crates/menu/src/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
use bevy::prelude::*;
use de_gui::{
ButtonCommands, ButtonOps, GuiCommands, LabelCommands, OuterStyle, TextBoxCommands, ToastEvent,
};
use de_lobby_model::GameMap;
use de_map::hash::MapHash;
use iyes_loopless::prelude::*;

use crate::{
mapselection::{MapSelectedEvent, SelectMapEvent},
menu::Menu,
MenuState,
};

pub(crate) struct CreateGamePlugin;

impl Plugin for CreateGamePlugin {
fn build(&self, app: &mut App) {
app.add_event::<CreateGameEvent>()
.add_enter_system(MenuState::GameCreation, setup)
.add_exit_system(MenuState::GameCreation, cleanup)
.add_system(
button_system
.run_in_state(MenuState::GameCreation)
.label(CreateLabel::Buttons),
)
.add_system(
map_selected_system
.run_in_state(MenuState::GameCreation)
.label(CreateLabel::MapSelected),
)
.add_system(
create_game_system
.run_in_state(MenuState::GameCreation)
.after(CreateLabel::Buttons)
.after(CreateLabel::MapSelected),
);
}
}

#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, SystemLabel)]
pub(crate) enum CreateLabel {
Buttons,
MapSelected,
}

#[derive(Component, Clone, Copy)]
enum ButtonAction {
SelectMap,
Create,
}

#[derive(Resource)]
struct Inputs {
map: Entity,
}

#[derive(Resource)]
struct SelectedMap(GameMap);

struct CreateGameEvent;

fn setup(mut commands: GuiCommands, menu: Res<Menu>) {
let column_id = column(&mut commands, menu.root_node());

let name_row_id = row(&mut commands, column_id);
text_input(&mut commands, name_row_id, "Name");

let max_players_row_id = row(&mut commands, column_id);
text_input(&mut commands, max_players_row_id, "Max Players");

let map_row_id = row(&mut commands, column_id);
let map_id = map_button(&mut commands, map_row_id);

commands.insert_resource(Inputs { map: map_id });

let buttons_row_id = row(&mut commands, column_id);
let create_id = commands
.spawn_button(
OuterStyle {
size: Size::new(Val::Percent(100.), Val::Percent(100.)),
..default()
},
"Create Game",
)
.insert(ButtonAction::Create)
.id();
commands.entity(buttons_row_id).add_child(create_id);
}

fn column(commands: &mut GuiCommands, parent_id: Entity) -> Entity {
let column_id = commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
size: Size::new(Val::Percent(50.), Val::Percent(100.)),
margin: UiRect::all(Val::Auto),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
..default()
})
.id();
commands.entity(parent_id).add_child(column_id);
column_id
}

fn row(commands: &mut GuiCommands, parent_id: Entity) -> Entity {
let row_id = commands
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Row,
size: Size::new(Val::Percent(100.), Val::Percent(8.)),
margin: UiRect::new(
Val::Percent(0.),
Val::Percent(0.),
Val::Percent(2.),
Val::Percent(2.),
),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
},
..default()
})
.id();
commands.entity(parent_id).add_child(row_id);
row_id
}

fn text_input(commands: &mut GuiCommands, parent_id: Entity, caption: &str) -> Entity {
spawn_caption(commands, parent_id, caption);

let input_id = commands
.spawn_text_box(
OuterStyle {
size: Size::new(Val::Percent(65.), Val::Percent(100.)),
..default()
},
false,
)
.id();
commands.entity(parent_id).add_child(input_id);
input_id
}

fn map_button(commands: &mut GuiCommands, parent_id: Entity) -> Entity {
spawn_caption(commands, parent_id, "Map");

let input_id = commands
.spawn_button(
OuterStyle {
size: Size::new(Val::Percent(65.), Val::Percent(100.)),
..default()
},
"-",
)
.insert(ButtonAction::SelectMap)
.id();
commands.entity(parent_id).add_child(input_id);
input_id
}

fn spawn_caption(commands: &mut GuiCommands, parent_id: Entity, caption: &str) {
let caption_id = commands
.spawn_label(
OuterStyle {
size: Size::new(Val::Percent(35.), Val::Percent(100.)),
..default()
},
caption,
)
.id();
commands.entity(parent_id).add_child(caption_id);
}

fn cleanup(mut commands: GuiCommands) {
commands.remove_resource::<Inputs>();
commands.remove_resource::<SelectedMap>();
}

fn button_system(
interactions: Query<(&Interaction, &ButtonAction), Changed<Interaction>>,
mut map_events: EventWriter<SelectMapEvent>,
mut create_events: EventWriter<CreateGameEvent>,
) {
for (&interaction, &action) in interactions.iter() {
if let Interaction::Clicked = interaction {
match action {
ButtonAction::SelectMap => map_events.send(SelectMapEvent),
ButtonAction::Create => create_events.send(CreateGameEvent),
}
}
}
}

fn map_selected_system(
mut commands: Commands,
mut map_selected_events: EventReader<MapSelectedEvent>,
intpus: Res<Inputs>,
mut buttons: ButtonOps,
mut toasts: EventWriter<ToastEvent>,
) {
let Some(event) = map_selected_events.iter().last() else { return };
let hash = match MapHash::try_from(event.path()) {
Ok(hash) => hash,
Err(error) => {
toasts.send(ToastEvent::new(format!("Map error: {error}")));
return;
}
};

buttons
.set_text(intpus.map, event.metadata().name().to_owned())
.unwrap();
commands.insert_resource(SelectedMap(GameMap::new(
hash.to_hex(),
event.metadata().name().to_owned(),
)));
}

fn create_game_system(
mut events: EventReader<CreateGameEvent>,
mut toasts: EventWriter<ToastEvent>,
) {
// Always exhaust the iterator
if events.iter().count() == 0 {
return;
}

toasts.send(ToastEvent::new("Not yet implemented."));
}
3 changes: 2 additions & 1 deletion crates/menu/src/gamelisting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,15 @@ fn list_games_system(
}

fn button_system(
mut commands: Commands,
interactions: Query<(&Interaction, &ButtonAction), Changed<Interaction>>,
mut toasts: EventWriter<ToastEvent>,
) {
for (&interaction, action) in interactions.iter() {
if let Interaction::Clicked = interaction {
match action {
ButtonAction::Create => {
toasts.send(ToastEvent::new("Not yet implemented (issue #326)."))
commands.insert_resource(NextState(MenuState::GameCreation))
}
ButtonAction::Join => {
toasts.send(ToastEvent::new("Not yet implemented (issue #301)."))
Expand Down
4 changes: 4 additions & 0 deletions crates/menu/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy::{app::PluginGroupBuilder, prelude::*};
use create::CreateGamePlugin;
use de_core::state::AppState;
use gamelisting::GameListingPlugin;
use iyes_loopless::prelude::*;
Expand All @@ -9,6 +10,7 @@ use menu::MenuPlugin;
use signin::SignInPlugin;
use singleplayer::SinglePlayerPlugin;

mod create;
mod gamelisting;
mod mainmenu;
mod mapselection;
Expand All @@ -28,6 +30,7 @@ impl PluginGroup for MenuPluginGroup {
.add(SignInPlugin)
.add(GameListingPlugin)
.add(SinglePlayerPlugin)
.add(CreateGamePlugin)
}
}

Expand All @@ -47,6 +50,7 @@ pub(crate) enum MenuState {
SinglePlayerGame,
SignIn,
GameListing,
GameCreation,
}

fn menu_entered_system(mut commands: Commands) {
Expand Down
Loading

0 comments on commit df501a5

Please sign in to comment.