diff --git a/client/assets/button_background.png b/client/assets/button_background.png deleted file mode 100644 index a8b01f3..0000000 Binary files a/client/assets/button_background.png and /dev/null differ diff --git a/client/assets/button_clicked_background.png b/client/assets/button_clicked_background.png deleted file mode 100644 index d6c6654..0000000 Binary files a/client/assets/button_clicked_background.png and /dev/null differ diff --git a/client/assets/button_hovered_background.png b/client/assets/button_hovered_background.png deleted file mode 100644 index d004de3..0000000 Binary files a/client/assets/button_hovered_background.png and /dev/null differ diff --git a/client/assets/restore-svgrepo-com.png b/client/assets/restore-svgrepo-com.png deleted file mode 100644 index 37c865a..0000000 Binary files a/client/assets/restore-svgrepo-com.png and /dev/null differ diff --git a/client/assets/window_background.png b/client/assets/window_background.png deleted file mode 100644 index 2bd90b3..0000000 Binary files a/client/assets/window_background.png and /dev/null differ diff --git a/client/src/advance_ui.rs b/client/src/advance_ui.rs index ef2d9e8..fc69a89 100644 --- a/client/src/advance_ui.rs +++ b/client/src/advance_ui.rs @@ -1,12 +1,16 @@ use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; -use crate::dialog_ui::dialog; +use crate::layout_ui::{left_mouse_button_pressed_in_rect, top_center_text}; use crate::payment_ui::{payment_dialog, HasPayment, Payment, ResourcePayment}; +use crate::player_ui::player_color; use crate::resource_ui::{new_resource_map, ResourceType}; use crate::select_ui::HasCountSelectableObject; +use crate::tooltip::show_tooltip_for_rect; use itertools::Itertools; -use macroquad::hash; +use macroquad::color::Color; use macroquad::math::{bool, vec2, Vec2}; -use macroquad::ui::widgets::Checkbox; +use macroquad::prelude::{ + draw_rectangle, draw_rectangle_lines, Rect, BLACK, BLUE, GRAY, WHITE, YELLOW, +}; use server::action::Action; use server::advance::{Advance, Bonus}; use server::content::advances; @@ -78,8 +82,8 @@ impl HasPayment for AdvancePayment { } } -pub fn show_advance_menu(game: &Game, player: &ShownPlayer) -> StateUpdate { - show_generic_advance_menu("Advances", game, player, |a| { +pub fn show_advance_menu(game: &Game, player: &ShownPlayer, state: &State) -> StateUpdate { + show_generic_advance_menu("Advances", game, player, state, |a| { StateUpdate::SetDialog(ActiveDialog::AdvancePayment(AdvancePayment::new( game, player.index, @@ -88,11 +92,12 @@ pub fn show_advance_menu(game: &Game, player: &ShownPlayer) -> StateUpdate { }) } -pub fn show_free_advance_menu(game: &Game, player: &ShownPlayer) -> StateUpdate { - show_generic_advance_menu("Select a free advance", game, player, |a| { - if can_advance(game, player, a) { +pub fn show_free_advance_menu(game: &Game, player: &ShownPlayer, state: &State) -> StateUpdate { + show_generic_advance_menu("Select a free advance", game, player, state, |a| { + let p = player.get(game); + if can_advance(game, p, a) { return StateUpdate::execute_with_confirm( - description(game.get_player(player.index), a), + description(p, a), Action::StatusPhase(StatusPhaseAction::FreeAdvance(a.name.clone())), ); } @@ -101,55 +106,94 @@ pub fn show_free_advance_menu(game: &Game, player: &ShownPlayer) -> StateUpdate } fn advance_info(game: &Game, player: &ShownPlayer, a: &Advance) -> StateUpdate { - StateUpdate::execute_with_cancel(description(game.get_player(player.index), a)) + StateUpdate::execute_with_cancel(description(player.get(game), a)) } pub fn show_generic_advance_menu( title: &str, game: &Game, player: &ShownPlayer, + state: &State, new_update: impl Fn(&Advance) -> StateUpdate, ) -> StateUpdate { - dialog(player, title, |ui| { - let p = player.get(game); + top_center_text(state, title, vec2(0., 10.)); + let p = player.get(game); + for pass in 0..2 { for advances in groups() { - let pos = group_pos(&advances[0]); + let (group_name, pos) = group_info(&advances[0]); + let pos = pos * vec2(140., 180.) + vec2(20., 70.); + if pass == 0 { + state.draw_text( + group_name, + pos.x + (140. - state.measure_text(group_name).width) / 2., + pos.y - 15., + ); + } + for (i, a) in advances.into_iter().enumerate() { - let pos = pos * vec2(140., 210.) + vec2(0., i as f32 * 35.); + let pos = pos + vec2(0., i as f32 * 35.); let name = &a.name; - let can_advance = can_advance(game, player, &a); + let can_advance = can_advance(game, p, &a); - if can_advance || p.has_advance(name) { - let mut data = p.has_advance(name); - Checkbox::new(hash!(name)) - // .label(name) - .pos(pos + vec2(60., 50.)) - .size(vec2(0., 0.)) - .ui(ui, &mut data); - if data != p.has_advance(name) { + let rect = Rect::new(pos.x, pos.y, 135., 30.); + if pass == 0 { + draw_rectangle( + rect.x, + rect.y, + rect.w, + rect.h, + fill_color(p, name, can_advance), + ); + state.draw_text(name, pos.x + 10., pos.y + 22.); + + draw_rectangle_lines(rect.x, rect.y, rect.w, rect.h, 4., border_color(&a)); + } else { + // tooltip should be shown on top of everything + show_tooltip_for_rect(state, &description(p, &a), rect); + + if player.can_control + && can_advance + && left_mouse_button_pressed_in_rect(rect, state) + { return new_update(&a); } } - // Button::new(name.clone()).position(pos + vec2(0., 0.)).ui(ui); - ui.label(pos + vec2(0., 0.), name); } } - StateUpdate::None - }) + } + StateUpdate::None +} + +fn fill_color(p: &Player, name: &str, can_advance: bool) -> Color { + if can_advance { + WHITE + } else if p.has_advance(name) { + player_color(p.index) + } else { + GRAY + } } -fn can_advance(game: &Game, player: &ShownPlayer, a: &Advance) -> bool { +fn border_color(a: &Advance) -> Color { + if let Some(b) = &a.bonus { + match b { + Bonus::MoodToken => YELLOW, + Bonus::CultureToken => BLUE, + } + } else { + BLACK + } +} + +fn can_advance(game: &Game, p: &Player, a: &Advance) -> bool { let name = &a.name; - let p = player.get(game); - if player.can_play_action { + if game.state == GameState::Playing && game.actions_left > 0 { p.can_advance(name) - } else if player.can_control - && matches!( - game.state, - GameState::StatusPhase(StatusPhaseState::FreeAdvance) - ) - { + } else if matches!( + game.state, + GameState::StatusPhase(StatusPhaseState::FreeAdvance) + ) { p.can_advance_free(name) } else { false @@ -173,16 +217,16 @@ fn groups() -> Vec> { .collect::>() } -fn group_pos(advance: &Advance) -> Vec2 { +fn group_info(advance: &Advance) -> (&str, Vec2) { match advance.name.as_str() { - "Farming" => vec2(0., 0.), - "Mining" => vec2(1., 0.), - "Fishing" => vec2(2., 0.), - "Philosophy" => vec2(3., 0.), - "Tactics" => vec2(4., 0.), - "Math" => vec2(2., 1.), - "Voting" => vec2(3., 1.), - "Dogma" => vec2(5., 1.), + "Farming" => ("Agriculture", vec2(0., 0.)), + "Mining" => ("Construction", vec2(1., 0.)), + "Fishing" => ("Seafaring", vec2(2., 0.)), + "Philosophy" => ("Education", vec2(3., 0.)), + "Tactics" => ("Warfare", vec2(4., 0.)), + "Math" => ("Science", vec2(2., 1.)), + "Voting" => ("Democracy", vec2(3., 1.)), + "Dogma" => ("Theocracy", vec2(5., 1.)), _ => panic!("Unknown advance: {}", advance.name), } } @@ -228,9 +272,8 @@ pub fn pay_advance_dialog( ) -> StateUpdate { let a = advances::get_advance_by_name(ap.name.as_str()).unwrap(); - if can_advance(game, player, &a) { + if can_advance(game, player.get(game), &a) { payment_dialog( - player, ap, AdvancePayment::valid, |ap| { diff --git a/client/src/assets.rs b/client/src/assets.rs index 0b5f9b5..81d3c71 100644 --- a/client/src/assets.rs +++ b/client/src/assets.rs @@ -1,10 +1,7 @@ use crate::client::Features; use crate::resource_ui::ResourceType; -use macroquad::prelude::{ - load_texture, load_ttf_font, Color, Font, Image, ImageFormat, RectOffset, -}; +use macroquad::prelude::{load_texture, load_ttf_font, Font, ImageFormat}; use macroquad::texture::Texture2D; -use macroquad::ui::{root_ui, Skin}; use server::city_pieces::Building; use server::map::Terrain; use server::unit::UnitType; @@ -14,7 +11,6 @@ pub struct Assets { pub terrain: HashMap, pub exhausted: Texture2D, pub units: HashMap, - pub skin: Skin, pub font: Font, // mood icons @@ -36,7 +32,6 @@ pub struct Assets { pub ok_blocked: Texture2D, pub ok: Texture2D, pub cancel: Texture2D, - pub restore_menu: Texture2D, pub zoom_in: Texture2D, pub zoom_out: Texture2D, @@ -64,7 +59,6 @@ impl Assets { terrain: Self::terrain(features).await, exhausted: load_png(include_bytes!("../assets/cross-svgrepo-com.png")), units: Self::units(), - skin: Self::skin(&load_ttf_font(&font_name).await.unwrap()), angry: load_png(include_bytes!("../assets/angry-face-svgrepo-com.png")), resources: Self::resources(), @@ -87,7 +81,6 @@ impl Assets { ok: load_png(include_bytes!("../assets/ok-circle-svgrepo-com.png")), ok_blocked: load_png(include_bytes!("../assets/in-progress-svgrepo-com.png")), cancel: load_png(include_bytes!("../assets/cancel-svgrepo-com.png")), - restore_menu: load_png(include_bytes!("../assets/restore-svgrepo-com.png")), zoom_in: load_png(include_bytes!("../assets/zoom-in-1462-svgrepo-com.png")), zoom_out: load_png(include_bytes!("../assets/zoom-out-1460-svgrepo-com.png")), @@ -244,106 +237,6 @@ impl Assets { } map } - - fn skin(font: &Font) -> Skin { - let image = - Image::from_file_with_format(include_bytes!("../assets/button_background.png"), None) - .unwrap(); - let label_style = root_ui() - .style_builder() - .background(image.clone()) - .background_margin(RectOffset::new(37.0, 37.0, 5.0, 5.0)) - .margin(RectOffset::new(10.0, 10.0, 0.0, 0.0)) - .with_font(font) - .unwrap() - .text_color(Color::from_rgba(180, 180, 120, 255)) - .font_size(20) - .build(); - - let window_style = root_ui() - .style_builder() - .background( - Image::from_file_with_format( - include_bytes!("../assets/window_background.png"), - None, - ) - .unwrap(), - ) - .background_margin(RectOffset::new(20.0, 20.0, 10.0, 10.0)) - .margin(RectOffset::new(-20.0, -30.0, 0.0, 0.0)) - .build(); - - let button_style = root_ui() - .style_builder() - .background(image) - .background_margin(RectOffset::new(37.0, 37.0, 5.0, 5.0)) - .margin(RectOffset::new(10.0, 10.0, 0.0, 0.0)) - .background_hovered( - Image::from_file_with_format( - include_bytes!("../assets/button_hovered_background.png"), - None, - ) - .unwrap(), - ) - .background_clicked( - Image::from_file_with_format( - include_bytes!("../assets/button_clicked_background.png"), - None, - ) - .unwrap(), - ) - .with_font(font) - .unwrap() - .text_color(Color::from_rgba(180, 180, 100, 255)) - .font_size(20) - .build(); - - let editbox_style = root_ui() - .style_builder() - .background_margin(RectOffset::new(0., 0., 0., 0.)) - .with_font(font) - .unwrap() - .text_color(Color::from_rgba(120, 120, 120, 255)) - .color_selected(Color::from_rgba(190, 190, 190, 255)) - .font_size(50) - .build(); - - // let checkbox_style = root_ui() - // .style_builder() - // .background( - // Image::from_file_with_format( - // include_bytes!("../examples/ui_assets/checkbox_background.png"), - // None, - // ) - // .unwrap(), - // ) - // .background_hovered( - // Image::from_file_with_format( - // include_bytes!("../examples/ui_assets/checkbox_hovered_background.png"), - // None, - // ) - // .unwrap(), - // ) - // .background_clicked( - // Image::from_file_with_format( - // include_bytes!("../examples/ui_assets/checkbox_clicked_background.png"), - // None, - // ) - // .unwrap(), - // ) - // .build(); - - Skin { - editbox_style, - window_style, - button_style, - window_titlebar_style: label_style.clone(), - label_style, - // checkbox_style, - title_height: 30., - ..root_ui().default_skin() - } - } } fn load_png(bytes: &[u8]) -> Texture2D { diff --git a/client/src/city_ui.rs b/client/src/city_ui.rs index 3987a2c..ce78884 100644 --- a/client/src/city_ui.rs +++ b/client/src/city_ui.rs @@ -1,4 +1,5 @@ use macroquad::prelude::*; +use std::ops::Add; use server::city::{City, MoodState}; use server::city_pieces::Building; @@ -190,29 +191,30 @@ pub fn draw_city(owner: &Player, city: &City, state: &State) { } draw_circle(c.x, c.y, 15.0, player_ui::player_color(owner.index)); - if let ActiveDialog::IncreaseHappiness(increase) = &state.active_dialog { + let mood = if let ActiveDialog::IncreaseHappiness(increase) = &state.active_dialog { let steps = increase .steps .iter() .find(|(p, _)| p == &city.position) - .map_or(String::new(), |(_, s)| format!("{s}")); - state.draw_text(&steps, c.x - 5., c.y + 6.); + .map_or(&0, |(_, s)| s); + &city.mood_state.clone().add(*steps) } else { - let t = match city.mood_state { - MoodState::Happy => Some(&state.assets.resources[&ResourceType::MoodTokens]), - MoodState::Neutral => None, - MoodState::Angry => Some(&state.assets.angry), - }; - if let Some(t) = t { - let size = 15.; - draw_scaled_icon( - state, - t, - &format!("Happiness: {:?}", city.mood_state), - c.to_vec2() + vec2(-size / 2., -size / 2.), - size, - ); - } + &city.mood_state + }; + let t = match mood { + MoodState::Happy => Some(&state.assets.resources[&ResourceType::MoodTokens]), + MoodState::Neutral => None, + MoodState::Angry => Some(&state.assets.angry), + }; + if let Some(t) = t { + let size = 15.; + draw_scaled_icon( + state, + t, + &format!("Happiness: {:?}", city.mood_state), + c.to_vec2() + vec2(-size / 2., -size / 2.), + size, + ); } let mut i = 0; diff --git a/client/src/client.rs b/client/src/client.rs index 7a4a8ff..a503611 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1,7 +1,6 @@ use macroquad::input::{is_mouse_button_pressed, mouse_position, MouseButton}; use macroquad::prelude::*; use macroquad::prelude::{clear_background, vec2}; -use macroquad::ui::root_ui; use server::action::Action; use server::game::Game; @@ -11,20 +10,20 @@ use crate::advance_ui::{pay_advance_dialog, show_advance_menu, show_free_advance use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate, StateUpdates}; use crate::collect_ui::collect_resources_dialog; use crate::construct_ui::pay_construction_dialog; -use crate::happiness_ui::{increase_happiness_dialog, increase_happiness_menu}; +use crate::happiness_ui::{increase_happiness_click, increase_happiness_menu}; use crate::hex_ui::pixel_to_coordinate; +use crate::layout_ui::{icon_pos, top_right_texture}; use crate::log_ui::show_log; use crate::map_ui::{draw_map, show_tile_menu}; use crate::player_ui::{player_select, show_global_controls, show_top_center, show_top_left}; use crate::status_phase_ui::raze_city_confirm_dialog; +use crate::unit_ui::unit_selection_click; use crate::{ combat_ui, dialog_ui, influence_ui, move_ui, recruit_unit_ui, status_phase_ui, tooltip, }; pub async fn init(features: &Features) -> State { - let state = State::new(features).await; - root_ui().push_skin(&state.assets.skin); - state + State::new(features).await } pub fn render_and_update( @@ -63,65 +62,94 @@ fn render(game: &Game, state: &mut State, features: &Features) -> StateUpdate { }; let mut updates = StateUpdates::new(); - updates.add(draw_map(game, state)); - show_top_left(game, player, state); - show_top_center(game, player, state); - updates.add(player_select(game, player, state)); - updates.add(show_global_controls(game, state, features)); + if !state.active_dialog.is_modal() { + updates.add(draw_map(game, state)); + } + if !state.active_dialog.is_full_modal() { + show_top_left(game, player, state); + } + if !state.active_dialog.is_modal() { + show_top_center(game, player, state); + } + if !state.active_dialog.is_full_modal() { + updates.add(player_select(game, player, state)); + updates.add(show_global_controls(game, state, features)); + } + + if top_right_texture(state, &state.assets.log, icon_pos(-1, 0), "Show log") { + if let ActiveDialog::Log = state.active_dialog { + return StateUpdate::CloseDialog; + } + return StateUpdate::OpenDialog(ActiveDialog::Log); + }; + if top_right_texture( + state, + &state.assets.advances, + icon_pos(-2, 0), + "Show advances", + ) { + if let ActiveDialog::AdvanceMenu = state.active_dialog { + return StateUpdate::CloseDialog; + } + return StateUpdate::OpenDialog(ActiveDialog::AdvanceMenu); + }; if player.can_control { if let Some(u) = &state.pending_update { - updates.add(dialog_ui::show_pending_update(u, player)); + updates.add(dialog_ui::show_pending_update(u, state)); return updates.result(); } } - updates.add(match &state.active_dialog { + if player.can_control || state.active_dialog.show_for_other_player() { + updates.add(render_active_dialog(game, state, player)); + } + + if player.can_control { + updates.add(try_click(game, state, player)); + } + updates.result() +} + +fn render_active_dialog(game: &Game, state: &mut State, player: &ShownPlayer) -> StateUpdate { + match &state.active_dialog { ActiveDialog::None | ActiveDialog::MoveUnits(_) + | ActiveDialog::DetermineFirstPlayer | ActiveDialog::WaitingForUpdate - | ActiveDialog::CulturalInfluence => StateUpdate::None, - ActiveDialog::Log => show_log(game, player), + | ActiveDialog::CulturalInfluence + | ActiveDialog::PlaceSettler => StateUpdate::None, + ActiveDialog::Log => show_log(game, state), ActiveDialog::TileMenu(p) => show_tile_menu(game, *p, player, state), // playing actions - ActiveDialog::IncreaseHappiness(h) => increase_happiness_menu(h, player), - ActiveDialog::AdvanceMenu => show_advance_menu(game, player), + ActiveDialog::IncreaseHappiness(h) => increase_happiness_menu(h, player, state, game), + ActiveDialog::AdvanceMenu => show_advance_menu(game, player, state), ActiveDialog::AdvancePayment(p) => pay_advance_dialog(p, player, game, state), - ActiveDialog::ConstructionPayment(p) => pay_construction_dialog(game, p, player, state), + ActiveDialog::ConstructionPayment(p) => pay_construction_dialog(game, p, state), ActiveDialog::CollectResources(c) => collect_resources_dialog(game, c, state), ActiveDialog::RecruitUnitSelection(s) => { recruit_unit_ui::select_dialog(game, s, player, state) } - ActiveDialog::ReplaceUnits(r) => recruit_unit_ui::replace_dialog(game, r, player), - ActiveDialog::CulturalInfluenceResolution(c) => { - influence_ui::cultural_influence_resolution_dialog(c, player) + ActiveDialog::ReplaceUnits(r) => recruit_unit_ui::replace_dialog(game, r, state), + ActiveDialog::CulturalInfluenceResolution(_) => { + influence_ui::cultural_influence_resolution_dialog(state) } //status phase - ActiveDialog::FreeAdvance => show_free_advance_menu(game, player), - ActiveDialog::RazeSize1City => status_phase_ui::raze_city_dialog(player), - ActiveDialog::CompleteObjectives => status_phase_ui::complete_objectives_dialog(player), - ActiveDialog::DetermineFirstPlayer => { - status_phase_ui::determine_first_player_dialog(game, player) - } - ActiveDialog::ChangeGovernmentType => { - status_phase_ui::change_government_type_dialog(game, player) - } + ActiveDialog::FreeAdvance => show_free_advance_menu(game, player, state), + ActiveDialog::RazeSize1City => status_phase_ui::raze_city_dialog(state), + ActiveDialog::CompleteObjectives => status_phase_ui::complete_objectives_dialog(state), + ActiveDialog::ChangeGovernmentType => status_phase_ui::change_government_type_dialog(), ActiveDialog::ChooseAdditionalAdvances(a) => { - status_phase_ui::choose_additional_advances_dialog(game, a, player) + status_phase_ui::choose_additional_advances_dialog(game, a, state) } //combat - ActiveDialog::PlayActionCard => combat_ui::play_action_card_dialog(player), - ActiveDialog::PlaceSettler => combat_ui::place_settler_dialog(player), - ActiveDialog::Retreat => combat_ui::retreat_dialog(player), - ActiveDialog::RemoveCasualties(s) => combat_ui::remove_casualties_dialog(game, s, player), - }); - - updates.add(try_click(game, state, player)); - - updates.result() + ActiveDialog::PlayActionCard => combat_ui::play_action_card_dialog(state), + ActiveDialog::Retreat => combat_ui::retreat_dialog(state), + ActiveDialog::RemoveCasualties(s) => combat_ui::remove_casualties_dialog(game, s, state), + } } pub fn try_click(game: &Game, state: &mut State, player: &ShownPlayer) -> StateUpdate { @@ -132,36 +160,37 @@ pub fn try_click(game: &Game, state: &mut State, player: &ShownPlayer) -> StateU return StateUpdate::None; } - if player.can_control { - if let ActiveDialog::CulturalInfluence = state.active_dialog { - return influence_ui::hover(pos, game, player, mouse_pos, state); - } + if let ActiveDialog::CulturalInfluence = state.active_dialog { + return influence_ui::hover(pos, game, player, mouse_pos, state); } if !is_mouse_button_pressed(MouseButton::Left) { return StateUpdate::None; } - if player.can_control { - match &state.active_dialog { - ActiveDialog::RemoveCasualties(_) | ActiveDialog::CollectResources(_) => { + match &state.active_dialog { + ActiveDialog::CollectResources(_) => StateUpdate::None, + ActiveDialog::MoveUnits(s) => move_ui::click(pos, s, mouse_pos, game), + ActiveDialog::RemoveCasualties(s) => { + unit_selection_click(game, player, pos, mouse_pos, s, |new| { + StateUpdate::SetDialog(ActiveDialog::RemoveCasualties(new.clone())) + }) + } + ActiveDialog::ReplaceUnits(s) => { + unit_selection_click(game, player, pos, mouse_pos, s, |new| { + StateUpdate::SetDialog(ActiveDialog::ReplaceUnits(new.clone())) + }) + } + ActiveDialog::RazeSize1City => raze_city_confirm_dialog(game, player, pos), + ActiveDialog::PlaceSettler => { + if player.get(game).get_city(pos).is_some() { + StateUpdate::Execute(Action::PlaceSettler(pos)) + } else { StateUpdate::None } - ActiveDialog::MoveUnits(s) => move_ui::click(pos, s, mouse_pos, game), - ActiveDialog::ReplaceUnits(r) => recruit_unit_ui::click_replace(pos, r), - ActiveDialog::RazeSize1City => raze_city_confirm_dialog(game, player, pos), - ActiveDialog::PlaceSettler => { - if player.get(game).get_city(pos).is_some() { - StateUpdate::Execute(Action::PlaceSettler(pos)) - } else { - StateUpdate::None - } - } - ActiveDialog::IncreaseHappiness(h) => increase_happiness_dialog(game, player, pos, h), - _ => StateUpdate::OpenDialog(ActiveDialog::TileMenu(pos)), } - } else { - StateUpdate::OpenDialog(ActiveDialog::TileMenu(pos)) + ActiveDialog::IncreaseHappiness(h) => increase_happiness_click(game, player, pos, h), + _ => StateUpdate::OpenDialog(ActiveDialog::TileMenu(pos)), } } diff --git a/client/src/client_state.rs b/client/src/client_state.rs index 01717de..61f6efc 100644 --- a/client/src/client_state.rs +++ b/client/src/client_state.rs @@ -9,6 +9,7 @@ use server::status_phase::{StatusPhaseAction, StatusPhaseState}; use crate::advance_ui::AdvancePayment; use crate::assets::Assets; +use crate::city_ui::building_name; use crate::client::{Features, GameSyncRequest}; use crate::collect_ui::CollectResources; use crate::combat_ui::RemoveCasualtiesSelection; @@ -113,7 +114,11 @@ impl ActiveDialog { ActiveDialog::CulturalInfluence => { vec!["Click on a building to influence its culture".to_string()] } - ActiveDialog::CulturalInfluenceResolution(_) => vec!["todo".to_string()], + ActiveDialog::CulturalInfluenceResolution(c) => vec![format!( + "Pay {} culture tokens to influence {}", + c.roll_boost_cost, + building_name(&c.city_piece) + )], ActiveDialog::FreeAdvance => { vec!["Click on an advance to take it for free".to_string()] } @@ -134,31 +139,28 @@ impl ActiveDialog { } ActiveDialog::PlayActionCard => vec!["Click on an action card to play it".to_string()], ActiveDialog::PlaceSettler => vec!["Click on a tile to place a settler".to_string()], - ActiveDialog::Retreat => vec!["Click on a unit to retreat".to_string()], - ActiveDialog::RemoveCasualties(_) => vec!["Click on a unit to remove it".to_string()], + ActiveDialog::Retreat => vec!["Do you want to retreat?".to_string()], + ActiveDialog::RemoveCasualties(r) => vec![format!( + "Remove {} units: click on a unit to remove it", + r.needed + )], ActiveDialog::WaitingForUpdate => vec!["Waiting for server update".to_string()], } } #[must_use] - pub fn is_map_dialog(&self) -> bool { - matches!( - self, - ActiveDialog::TileMenu(_) - | ActiveDialog::IncreaseHappiness(_) - | ActiveDialog::CollectResources(_) - | ActiveDialog::MoveUnits(_) - | ActiveDialog::PlaceSettler - | ActiveDialog::RazeSize1City - ) + pub fn show_for_other_player(&self) -> bool { + matches!(self, ActiveDialog::Log | ActiveDialog::AdvanceMenu) } #[must_use] - pub fn can_restore(&self) -> bool { - !matches!( - self, - ActiveDialog::MoveUnits(_) | ActiveDialog::ReplaceUnits(_) | ActiveDialog::None - ) + pub fn is_modal(&self) -> bool { + matches!(self, ActiveDialog::Log | ActiveDialog::AdvanceMenu) + } + + #[must_use] + pub fn is_full_modal(&self) -> bool { + matches!(self, ActiveDialog::AdvanceMenu) } } @@ -408,13 +410,6 @@ impl State { } fn open_dialog(&mut self, dialog: ActiveDialog) { - if self.active_dialog.title() == dialog.title() { - self.close_dialog(); - return; - } - if matches!(self.active_dialog, ActiveDialog::TileMenu(_)) { - self.close_dialog(); - } self.active_dialog = dialog; } @@ -423,11 +418,7 @@ impl State { } fn close_dialog(&mut self) { - if self.active_dialog.can_restore() { - self.active_dialog = ActiveDialog::None; - } else if let ActiveDialog::ReplaceUnits(r) = &mut self.active_dialog { - r.clear(); - } + self.active_dialog = ActiveDialog::None; } pub fn update_from_game(&mut self, game: &Game) -> GameSyncRequest { diff --git a/client/src/collect_ui.rs b/client/src/collect_ui.rs index 2a51144..d4fa449 100644 --- a/client/src/collect_ui.rs +++ b/client/src/collect_ui.rs @@ -2,9 +2,8 @@ use std::collections::HashMap; use std::iter; use macroquad::color::BLACK; -use macroquad::input::MouseButton; use macroquad::math::{i32, vec2}; -use macroquad::prelude::{is_mouse_button_pressed, mouse_position, WHITE}; +use macroquad::prelude::WHITE; use macroquad::shapes::draw_circle; use server::action::Action; use server::consts::PORT_CHOICES; @@ -17,7 +16,9 @@ use crate::client_state::{ActiveDialog, State, StateUpdate}; use crate::dialog_ui::{cancel_button, ok_button}; use crate::hex_ui; use crate::hex_ui::Point; -use crate::layout_ui::{draw_icon, draw_scaled_icon, is_in_circle, ICON_SIZE}; +use crate::layout_ui::{ + draw_icon, draw_scaled_icon, is_in_circle, left_mouse_button_pressed, ICON_SIZE, +}; use crate::resource_ui::{new_resource_map, resource_name, resource_types, ResourceType}; #[derive(Clone)] @@ -163,11 +164,10 @@ pub fn draw_resource_collect_tile(state: &State, pos: Position) -> StateUpdate { WHITE }; draw_circle(center.x, center.y, 20., color); - let (x, y) = mouse_position(); - if is_mouse_button_pressed(MouseButton::Left) - && is_in_circle(state.screen_to_world(vec2(x, y)), center, 20.) - { - return click_collect_option(collect, pos, pile); + if let Some(p) = left_mouse_button_pressed(state) { + if is_in_circle(p, center, 20.) { + return click_collect_option(collect, pos, pile); + } } let map = new_resource_map(pile); diff --git a/client/src/combat_ui.rs b/client/src/combat_ui.rs index 7d799f1..98b6355 100644 --- a/client/src/combat_ui.rs +++ b/client/src/combat_ui.rs @@ -3,34 +3,26 @@ use server::game::Game; use server::position::Position; use server::unit::Unit; -use crate::client_state::{ActiveDialog, ShownPlayer, StateUpdate}; -use crate::dialog_ui::active_dialog_window; +use crate::client_state::{State, StateUpdate}; +use crate::dialog_ui::{cancel_button_with_tooltip, ok_button_with_tooltip}; use crate::select_ui::{ConfirmSelection, SelectionConfirm}; use crate::unit_ui; use crate::unit_ui::UnitSelection; -pub fn retreat_dialog(player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Do you want to retreat?", |ui| { - if ui.button(None, "Retreat") { - return retreat(true); - } - if ui.button(None, "Decline") { - return retreat(false); - } - StateUpdate::None - }) +pub fn retreat_dialog(state: &State) -> StateUpdate { + if ok_button_with_tooltip(state, true, "Retreat") { + return retreat(true); + } + if cancel_button_with_tooltip(state, "Decline") { + return retreat(false); + } + StateUpdate::None } fn retreat(retreat: bool) -> StateUpdate { StateUpdate::Execute(Action::Combat(CombatAction::Retreat(retreat))) } -pub fn place_settler_dialog(player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Select a city to place a settler in.", |_| { - StateUpdate::None - }) -} - #[derive(Clone)] pub struct RemoveCasualtiesSelection { pub position: Position, @@ -51,10 +43,6 @@ impl RemoveCasualtiesSelection { } impl UnitSelection for RemoveCasualtiesSelection { - fn selected_units(&self) -> &[u32] { - &self.units - } - fn selected_units_mut(&mut self) -> &mut Vec { &mut self.units } @@ -62,10 +50,6 @@ impl UnitSelection for RemoveCasualtiesSelection { fn can_select(&self, _game: &Game, unit: &Unit) -> bool { self.selectable.contains(&unit.id) } - - fn current_tile(&self) -> Option { - Some(self.position) - } } impl ConfirmSelection for RemoveCasualtiesSelection { @@ -85,30 +69,25 @@ impl ConfirmSelection for RemoveCasualtiesSelection { pub fn remove_casualties_dialog( game: &Game, sel: &RemoveCasualtiesSelection, - player: &ShownPlayer, + state: &State, ) -> StateUpdate { unit_ui::unit_selection_dialog::( game, - player, - "Remove casualties", sel, - |new| StateUpdate::SetDialog(ActiveDialog::RemoveCasualties(new.clone())), |new: RemoveCasualtiesSelection| { StateUpdate::Execute(Action::Combat(CombatAction::RemoveCasualties( new.units.clone(), ))) }, - |_| StateUpdate::None, + state, ) } -pub fn play_action_card_dialog(player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Play action card", |ui| { - if ui.button(None, "None") { - return StateUpdate::Execute(Action::Combat(CombatAction::PlayActionCard( - PlayActionCard::None, - ))); - } - StateUpdate::None - }) +pub fn play_action_card_dialog(state: &State) -> StateUpdate { + if cancel_button_with_tooltip(state, "Play no action card") { + return StateUpdate::Execute(Action::Combat(CombatAction::PlayActionCard( + PlayActionCard::None, + ))); + } + StateUpdate::None } diff --git a/client/src/construct_ui.rs b/client/src/construct_ui.rs index 93511fc..9d5d7f6 100644 --- a/client/src/construct_ui.rs +++ b/client/src/construct_ui.rs @@ -13,7 +13,7 @@ use server::position::Position; use server::resource_pile::PaymentOptions; use server::unit::UnitType; -use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; +use crate::client_state::{ActiveDialog, State, StateUpdate}; use crate::payment_ui::{payment_dialog, HasPayment, Payment, ResourcePayment}; use crate::recruit_unit_ui::RecruitSelection; use crate::resource_ui::{new_resource_map, ResourceType}; @@ -39,11 +39,9 @@ pub fn building_positions(building: Building, city: &City, map: &Map) -> Vec StateUpdate { payment_dialog( - player, payment, |cp| cp.payment.get(ResourceType::Discount).selectable.current == 0, |cp| match &cp.project { diff --git a/client/src/dialog_ui.rs b/client/src/dialog_ui.rs index 11cf778..1af38a1 100644 --- a/client/src/dialog_ui.rs +++ b/client/src/dialog_ui.rs @@ -1,103 +1,46 @@ -use crate::client_state::{PendingUpdate, ShownPlayer, State, StateUpdate}; -use crate::layout_ui::{bottom_right_texture, cancel_pos, icon_pos, ok_only_pos, ok_pos}; -use macroquad::hash; -use macroquad::math::{vec2, Vec2}; -use macroquad::ui::widgets::Window; -use macroquad::ui::{root_ui, Ui}; +use crate::client_state::{PendingUpdate, State, StateUpdate}; +use crate::layout_ui::{bottom_center_text, bottom_right_texture, icon_pos}; +use macroquad::math::vec2; -pub fn active_dialog_window(player: &ShownPlayer, title: &str, f: F) -> StateUpdate -where - F: FnOnce(&mut Ui) -> StateUpdate, -{ - dialog(player, title, |ui| { - if player.can_control { - f(ui) - } else { - StateUpdate::None - } - }) -} - -pub fn dialog(player: &ShownPlayer, title: &str, f: F) -> StateUpdate -where - F: FnOnce(&mut Ui) -> StateUpdate, -{ - let size = player.screen_size; - let width = size.x - 20.; - let size = if player.active_dialog.is_map_dialog() { - vec2(width / 2.0, 270.) +pub fn show_pending_update(update: &PendingUpdate, state: &State) -> StateUpdate { + let t = if update.warning.is_empty() { + "Are you sure?" } else { - vec2(width, size.y - 40.) + &format!("Warning: {}", update.warning.join(", ")) }; - custom_dialog(title, vec2(10., 10.), size, f) -} + let dimensions = state.measure_text(t); + bottom_center_text(state, t, vec2(-dimensions.width / 2., -50.)); -fn custom_dialog(title: &str, position: Vec2, size: Vec2, f: F) -> StateUpdate -where - F: FnOnce(&mut Ui) -> StateUpdate, -{ - let window = Window::new(hash!(), position, size) - .titlebar(true) - .movable(false) - .label(title) - .close_button(true); - - let (update, open) = show_window(window, f); - if matches!(update, StateUpdate::None) { - if open { - StateUpdate::None - } else { - StateUpdate::CloseDialog - } - } else { - update + if ok_button(state, update.can_confirm) { + return StateUpdate::ResolvePendingUpdate(true); } + if cancel_button(state) { + return StateUpdate::ResolvePendingUpdate(false); + } + StateUpdate::None } -fn show_window(window: Window, f: F) -> (R, bool) -where - F: FnOnce(&mut Ui) -> R, -{ - let ui = &mut root_ui(); - let token = window.begin(ui); - let update = f(ui); - let open = token.end(ui); - (update, open) +#[must_use] +pub fn cancel_button(state: &State) -> bool { + cancel_button_with_tooltip(state, "Cancel") } -pub fn show_pending_update(update: &PendingUpdate, player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Are you sure?", |ui| { - for i in &update.info { - ui.label(None, i); - } - if !update.warning.is_empty() { - ui.label(None, &format!("Warning: {}", update.warning.join(", "))); - } - if update.can_confirm && ui.button(ok_pos(player), "OK") { - return StateUpdate::ResolvePendingUpdate(true); - } - let p = if update.can_confirm { - cancel_pos(player) - } else { - ok_only_pos(player) - }; - if ui.button(p, "Cancel") { - return StateUpdate::ResolvePendingUpdate(false); - } - StateUpdate::None - }) +#[must_use] +pub fn cancel_button_with_tooltip(state: &State, tooltip: &str) -> bool { + bottom_right_texture(state, &state.assets.cancel, icon_pos(-7, -1), tooltip) } -pub fn cancel_button(state: &State) -> bool { - bottom_right_texture(state, &state.assets.cancel, icon_pos(-7, -1), "Cancel") +#[must_use] +pub fn ok_button(state: &State, valid: bool) -> bool { + ok_button_with_tooltip(state, valid, if valid { "OK" } else { "Invalid selection" }) } -pub fn ok_button(state: &State, valid: bool) -> bool { +#[must_use] +pub fn ok_button_with_tooltip(state: &State, valid: bool, tooltip: &str) -> bool { let ok = if valid { &state.assets.ok } else { &state.assets.ok_blocked }; - let ok_tooltip = if valid { "OK" } else { "Invalid selection" }; - bottom_right_texture(state, ok, icon_pos(-8, -1), ok_tooltip) && valid + bottom_right_texture(state, ok, icon_pos(-8, -1), tooltip) && valid } diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index 529ea14..ac6c2be 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -1,14 +1,12 @@ use server::action::Action; use server::city::City; use server::game::Game; -use server::player::Player; use server::playing_actions::PlayingAction; use server::position::Position; use server::resource_pile::ResourcePile; -use crate::client_state::{ActiveDialog, ShownPlayer, StateUpdate}; -use crate::dialog_ui::active_dialog_window; -use crate::layout_ui::{cancel_pos, ok_pos}; +use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; +use crate::dialog_ui::{cancel_button, ok_button}; #[derive(Clone)] pub struct IncreaseHappiness { @@ -22,7 +20,7 @@ impl IncreaseHappiness { } } -pub fn increase_happiness_dialog( +pub fn increase_happiness_click( game: &Game, player: &ShownPlayer, pos: Position, @@ -30,10 +28,7 @@ pub fn increase_happiness_dialog( ) -> StateUpdate { if let Some(city) = player.get(game).get_city(pos) { StateUpdate::SetDialog(ActiveDialog::IncreaseHappiness(add_increase_happiness( - player.get(game), - city, - pos, - h, + city, pos, h, ))) } else { StateUpdate::None @@ -41,7 +36,6 @@ pub fn increase_happiness_dialog( } pub fn add_increase_happiness( - player: &Player, city: &City, pos: Position, increase_happiness: &IncreaseHappiness, @@ -53,7 +47,7 @@ pub fn add_increase_happiness( .map(|(p, steps)| { let old_steps = *steps; if *p == pos { - if let Some(r) = increase_happiness_steps(player, city, &total_cost, old_steps) { + if let Some(r) = increase_happiness_steps(city, &total_cost, old_steps) { total_cost = r.1; return (*p, r.0); }; @@ -66,24 +60,20 @@ pub fn add_increase_happiness( } fn increase_happiness_steps( - player: &Player, city: &City, total_cost: &ResourcePile, old_steps: u32, ) -> Option<(u32, ResourcePile)> { - if let Some(value) = - increase_happiness_new_steps(player, city, total_cost, old_steps, old_steps + 1) - { + if let Some(value) = increase_happiness_new_steps(city, total_cost, old_steps, old_steps + 1) { return Some(value); } - if let Some(value) = increase_happiness_new_steps(player, city, total_cost, old_steps, 0) { + if let Some(value) = increase_happiness_new_steps(city, total_cost, old_steps, 0) { return Some(value); } None } fn increase_happiness_new_steps( - player: &Player, city: &City, total_cost: &ResourcePile, old_steps: u32, @@ -97,31 +87,32 @@ fn increase_happiness_new_steps( .expect("invalid steps"); } new_total += new_cost; - if player.resources.can_afford(&new_total) { - return Some((new_steps, new_total)); - } + return Some((new_steps, new_total)); } None } -pub fn increase_happiness_menu(h: &IncreaseHappiness, player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Increase Happiness", |ui| { - ui.label(None, &format!("Cost: {:?}", h.cost)); - if ui.button(cancel_pos(player), "Cancel") { - return StateUpdate::Cancel; - } - if ui.button(ok_pos(player), "Confirm") { - return StateUpdate::Execute(Action::Playing(PlayingAction::IncreaseHappiness { - happiness_increases: h.steps.clone(), - })); - } - StateUpdate::None - }) +pub fn increase_happiness_menu( + h: &IncreaseHappiness, + player: &ShownPlayer, + state: &State, + game: &Game, +) -> StateUpdate { + if ok_button(state, player.get(game).resources.can_afford(&h.cost)) { + return StateUpdate::Execute(Action::Playing(PlayingAction::IncreaseHappiness { + happiness_increases: h.steps.clone(), + })); + } + if cancel_button(state) { + return StateUpdate::Cancel; + } + StateUpdate::None } pub fn start_increase_happiness(game: &Game, player: &ShownPlayer) -> StateUpdate { StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( - game.get_player(player.index) + player + .get(game) .cities .iter() .map(|c| (c.position, 0)) diff --git a/client/src/influence_ui.rs b/client/src/influence_ui.rs index 28bf84e..43859dd 100644 --- a/client/src/influence_ui.rs +++ b/client/src/influence_ui.rs @@ -1,13 +1,13 @@ use crate::city_ui::{building_name, building_position, BUILDING_SIZE}; use crate::client_state::{ShownPlayer, State, StateUpdate}; -use crate::dialog_ui::active_dialog_window; +use crate::dialog_ui::{cancel_button_with_tooltip, ok_button}; use crate::hex_ui; use crate::layout_ui::is_in_circle; use crate::tooltip::show_tooltip_for_circle; use macroquad::input::{is_mouse_button_pressed, MouseButton}; use macroquad::math::Vec2; use server::action::Action; -use server::game::{CulturalInfluenceResolution, Game}; +use server::game::Game; use server::player::Player; use server::playing_actions::{InfluenceCultureAttempt, PlayingAction}; use server::position::Position; @@ -21,26 +21,15 @@ fn closest_city(player: &Player, position: Position) -> Position { .position } -pub fn cultural_influence_resolution_dialog( - c: &CulturalInfluenceResolution, - player: &ShownPlayer, -) -> StateUpdate { - active_dialog_window(player, "Cultural Influence Resolution", |ui| { - if ui.button( - None, - format!( - "Pay {} culture tokens to influence {}", - c.roll_boost_cost, - building_name(&c.city_piece) - ), - ) { - StateUpdate::Execute(Action::CulturalInfluenceResolution(true)) - } else if ui.button(None, "Decline") { - StateUpdate::Execute(Action::CulturalInfluenceResolution(false)) - } else { - StateUpdate::None - } - }) +pub fn cultural_influence_resolution_dialog(state: &State) -> StateUpdate { + if ok_button(state, true) { + return StateUpdate::Execute(Action::CulturalInfluenceResolution(true)); + } + if cancel_button_with_tooltip(state, "Decline") { + return StateUpdate::Execute(Action::CulturalInfluenceResolution(false)); + } + + StateUpdate::None } pub fn hover( @@ -50,7 +39,7 @@ pub fn hover( mouse_pos: Vec2, state: &mut State, ) -> StateUpdate { - let player = game.get_player(shown_player.index); + let player = shown_player.get(game); if let Some(city) = game.get_any_city(position) { let c = hex_ui::center(city.position); let mut i = city.pieces.wonders.len() as i32; @@ -73,7 +62,7 @@ pub fn hover( city.position, *b, ) { - let player = game.get_player(shown_player.index); + let player = shown_player.get(game); if player.resources.can_afford(&cost) { if is_mouse_button_pressed(MouseButton::Left) { return StateUpdate::Execute(Action::Playing( diff --git a/client/src/layout_ui.rs b/client/src/layout_ui.rs index 7c27d04..88c3c22 100644 --- a/client/src/layout_ui.rs +++ b/client/src/layout_ui.rs @@ -1,4 +1,4 @@ -use crate::client_state::{ShownPlayer, State}; +use crate::client_state::State; use crate::hex_ui::Point; use crate::tooltip; use macroquad::color::WHITE; @@ -20,10 +20,19 @@ pub fn icon_pos(x: i8, y: i8) -> Vec2 { } pub fn top_center_texture(state: &State, texture: &Texture2D, p: Vec2, tooltip: &str) -> bool { - let anchor = vec2(state.screen_size.x / 2., MARGIN); + let anchor = top_center_anchor(state); draw_icon(state, texture, tooltip, anchor + p) } +pub fn top_center_text(state: &State, text: &str, p: Vec2) { + let p = top_center_anchor(state) + p; + state.draw_text(text, p.x, p.y); +} + +fn top_center_anchor(state: &State) -> Vec2 { + vec2(state.screen_size.x / 2., MARGIN) +} + pub fn top_right_texture(state: &State, texture: &Texture2D, p: Vec2, tooltip: &str) -> bool { let anchor = vec2(state.screen_size.x - MARGIN, MARGIN); draw_icon(state, texture, tooltip, anchor + p) @@ -39,6 +48,11 @@ pub fn bottom_center_texture(state: &State, texture: &Texture2D, p: Vec2, toolti draw_icon(state, texture, tooltip, anchor + p) } +pub fn bottom_center_text(state: &State, text: &str, p: Vec2) { + let p = bottom_center_anchor(state) + p; + state.draw_text(text, p.x, p.y); +} + pub fn bottom_center_anchor(state: &State) -> Vec2 { vec2(state.screen_size.x / 2., state.screen_size.y - MARGIN) } @@ -72,48 +86,28 @@ pub fn draw_scaled_icon( let rect = Rect::new(origin.x, origin.y, size, size); if !tooltip.is_empty() { - tooltip::show_tooltip_for_rect(state, tooltip, rect); - } - left_mouse_button(rect) -} - -pub fn left_mouse_button(rect: Rect) -> bool { - if is_mouse_button_pressed(MouseButton::Left) { - let (x, y) = mouse_position(); - rect.contains(vec2(x, y)) - } else { - false + tooltip::show_tooltip_for_rect(state, &[tooltip.to_string()], rect); } + left_mouse_button_pressed_in_rect(rect, state) } -pub fn cancel_pos(player: &ShownPlayer) -> Vec2 { - small_dialog(player) - .then(|| Vec2::new(player.screen_size.x / 4.0, 190.)) - .unwrap_or_else(|| Vec2::new(player.screen_size.x / 2., player.screen_size.y - 130.)) -} - -pub fn ok_pos(player: &ShownPlayer) -> Vec2 { - small_dialog(player) - .then(|| Vec2::new(player.screen_size.x / 4.0 - 150., 190.)) - .unwrap_or_else(|| { - Vec2::new( - player.screen_size.x / 2. - 150., - player.screen_size.y - 130., - ) - }) -} - -pub fn ok_only_pos(player: &ShownPlayer) -> Vec2 { - small_dialog(player) - .then(|| Vec2::new(player.screen_size.x / 4.0 - 75., 190.)) - .unwrap_or_else(|| Vec2::new(player.screen_size.x / 2. - 75., player.screen_size.y - 130.)) -} - -fn small_dialog(player: &ShownPlayer) -> bool { - player.active_dialog.is_map_dialog() || player.pending_update +#[must_use] +pub fn left_mouse_button_pressed_in_rect(rect: Rect, state: &State) -> bool { + left_mouse_button_pressed(state).is_some_and(|p| rect.contains(p)) } +#[must_use] pub fn is_in_circle(mouse_pos: Vec2, p: Point, radius: f32) -> bool { let d = vec2(p.x - mouse_pos.x, p.y - mouse_pos.y); d.length() <= radius } + +#[must_use] +pub fn left_mouse_button_pressed(state: &State) -> Option { + if is_mouse_button_pressed(MouseButton::Left) { + let (x, y) = mouse_position(); + Some(state.screen_to_world(vec2(x, y))) + } else { + None + } +} diff --git a/client/src/log_ui.rs b/client/src/log_ui.rs index e1cb476..508394f 100644 --- a/client/src/log_ui.rs +++ b/client/src/log_ui.rs @@ -1,31 +1,31 @@ -use macroquad::ui::Ui; +use macroquad::math::vec2; use server::game::Game; -use crate::client_state::{ShownPlayer, StateUpdate}; -use crate::dialog_ui::dialog; +use crate::client_state::{State, StateUpdate}; -pub fn show_log(game: &Game, player: &ShownPlayer) -> StateUpdate { - dialog(player, "Log", |ui| { - game.log.iter().for_each(|l| { - multiline(ui, l); - }); - StateUpdate::None - }) -} +pub fn show_log(game: &Game, state: &mut State) -> StateUpdate { + let mut y = 0.; + let mut label = |label: &str| { + let p = vec2(300., y * 25. + 20.); + y += 1.; + state.draw_text(label, p.x, p.y); + }; -fn multiline(ui: &mut Ui, text: &str) { - let mut line = String::new(); - text.split(' ').for_each(|s| { - if line.len() + s.len() > 100 { - ui.label(None, &line); - line = String::new(); - } + game.log.iter().for_each(|l| { + let mut line = String::new(); + l.split(' ').for_each(|s| { + if line.len() + s.len() > 100 { + label(&line); + line = String::new(); + } + if !line.is_empty() { + line.push(' '); + } + line.push_str(s); + }); if !line.is_empty() { - line.push(' '); + label(&line); } - line.push_str(s); }); - if !line.is_empty() { - ui.label(None, &line); - } + StateUpdate::None } diff --git a/client/src/map_ui.rs b/client/src/map_ui.rs index a5926e6..c6cae9b 100644 --- a/client/src/map_ui.rs +++ b/client/src/map_ui.rs @@ -85,7 +85,6 @@ fn alpha(game: &Game, state: &State, pos: Position) -> f32 { 0. } } - ActiveDialog::ReplaceUnits(s) => highlight_if(s.current_city.is_some_and(|p| p == pos)), ActiveDialog::RazeSize1City => { highlight_if(game.players[game.active_player()].can_raze_city(pos)) } diff --git a/client/src/move_ui.rs b/client/src/move_ui.rs index e624bdc..91f6826 100644 --- a/client/src/move_ui.rs +++ b/client/src/move_ui.rs @@ -8,7 +8,7 @@ use server::position::Position; use server::unit::MovementAction; use crate::client_state::{ActiveDialog, StateUpdate}; -use crate::unit_ui::unit_at_pos; +use crate::unit_ui::{unit_at_pos, unit_selection_clicked}; fn possible_destinations( game: &Game, @@ -62,12 +62,7 @@ pub fn click(pos: Position, s: &MoveSelection, mouse_pos: Vec2, game: &Game) -> let unit = unit_at_pos(pos, mouse_pos, p); unit.map_or(StateUpdate::None, |unit_id| { new.start = Some(pos); - if new.units.contains(&unit_id) { - // deselect unit - new.units.retain(|&id| id != unit_id); - } else { - new.units.push(unit_id); - } + unit_selection_clicked(unit_id, &mut new.units); if new.units.is_empty() { new.destinations.clear(); new.start = None; diff --git a/client/src/payment_ui.rs b/client/src/payment_ui.rs index beeb95d..f82d74a 100644 --- a/client/src/payment_ui.rs +++ b/client/src/payment_ui.rs @@ -2,7 +2,7 @@ use macroquad::math::{bool, vec2}; use server::resource_pile::ResourcePile; -use crate::client_state::{ShownPlayer, State, StateUpdate}; +use crate::client_state::{State, StateUpdate}; use crate::layout_ui::draw_icon; use crate::resource_ui::{resource_name, ResourceType}; use crate::select_ui; @@ -79,7 +79,6 @@ pub trait HasPayment { #[allow(clippy::too_many_arguments)] pub fn payment_dialog( - player: &ShownPlayer, has_payment: &T, is_valid: impl FnOnce(&T) -> bool, execute_action: impl FnOnce(&T) -> StateUpdate, @@ -89,7 +88,6 @@ pub fn payment_dialog( state: &State, ) -> StateUpdate { select_ui::count_dialog( - player, state, has_payment, |p| p.payment().resources.clone(), diff --git a/client/src/player_ui.rs b/client/src/player_ui.rs index 9db5fb3..c9616a5 100644 --- a/client/src/player_ui.rs +++ b/client/src/player_ui.rs @@ -4,8 +4,8 @@ use crate::client::Features; use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate, OFFSET, ZOOM}; use crate::happiness_ui::start_increase_happiness; use crate::layout_ui::{ - bottom_left_texture, bottom_right_texture, icon_pos, left_mouse_button, top_center_texture, - top_right_texture, ICON_SIZE, + bottom_left_texture, bottom_right_texture, icon_pos, left_mouse_button_pressed_in_rect, + top_center_texture, ICON_SIZE, }; use crate::map_ui::terrain_name; use crate::resource_ui::{new_resource_map, resource_name, resource_types, ResourceType}; @@ -16,6 +16,7 @@ use server::action::Action; use server::consts::ARMY_MOVEMENT_REQUIRED_ADVANCE; use server::game::{Game, GameState}; use server::playing_actions::PlayingAction; +use server::status_phase::StatusPhaseAction; use server::unit::MovementAction; pub fn player_select(game: &Game, player: &ShownPlayer, state: &State) -> StateUpdate { @@ -29,8 +30,8 @@ pub fn player_select(game: &Game, player: &ShownPlayer, state: &State) -> StateU let mut y = (players.len() as f32 * -ICON_SIZE) / 2.; - for p in players { - let pl = game.get_player(p); + for player_index in players { + let pl = game.get_player(player_index); let shown = player.index == pl.index; let pos = vec2(player.screen_size.x, player.screen_size.y / 2.0) + vec2(-20., y); @@ -58,7 +59,14 @@ pub fn player_select(game: &Game, player: &ShownPlayer, state: &State) -> StateU ); } - if !shown && left_mouse_button(Rect::new(x, pos.y, w, ICON_SIZE)) { + if !shown && left_mouse_button_pressed_in_rect(Rect::new(x, pos.y, w, ICON_SIZE), state) { + if player.can_control { + if let ActiveDialog::DetermineFirstPlayer = state.active_dialog { + return StateUpdate::status_phase(StatusPhaseAction::DetermineFirstPlayer( + player_index, + )); + } + }; return StateUpdate::SetShownPlayer(pl.index); } @@ -104,7 +112,7 @@ pub fn top_icon_with_label( } pub fn show_top_center(game: &Game, shown_player: &ShownPlayer, state: &State) { - let player = game.get_player(shown_player.index); + let player = shown_player.get(game); top_icon_with_label( shown_player, @@ -124,6 +132,9 @@ pub fn show_top_center(game: &Game, shown_player: &ShownPlayer, state: &State) { ActiveDialog::CollectResources(c) => { format!("{}+{}", a, new_resource_map(&c.collected())[r]) } + ActiveDialog::IncreaseHappiness(h) => { + format!("{}-{}", a, new_resource_map(&h.cost)[r]) + } _ => format!("{a}/{l}"), }; resource_label(shown_player, state, &s, *r, icon_pos(x, 0)); @@ -147,7 +158,7 @@ pub fn show_top_left(game: &Game, player: &ShownPlayer, state: &State) { _ => label(&format!("Round {}", game.round)), } - let p = game.get_player(player.index); + let p = player.get(game); label(&p.get_name()); @@ -162,40 +173,40 @@ pub fn show_top_left(game: &Game, player: &ShownPlayer, state: &State) { } )); - if game.current_player_index == p.index { + if game.current_player_index == player.index { match &game.state { GameState::StatusPhase(_) | GameState::Finished => {} _ => label(&format!("{} actions left", game.actions_left)), } + if let Some(moves) = moves_left(&game.state) { + label(&move_units_message(moves)); + } + } + + if let GameState::Combat(c) = &game.state { + if c.attacker == player.index { + label(&format!("Attack - combat round {}", c.round)); + } else if c.defender == player.index { + label(&format!("Defend - combat round {}", c.round)); + } + } + if game.active_player() == player.index { match &game.state { - GameState::Movement { - movement_actions_left, - .. - } => label(&format!("Move units: {movement_actions_left} moves left")), GameState::CulturalInfluenceResolution(_) => { label("Cultural Influence Resolution"); } - GameState::Combat(c) => label(&format!( - "Combat Round {} Phase {:?}{}", - c.round, - c.phase, - moves_left(&game.state) - .map(|m| format!(", {m} moves left")) - .unwrap_or_default() - )), - GameState::PlaceSettler { - player_index: _, - movement_actions_left, - .. - } => label(&format!( - "Place Settler: {movement_actions_left} moves left" - )), + GameState::PlaceSettler { .. } => label("Place Settler"), _ => {} } for m in state.active_dialog.help_message(game) { label(&m); } + if let Some(u) = &state.pending_update { + for m in &u.info { + label(m); + } + } } if let ActiveDialog::TileMenu(position) = state.active_dialog { @@ -223,6 +234,10 @@ pub fn show_top_left(game: &Game, player: &ShownPlayer, state: &State) { } } +fn move_units_message(movement_actions_left: u32) -> String { + format!("Move units: {movement_actions_left} moves left") +} + fn moves_left(state: &GameState) -> Option { match state { GameState::Combat(c) => moves_left(&c.initiation), @@ -244,46 +259,27 @@ pub fn show_global_controls(game: &Game, state: &mut State, features: &Features) let assets = &state.assets; - if let Some(tooltip) = can_end_move(game) { - if player.can_control - && bottom_right_texture(state, &assets.end_turn, icon_pos(-4, -1), tooltip) - { - return end_move(game); + if player.can_control { + if let Some(tooltip) = can_end_move(game) { + if bottom_right_texture(state, &assets.end_turn, icon_pos(-4, -1), tooltip) { + return end_move(game); + } + } + if game.can_redo() && bottom_right_texture(state, &assets.redo, icon_pos(-5, -1), "Redo") { + return StateUpdate::Execute(Action::Redo); + } + if game.can_undo() && bottom_right_texture(state, &assets.undo, icon_pos(-6, -1), "Undo") { + return StateUpdate::Execute(Action::Undo); } - } - if game.can_redo() && bottom_right_texture(state, &assets.redo, icon_pos(-5, -1), "Redo") { - return StateUpdate::Execute(Action::Redo); - } - if game.can_undo() && bottom_right_texture(state, &assets.undo, icon_pos(-6, -1), "Undo") { - return StateUpdate::Execute(Action::Undo); - } - let d = state.game_state_dialog(game, &ActiveDialog::None); - if d.can_restore() - && d.title() != state.active_dialog.title() - && bottom_right_texture( - state, - &assets.restore_menu, - icon_pos(-7, -1), - format!("Restore {}", d.title()).as_str(), - ) - { - return StateUpdate::OpenDialog(d); - } - if player.can_play_action { - let update = action_buttons(game, state, player, assets); - if !matches!(update, StateUpdate::None) { - return update; + if player.can_play_action { + let update = action_buttons(game, state, player, assets); + if !matches!(update, StateUpdate::None) { + return update; + } } } - if top_right_texture(state, &assets.log, icon_pos(-1, 0), "Show log") { - return StateUpdate::OpenDialog(ActiveDialog::Log); - }; - if top_right_texture(state, &assets.advances, icon_pos(-2, 0), "Show advances") { - return StateUpdate::OpenDialog(ActiveDialog::AdvanceMenu); - }; - if features.import_export { if bottom_right_texture(state, &assets.export, icon_pos(-1, -3), "Export") { return StateUpdate::Export; diff --git a/client/src/recruit_unit_ui.rs b/client/src/recruit_unit_ui.rs index 08a5227..4e2e0dd 100644 --- a/client/src/recruit_unit_ui.rs +++ b/client/src/recruit_unit_ui.rs @@ -156,7 +156,6 @@ pub struct RecruitSelection { pub available_units: Units, pub need_replacement: Units, pub replaced_units: Vec, - pub current_city: Option, } impl RecruitSelection { @@ -169,24 +168,15 @@ impl RecruitSelection { available_units, need_replacement, replaced_units, - current_city: None, } } - pub fn clear(&mut self) { - self.current_city = None; - } - pub fn is_finished(&self) -> bool { self.need_replacement.is_empty() } } impl UnitSelection for RecruitSelection { - fn selected_units(&self) -> &[u32] { - &self.replaced_units - } - fn selected_units_mut(&mut self) -> &mut Vec { &mut self.replaced_units } @@ -194,10 +184,6 @@ impl UnitSelection for RecruitSelection { fn can_select(&self, _game: &Game, unit: &Unit) -> bool { self.need_replacement.has_unit(&unit.unit_type) } - - fn current_tile(&self) -> Option { - self.current_city - } } impl ConfirmSelection for RecruitSelection { @@ -222,7 +208,6 @@ pub fn select_dialog( state: &State, ) -> StateUpdate { select_ui::count_dialog( - player, state, a, |s| s.selectable.clone(), @@ -295,13 +280,10 @@ fn update_selection( ) } -pub fn replace_dialog(game: &Game, sel: &RecruitSelection, player: &ShownPlayer) -> StateUpdate { +pub fn replace_dialog(game: &Game, sel: &RecruitSelection, state: &State) -> StateUpdate { unit_ui::unit_selection_dialog::( game, - player, - "Replace units", sel, - |new| StateUpdate::SetDialog(ActiveDialog::ReplaceUnits(new.clone())), |new: RecruitSelection| { StateUpdate::SetDialog(ActiveDialog::ConstructionPayment(ConstructionPayment::new( game, @@ -311,12 +293,6 @@ pub fn replace_dialog(game: &Game, sel: &RecruitSelection, player: &ShownPlayer) ConstructionProject::Units(new), ))) }, - |_| StateUpdate::None, + state, ) } - -pub fn click_replace(pos: Position, s: &RecruitSelection) -> StateUpdate { - let mut new = s.clone(); - new.current_city = Some(pos); - StateUpdate::SetDialog(ActiveDialog::ReplaceUnits(new)) -} diff --git a/client/src/select_ui.rs b/client/src/select_ui.rs index 87ca428..2449af4 100644 --- a/client/src/select_ui.rs +++ b/client/src/select_ui.rs @@ -1,11 +1,10 @@ -use crate::client_state::{ShownPlayer, State, StateUpdate, StateUpdates}; -use crate::dialog_ui::{active_dialog_window, cancel_button, ok_button}; -use crate::layout_ui::{bottom_center_anchor, bottom_center_texture, ok_pos, ICON_SIZE}; +use crate::client_state::{State, StateUpdate, StateUpdates}; +use crate::dialog_ui::{cancel_button, cancel_button_with_tooltip, ok_button}; +use crate::layout_ui::{bottom_center_anchor, bottom_center_texture, ICON_SIZE}; use macroquad::color::BLACK; use macroquad::math::{bool, vec2, Vec2}; use macroquad::prelude::TextParams; use macroquad::text::draw_text_ex; -use macroquad::ui::Ui; use server::game::Game; #[derive(PartialEq, Eq, Debug, Clone)] @@ -22,7 +21,6 @@ pub trait HasCountSelectableObject { #[allow(clippy::too_many_arguments)] pub fn count_dialog( - player: &ShownPlayer, state: &State, container: &C, get_objects: impl Fn(&C) -> Vec, @@ -33,10 +31,6 @@ pub fn count_dialog( plus: impl Fn(&C, &O) -> StateUpdate, minus: impl Fn(&C, &O) -> StateUpdate, ) -> StateUpdate { - if !player.can_control { - return StateUpdate::None; - } - let mut updates = StateUpdates::new(); let objects = get_objects(container) .into_iter() @@ -105,72 +99,31 @@ pub trait ConfirmSelection: Clone { fn confirm(&self, game: &Game) -> SelectionConfirm; } -pub trait Selection: ConfirmSelection { - fn all(&self) -> &[String]; - fn selected(&self) -> &[String]; - fn selected_mut(&mut self) -> &mut Vec; - fn can_select(&self, game: &Game, name: &str) -> bool; -} - -pub fn selection_dialog( - game: &Game, - player: &ShownPlayer, - title: &str, - sel: &T, - on_change: impl Fn(T) -> StateUpdate, - on_ok: impl FnOnce(T) -> StateUpdate, -) -> StateUpdate { - active_dialog_window(player, title, |ui| { - for name in sel.all() { - let can_sel = sel.can_select(game, name); - let is_selected = sel.selected().contains(name); - let mut l = name.to_string(); - if is_selected { - l += " (selected)"; - } - - if !can_sel { - ui.label(None, &l); - } else if ui.button(None, l) { - let mut new = sel.clone(); - if is_selected { - new.selected_mut().retain(|n| n != name); - } else { - new.selected_mut().push(name.to_string()); - } - return on_change(new); - } - } - confirm_update(sel, player, || on_ok(sel.clone()), ui, &sel.confirm(game)) - }) -} - pub fn confirm_update( sel: &T, - player: &ShownPlayer, on_ok: impl FnOnce() -> StateUpdate, - ui: &mut Ui, confirm: &SelectionConfirm, + state: &State, ) -> StateUpdate { match confirm { SelectionConfirm::NoConfirm => StateUpdate::None, SelectionConfirm::Invalid => { - ui.label(ok_pos(player), "Invalid selection"); - may_cancel(sel, ui) + let _ = ok_button(state, false); + may_cancel(sel, state) } SelectionConfirm::Valid => { - if ui.button(ok_pos(player), "OK") { + if ok_button(state, true) { on_ok() } else { - may_cancel(sel, ui) + may_cancel(sel, state) } } } } -fn may_cancel(sel: &impl ConfirmSelection, ui: &mut Ui) -> StateUpdate { +fn may_cancel(sel: &impl ConfirmSelection, state: &State) -> StateUpdate { if let Some(cancel_name) = sel.cancel_name() { - if ui.button(None, cancel_name) { + if cancel_button_with_tooltip(state, cancel_name) { sel.cancel() } else { StateUpdate::None diff --git a/client/src/status_phase_ui.rs b/client/src/status_phase_ui.rs index 5e338c1..10965f1 100644 --- a/client/src/status_phase_ui.rs +++ b/client/src/status_phase_ui.rs @@ -1,35 +1,13 @@ -use crate::client_state::{ActiveDialog, ShownPlayer, StateUpdate}; -use crate::dialog_ui::active_dialog_window; -use crate::select_ui; -use crate::select_ui::{ConfirmSelection, Selection, SelectionConfirm}; +use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; +use crate::dialog_ui::{cancel_button, cancel_button_with_tooltip}; +use crate::select_ui::{confirm_update, ConfirmSelection, SelectionConfirm}; use server::action::Action; -use server::content::advances; use server::game::Game; use server::position::Position; use server::status_phase::{ ChangeGovernment, ChangeGovernmentType, RazeSize1City, StatusPhaseAction, }; -pub fn determine_first_player_dialog(game: &Game, player: &ShownPlayer) -> StateUpdate { - active_dialog_window( - player, - "Who should be the first player in the next age?", - |ui| { - for p in &game.players { - if ui.button( - None, - format!("Player {} - {}", p.index, p.civilization.name), - ) { - return StateUpdate::status_phase(StatusPhaseAction::DetermineFirstPlayer( - p.index, - )); - } - } - StateUpdate::None - }, - ) -} - pub fn raze_city_confirm_dialog(game: &Game, player: &ShownPlayer, pos: Position) -> StateUpdate { if player.get(game).can_raze_city(pos) { StateUpdate::execute_with_confirm( @@ -43,15 +21,11 @@ pub fn raze_city_confirm_dialog(game: &Game, player: &ShownPlayer, pos: Position } } -pub fn raze_city_dialog(player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Select a city to raze - or decline.", |ui| { - if ui.button(None, "Decline") { - return StateUpdate::status_phase(StatusPhaseAction::RazeSize1City( - RazeSize1City::None, - )); - } - StateUpdate::None - }) +pub fn raze_city_dialog(state: &State) -> StateUpdate { + if cancel_button(state) { + return StateUpdate::status_phase(StatusPhaseAction::RazeSize1City(RazeSize1City::None)); + } + StateUpdate::None } #[derive(Clone)] @@ -61,33 +35,15 @@ pub struct ChooseAdditionalAdvances { selected: Vec, } -impl ChooseAdditionalAdvances { - fn new(government: String, advances: Vec) -> Self { - Self { - government, - advances, - selected: Vec::new(), - } - } -} - -impl Selection for ChooseAdditionalAdvances { - fn all(&self) -> &[String] { - &self.advances - } - - fn selected(&self) -> &[String] { - &self.selected - } - - fn selected_mut(&mut self) -> &mut Vec { - &mut self.selected - } - - fn can_select(&self, _game: &Game, _name: &str) -> bool { - true - } -} +// impl ChooseAdditionalAdvances { +// fn new(government: String, advances: Vec) -> Self { +// Self { +// government, +// advances, +// selected: Vec::new(), +// } +// } +// } impl ConfirmSelection for ChooseAdditionalAdvances { fn cancel_name(&self) -> Option<&str> { @@ -107,49 +63,48 @@ impl ConfirmSelection for ChooseAdditionalAdvances { } } -pub fn change_government_type_dialog(game: &Game, player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Select additional advances", |ui| { - let current = player - .get(game) - .government() - .expect("should have government"); - for (g, _) in advances::get_governments() - .iter() - .filter(|(g, _)| g != ¤t) - { - if ui.button(None, format!("Change to {g}")) { - let additional = advances::get_government(g) - .iter() - .skip(1) // the government advance itself is always chosen - .map(|a| a.name.clone()) - .collect::>(); - return StateUpdate::SetDialog(ActiveDialog::ChooseAdditionalAdvances( - ChooseAdditionalAdvances::new(g.clone(), additional), - )); - } - } - - if ui.button(None, "Decline") { - return StateUpdate::status_phase(StatusPhaseAction::ChangeGovernmentType( - ChangeGovernmentType::KeepGovernment, - )); - } - StateUpdate::None - }) +pub fn change_government_type_dialog() -> StateUpdate { + //todo integrate in advance selection dialog + // active_dialog_window(player, "Select additional advances", |ui| { + // let current = player + // .get(game) + // .government() + // .expect("should have government"); + // for (g, _) in advances::get_governments() + // .iter() + // .filter(|(g, _)| g != ¤t) + // { + // if ui.button(None, format!("Change to {g}")) { + // let additional = advances::get_government(g) + // .iter() + // .skip(1) // the government advance itself is always chosen + // .map(|a| a.name.clone()) + // .collect::>(); + // return StateUpdate::SetDialog(ActiveDialog::ChooseAdditionalAdvances( + // ChooseAdditionalAdvances::new(g.clone(), additional), + // )); + // } + // } + // + // if ui.button(None, "Decline") { + // return StateUpdate::status_phase(StatusPhaseAction::ChangeGovernmentType( + // ChangeGovernmentType::KeepGovernment, + // )); + // } + // StateUpdate::None + // }) + StateUpdate::None } pub fn choose_additional_advances_dialog( game: &Game, - additional_advances: &ChooseAdditionalAdvances, - player: &ShownPlayer, + a: &ChooseAdditionalAdvances, + state: &State, ) -> StateUpdate { - select_ui::selection_dialog( - game, - player, - "Select additional advances:", - additional_advances, - |a| StateUpdate::SetDialog(ActiveDialog::ChooseAdditionalAdvances(a)), - |a| { + // todo actual selection should be done in advance selection dialog + confirm_update( + a, + || { StateUpdate::status_phase(StatusPhaseAction::ChangeGovernmentType( ChangeGovernmentType::ChangeGovernment(ChangeGovernment { new_government: a.government.clone(), @@ -157,14 +112,14 @@ pub fn choose_additional_advances_dialog( }), )) }, + &a.confirm(game), + state, ) } -pub fn complete_objectives_dialog(player: &ShownPlayer) -> StateUpdate { - active_dialog_window(player, "Complete Objectives", |ui| { - if ui.button(None, "None") { - return StateUpdate::status_phase(StatusPhaseAction::CompleteObjectives(vec![])); - } - StateUpdate::None - }) +pub fn complete_objectives_dialog(state: &State) -> StateUpdate { + if cancel_button_with_tooltip(state, "Complete no objectives") { + return StateUpdate::status_phase(StatusPhaseAction::CompleteObjectives(vec![])); + } + StateUpdate::None } diff --git a/client/src/tooltip.rs b/client/src/tooltip.rs index 8371d32..0ea8d1b 100644 --- a/client/src/tooltip.rs +++ b/client/src/tooltip.rs @@ -26,7 +26,7 @@ fn is_rect_tooltip_active(state: &State, rect: Rect) -> bool { .all(|mp| rect.contains(state.screen_to_world(mp.position))) } -pub fn show_tooltip_for_rect(state: &State, tooltip: &str, rect: Rect) { +pub fn show_tooltip_for_rect(state: &State, tooltip: &[String], rect: Rect) { let origin = rect.point(); let screen_origin = state.world_to_screen(rect.point()); if is_rect_tooltip_active(state, rect) { @@ -55,18 +55,28 @@ pub fn show_tooltip_for_circle(state: &State, tooltip: &str, center: Vec2, radiu if is_circle_tooltip_active(state, center, radius) { draw_circle(center.x, center.y, radius, Color::new(0.0, 0.0, 0.0, 0.5)); set_default_camera(); - show_tooltip_text(state, tooltip, screen_center + vec2(radius, radius)); + show_tooltip_text( + state, + &[tooltip.to_string()], + screen_center + vec2(radius, radius), + ); state.set_camera(); } } -fn show_tooltip_text(state: &State, tooltip: &str, origin: Vec2) { - let dimensions = state.measure_text(tooltip); - let tooltip_rect = Rect::new(origin.x, origin.y, dimensions.width, dimensions.height); +fn show_tooltip_text(state: &State, tooltip: &[String], origin: Vec2) { + let dim = tooltip.iter().map(|t| state.measure_text(t)); + let total = dim.fold(Vec2::new(0., 0.), |acc, d| { + vec2(acc.x.max(d.width), acc.y + 20.) + }); + + let tooltip_rect = Rect::new(origin.x, origin.y, total.x, total.y); let w = tooltip_rect.size().x + 10.; let sx = state.screen_size.x; let x = tooltip_rect.left().min(sx - w); let y = (tooltip_rect.top() - 10.).max(40.); draw_rectangle(x, y, w, tooltip_rect.size().y + 10., GRAY); - state.draw_text(tooltip, x + 5., y + 20.); + for (i, line) in tooltip.iter().enumerate() { + state.draw_text(line, x + 5., y + 20. + i as f32 * 20.); + } } diff --git a/client/src/unit_ui.rs b/client/src/unit_ui.rs index d4a0f64..a5b57cf 100644 --- a/client/src/unit_ui.rs +++ b/client/src/unit_ui.rs @@ -2,14 +2,12 @@ use macroquad::color::BLACK; use macroquad::math::{u32, vec2, Vec2}; use macroquad::prelude::WHITE; use macroquad::shapes::draw_circle; -use macroquad::ui::Ui; use server::game::Game; use server::position::Position; use server::unit::{Unit, UnitType}; use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; -use crate::dialog_ui::active_dialog_window; use crate::select_ui::{confirm_update, ConfirmSelection}; use crate::{hex_ui, player_ui}; @@ -125,53 +123,35 @@ pub fn draw_units(game: &Game, state: &State, tooltip: bool) { } pub trait UnitSelection: ConfirmSelection { - fn selected_units(&self) -> &[u32]; fn selected_units_mut(&mut self) -> &mut Vec; fn can_select(&self, game: &Game, unit: &Unit) -> bool; - fn current_tile(&self) -> Option; } -pub fn unit_selection_dialog( +pub fn unit_selection_click( game: &Game, player: &ShownPlayer, - title: &str, + pos: Position, + mouse_pos: Vec2, sel: &T, on_change: impl Fn(T) -> StateUpdate, - on_ok: impl FnOnce(T) -> StateUpdate, - additional: impl FnOnce(&mut Ui) -> StateUpdate, ) -> StateUpdate { - if let Some(current_tile) = sel.current_tile() { - active_dialog_window(player, title, |ui| { - for (i, (p, unit)) in units_on_tile(game, current_tile).enumerate() { - let can_sel = sel.can_select(game, &unit); - let is_selected = sel.selected_units().contains(&unit.id); - let army_move = game - .get_player(p) - .has_advance(ARMY_MOVEMENT_REQUIRED_ADVANCE); - let mut l = unit_label(&unit, army_move); - if is_selected { - l += " (selected)"; - } - - let pos = vec2(((i / 4) as f32) * 200., i.rem_euclid(4) as f32 * 35.); - if !can_sel { - ui.label(pos, &l); - } else if ui.button(pos, l) { - let mut new = sel.clone(); - if is_selected { - new.selected_units_mut().retain(|u| u != &unit.id); - } else { - new.selected_units_mut().push(unit.id); - } - return on_change(new); - } - } - confirm_update(sel, player, || on_ok(sel.clone()), ui, &sel.confirm(game)) - .or(|| additional(ui)) - }) - } else { - StateUpdate::None + if let Some(unit_id) = unit_at_pos(pos, mouse_pos, player.get(game)) { + if sel.can_select(game, player.get(game).get_unit(unit_id).unwrap()) { + let mut new = sel.clone(); + unit_selection_clicked(unit_id, new.selected_units_mut()); + return on_change(new); + } } + StateUpdate::None +} + +pub fn unit_selection_dialog( + game: &Game, + sel: &T, + on_ok: impl FnOnce(T) -> StateUpdate, + state: &State, +) -> StateUpdate { + confirm_update(sel, || on_ok(sel.clone()), &sel.confirm(game), state) } pub fn units_on_tile(game: &Game, pos: Position) -> impl Iterator + '_ { @@ -212,3 +192,12 @@ pub fn unit_label(unit: &Unit, army_move: bool) -> String { format!("{name}{res}") } + +pub fn unit_selection_clicked(unit_id: u32, units: &mut Vec) { + if units.contains(&unit_id) { + // deselect unit + units.retain(|&id| id != unit_id); + } else { + units.push(unit_id); + } +}