diff --git a/client/src/assets.rs b/client/src/assets.rs index ee47492c..e6520912 100644 --- a/client/src/assets.rs +++ b/client/src/assets.rs @@ -17,7 +17,7 @@ pub struct Assets { pub angry: Texture2D, // action icons - pub movement: Texture2D, + pub move_units: Texture2D, pub log: Texture2D, pub end_turn: Texture2D, pub advances: Texture2D, @@ -63,7 +63,7 @@ impl Assets { advances: load_png(include_bytes!("../assets/lab-svgrepo-com.png")), end_turn: load_png(include_bytes!("../assets/hour-glass-svgrepo-com.png")), log: load_png(include_bytes!("../assets/scroll-svgrepo-com.png")), - movement: load_png(include_bytes!("../assets/route-start-svgrepo-com.png")), + move_units: load_png(include_bytes!("../assets/route-start-svgrepo-com.png")), settle: load_png(include_bytes!("../assets/castle-manor-14-svgrepo-com.png")), // UI diff --git a/client/src/city_ui.rs b/client/src/city_ui.rs index 87dfc5d9..10948a14 100644 --- a/client/src/city_ui.rs +++ b/client/src/city_ui.rs @@ -1,21 +1,21 @@ +use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; +use crate::collect_ui::{possible_resource_collections, CollectResources}; +use crate::construct_ui::{new_building_positions, ConstructionPayment, ConstructionProject}; +use crate::happiness_ui::{add_increase_happiness, IncreaseHappiness}; +use crate::hex_ui::Point; +use crate::layout_ui::draw_scaled_icon; +use crate::map_ui::{move_units_button, show_map_action_buttons}; +use crate::recruit_unit_ui::RecruitAmount; +use crate::resource_ui::ResourceType; +use crate::{hex_ui, player_ui}; use macroquad::prelude::*; -use std::ops::Add; - use server::city::{City, MoodState}; use server::city_pieces::Building; use server::game::Game; use server::player::Player; use server::position::Position; use server::unit::{UnitType, Units}; - -use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; -use crate::collect_ui::{possible_resource_collections, CollectResources}; -use crate::construct_ui::{building_positions, ConstructionPayment, ConstructionProject}; -use crate::hex_ui::Point; -use crate::layout_ui::{bottom_center_texture, draw_scaled_icon, icon_pos}; -use crate::recruit_unit_ui::RecruitAmount; -use crate::resource_ui::ResourceType; -use crate::{hex_ui, player_ui}; +use std::ops::Add; pub struct CityMenu { pub player: ShownPlayer, @@ -45,79 +45,77 @@ impl CityMenu { } } -pub type IconActionVec<'a> = Vec<(&'a Texture2D, String, Box StateUpdate + 'a>)>; +pub type IconAction<'a> = (&'a Texture2D, String, Box StateUpdate + 'a>); + +pub type IconActionVec<'a> = Vec>; pub fn show_city_menu<'a>(game: &'a Game, menu: &'a CityMenu, state: &'a State) -> StateUpdate { let city = menu.get_city(game); + let pos = menu.city_position; let can_play = menu.player.can_play_action && menu.is_city_owner() && city.can_activate(); if !can_play { return StateUpdate::None; } - let mut icons: IconActionVec<'a> = vec![]; - icons.push(( - &state.assets.resources[&ResourceType::Food], - "Collect Resources".to_string(), - Box::new(|| { - StateUpdate::OpenDialog(ActiveDialog::CollectResources(CollectResources::new( - menu.player.index, - menu.city_position, - possible_resource_collections(game, menu.city_position, menu.city_owner_index), - ))) - }), - )); - icons.push(( - &state.assets.units[&UnitType::Infantry], - "Recruit Units".to_string(), - Box::new(|| { - RecruitAmount::new_selection( - game, - menu.player.index, - menu.city_position, - Units::empty(), - None, - &[], - ) + + let base_icons: IconActionVec<'a> = vec![ + increase_happiness_button(game, menu, state), + move_units_button(game, pos, &menu.player, state), + Some(collect_resources_button(game, menu, state)), + Some(recruit_button(game, menu, state)), + ] + .into_iter() + .flatten() + .collect(); + + let buildings: IconActionVec<'a> = building_icons(game, menu, state); + + let wonders: IconActionVec<'a> = wonder_icons(game, menu, state); + + show_map_action_buttons( + state, + &vec![base_icons, buildings, wonders] + .into_iter() + .flatten() + .collect(), + ) +} + +fn increase_happiness_button<'a>( + game: &'a Game, + menu: &'a CityMenu, + state: &'a State, +) -> Option> { + let city = menu.get_city(game); + if city.mood_state == MoodState::Happy { + return None; + } + Some(( + &state.assets.resources[&ResourceType::MoodTokens], + "Increase happiness".to_string(), + Box::new(move || { + let player = &menu.player; + let mut happiness = IncreaseHappiness::new(player.get(game)); + let mut target = city.mood_state.clone(); + while target != MoodState::Happy { + happiness = add_increase_happiness(city, &happiness); + target = target.clone().add(1); + } + StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(happiness)) }), - )); + )) +} +fn wonder_icons<'a>(game: &'a Game, menu: &'a CityMenu, state: &'a State) -> IconActionVec<'a> { let owner = menu.get_city_owner(game); let city = menu.get_city(game); - for (building, name) in building_names() { - if menu.is_city_owner() - && menu.player.can_play_action - && city.can_construct(building, owner) - { - for pos in building_positions(building, city, &game.map) { - let tooltip = format!( - "Built {}{} for {}", - name, - pos.map_or(String::new(), |p| format!(" at {p}")), - owner.construct_cost(building, city), - ); - icons.push(( - &state.assets.buildings[&building], - tooltip, - Box::new(move || { - StateUpdate::OpenDialog(ActiveDialog::ConstructionPayment( - ConstructionPayment::new( - game, - name, - menu.player.index, - menu.city_position, - ConstructionProject::Building(building, pos), - ), - )) - }), - )); - } - } - } - - for w in &owner.wonder_cards { - if city.can_build_wonder(w, owner, game) { - icons.push(( + owner + .wonder_cards + .iter() + .filter(|w| city.can_build_wonder(w, owner, game)) + .map(|w| { + let a: IconAction<'a> = ( &state.assets.wonders[&w.name], format!("Build wonder {}", w.name), Box::new(move || { @@ -131,21 +129,88 @@ pub fn show_city_menu<'a>(game: &'a Game, menu: &'a CityMenu, state: &'a State) ), )) }), - )); - } - } + ); + a + }) + .collect() +} - for (i, (icon, tooltip, action)) in icons.iter().enumerate() { - if bottom_center_texture( - state, - icon, - icon_pos(-(icons.len() as i8) / 2 + i as i8, -1), - tooltip, - ) { - return action(); - } - } - StateUpdate::None +fn building_icons<'a>(game: &'a Game, menu: &'a CityMenu, state: &'a State) -> IconActionVec<'a> { + let owner = menu.get_city_owner(game); + let city = menu.get_city(game); + building_names() + .iter() + .filter_map(|(b, _)| { + if menu.is_city_owner() && menu.player.can_play_action && city.can_construct(*b, owner) + { + Some(*b) + } else { + None + } + }) + .flat_map(|b| new_building_positions(b, city, &game.map)) + .map(|(b, pos)| { + let name = building_name(b); + let tooltip = format!( + "Built {}{} for {}", + name, + pos.map_or(String::new(), |p| format!(" at {p}")), + owner.construct_cost(b, city), + ); + let a: IconAction<'a> = ( + &state.assets.buildings[&b], + tooltip, + Box::new(move || { + StateUpdate::OpenDialog(ActiveDialog::ConstructionPayment( + ConstructionPayment::new( + game, + name, + menu.player.index, + menu.city_position, + ConstructionProject::Building(b, pos), + ), + )) + }), + ); + a + }) + .collect() +} + +fn recruit_button<'a>(game: &'a Game, menu: &'a CityMenu, state: &'a State) -> IconAction<'a> { + ( + &state.assets.units[&UnitType::Infantry], + "Recruit Units".to_string(), + Box::new(|| { + RecruitAmount::new_selection( + game, + menu.player.index, + menu.city_position, + Units::empty(), + None, + &[], + ) + }), + ) +} + +fn collect_resources_button<'a>( + game: &'a Game, + menu: &'a CityMenu, + state: &'a State, +) -> IconAction<'a> { + ( + &state.assets.resources[&ResourceType::Food], + "Collect Resources".to_string(), + Box::new(|| { + let pos = menu.city_position; + StateUpdate::OpenDialog(ActiveDialog::CollectResources(CollectResources::new( + menu.player.index, + pos, + possible_resource_collections(game, pos, menu.city_owner_index), + ))) + }), + ) } pub fn city_labels(game: &Game, city: &City) -> Vec { @@ -166,11 +231,11 @@ pub fn city_labels(game: &Game, city: &City) -> Vec { .filter_map(|(b, o)| { o.as_ref().map(|o| { if city.player_index == *o { - building_name(b).to_string() + building_name(*b).to_string() } else { format!( "{} (owned by {})", - building_name(b), + building_name(*b), game.get_player(*o).get_name() ) } @@ -244,7 +309,7 @@ pub fn draw_city(owner: &Player, city: &City, state: &State) { let tooltip = if matches!(state.active_dialog, ActiveDialog::CulturalInfluence) { "" } else { - building_name(b) + building_name(*b) }; draw_scaled_icon( state, @@ -271,10 +336,10 @@ pub fn building_position(city: &City, center: Point, i: i32, building: Building) } } -pub fn building_name(b: &Building) -> &str { +pub fn building_name(b: Building) -> &'static str { building_names() .iter() - .find_map(|(b2, n)| if b == b2 { Some(n) } else { None }) + .find_map(|(b2, n)| if &b == b2 { Some(n) } else { None }) .unwrap() } diff --git a/client/src/client_state.rs b/client/src/client_state.rs index 1e5d8a29..893d7b1c 100644 --- a/client/src/client_state.rs +++ b/client/src/client_state.rs @@ -112,7 +112,7 @@ impl ActiveDialog { ActiveDialog::CulturalInfluenceResolution(c) => vec![format!( "Pay {} culture tokens to influence {}", c.roll_boost_cost, - building_name(&c.city_piece) + building_name(c.city_piece) )], ActiveDialog::FreeAdvance => { vec!["Click on an advance to take it for free".to_string()] @@ -425,18 +425,20 @@ impl State { } pub fn update_from_game(&mut self, game: &Game) -> GameSyncRequest { + let dialog = self.game_state_dialog(game); self.clear(); - - self.active_dialog = self.game_state_dialog(game); + self.active_dialog = dialog; GameSyncRequest::None } #[must_use] pub fn game_state_dialog(&self, game: &Game) -> ActiveDialog { match &game.state { - GameState::Movement { .. } => { - ActiveDialog::MoveUnits(MoveSelection::new(game.active_player())) - } + GameState::Movement { .. } => ActiveDialog::MoveUnits(MoveSelection::new( + game.active_player(), + self.focused_tile, + game, + )), GameState::CulturalInfluenceResolution(c) => { ActiveDialog::CulturalInfluenceResolution(c.clone()) } diff --git a/client/src/construct_ui.rs b/client/src/construct_ui.rs index ea74a55e..9d5a9c5d 100644 --- a/client/src/construct_ui.rs +++ b/client/src/construct_ui.rs @@ -20,16 +20,20 @@ use crate::recruit_unit_ui::RecruitSelection; use crate::resource_ui::{new_resource_map, ResourceType}; use crate::select_ui::CountSelector; -pub fn building_positions(building: Building, city: &City, map: &Map) -> Vec> { +pub fn new_building_positions( + building: Building, + city: &City, + map: &Map, +) -> Vec<(Building, Option)> { if building != Building::Port { - return vec![None]; + return vec![(building, None)]; } map.tiles .iter() .filter_map(|(p, t)| { if *t == Terrain::Water && city.position.is_neighbor(*p) { - Some(Some(*p)) + Some((building, Some(*p))) } else { None } diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index d34dec6f..4923d52e 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -1,6 +1,7 @@ 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; @@ -16,8 +17,12 @@ pub struct IncreaseHappiness { } impl IncreaseHappiness { - pub fn new(steps: Vec<(Position, u32)>, cost: ResourcePile) -> IncreaseHappiness { - IncreaseHappiness { steps, cost } + pub fn new(p: &Player) -> IncreaseHappiness { + let steps = p.cities.iter().map(|c| (c.position, 0)).collect(); + IncreaseHappiness { + steps, + cost: ResourcePile::empty(), + } } } @@ -29,7 +34,7 @@ pub fn increase_happiness_click( ) -> StateUpdate { if let Some(city) = player.get(game).get_city(pos) { StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(add_increase_happiness( - city, pos, h, + city, h, ))) } else { StateUpdate::None @@ -38,7 +43,6 @@ pub fn increase_happiness_click( pub fn add_increase_happiness( city: &City, - pos: Position, increase_happiness: &IncreaseHappiness, ) -> IncreaseHappiness { let mut total_cost = increase_happiness.cost.clone(); @@ -47,7 +51,7 @@ pub fn add_increase_happiness( .iter() .map(|(p, steps)| { let old_steps = *steps; - if *p == pos { + if *p == city.position { if let Some(r) = increase_happiness_steps(city, &total_cost, old_steps) { total_cost = r.1; return (*p, r.0); @@ -57,7 +61,10 @@ pub fn add_increase_happiness( }) .collect(); - IncreaseHappiness::new(new_steps, total_cost) + IncreaseHappiness { + steps: new_steps, + cost: total_cost, + } } fn increase_happiness_steps( @@ -116,15 +123,3 @@ pub fn increase_happiness_menu( } StateUpdate::None } - -pub fn start_increase_happiness(game: &Game, player: &ShownPlayer) -> StateUpdate { - StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( - player - .get(game) - .cities - .iter() - .map(|c| (c.position, 0)) - .collect(), - ResourcePile::empty(), - ))) -} diff --git a/client/src/influence_ui.rs b/client/src/influence_ui.rs index 58d428d8..c790e403 100644 --- a/client/src/influence_ui.rs +++ b/client/src/influence_ui.rs @@ -30,7 +30,7 @@ pub fn cultural_influence_resolution_dialog( r: &CulturalInfluenceResolution, player: &ShownPlayer, ) -> StateUpdate { - let name = building_name(&r.city_piece); + let name = building_name(r.city_piece); let pile = ResourcePile::culture_tokens(r.roll_boost_cost); show_resource_pile(state, player, &pile); if ok_button( @@ -96,7 +96,7 @@ fn show_city( *b, ) { if player.resources.can_afford(&cost) { - let name = building_name(b); + let name = building_name(*b); state.set_world_camera(); draw_circle_lines(center.x, center.y, BUILDING_SIZE, 1., WHITE); show_tooltip_for_circle( diff --git a/client/src/map_ui.rs b/client/src/map_ui.rs index f7d276c1..243edb5f 100644 --- a/client/src/map_ui.rs +++ b/client/src/map_ui.rs @@ -8,11 +8,12 @@ use server::game::{Game, GameState}; use server::map::Terrain; use server::playing_actions::PlayingAction; use server::position::Position; -use server::unit::MovementRestriction; +use server::unit::{MovementRestriction, Unit}; -use crate::city_ui::{draw_city, show_city_menu, CityMenu}; +use crate::city_ui::{draw_city, show_city_menu, CityMenu, IconAction, IconActionVec}; use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; use crate::layout_ui::{bottom_center_texture, icon_pos}; +use crate::move_ui::movable_units; use crate::{collect_ui, hex_ui, unit_ui}; fn terrain_font_color(t: &Terrain) -> Color { @@ -139,41 +140,84 @@ fn highlight_if(b: bool) -> f32 { } } -pub fn show_tile_menu( - game: &Game, - position: Position, - player: &ShownPlayer, - state: &State, +pub fn show_tile_menu<'a>( + game: &'a Game, + pos: Position, + player: &'a ShownPlayer, + state: &'a State, ) -> StateUpdate { - if let Some(c) = game.get_any_city(position) { - show_city_menu( - game, - &CityMenu::new(player, c.player_index, position), - state, - ) + if let Some(c) = game.get_any_city(pos) { + return show_city_menu(game, &CityMenu::new(player, c.player_index, pos), state); + }; + + let settlers: Vec = unit_ui::units_on_tile(game, pos) + .filter_map(|(_, unit)| { + if unit.can_found_city(game) { + Some(unit) + } else { + None + } + }) + .collect::>(); + + show_map_action_buttons( + state, + &vec![ + move_units_button(game, pos, player, state), + found_city_button(state, settlers), + ] + .into_iter() + .flatten() + .collect(), + ) +} + +fn found_city_button(state: &State, settlers: Vec) -> Option> { + if settlers.is_empty() { + None } else { - let units = unit_ui::units_on_tile(game, position); - let settlers = units - .filter_map(|(_, unit)| { - if unit.can_found_city(game) { - Some(unit) - } else { - None - } - }) - .collect::>(); - if !settlers.is_empty() - && bottom_center_texture(state, &state.assets.settle, icon_pos(0, -1), "Settle") - { - let settler = settlers - .iter() - .find(|u| u.movement_restriction != MovementRestriction::None) - .unwrap_or(&settlers[0]); - StateUpdate::execute(Action::Playing(PlayingAction::FoundCity { - settler: settler.id, - })) - } else { - StateUpdate::None + Some(( + &state.assets.settle, + "Settle".to_string(), + Box::new(move || { + let settler = settlers + .iter() + .find(|u| u.movement_restriction != MovementRestriction::None) + .unwrap_or(&settlers[0]); + StateUpdate::execute(Action::Playing(PlayingAction::FoundCity { + settler: settler.id, + })) + }), + )) + } +} + +pub fn move_units_button<'a>( + game: &'a Game, + pos: Position, + player: &'a ShownPlayer, + state: &'a State, +) -> Option> { + if movable_units(pos, game, player.get(game)).is_empty() { + return None; + } + Some(( + &state.assets.move_units, + "Move units".to_string(), + Box::new(move || StateUpdate::execute(Action::Playing(PlayingAction::MoveUnits))), + )) +} + +pub fn show_map_action_buttons(state: &State, icons: &IconActionVec) -> StateUpdate { + for (i, (icon, tooltip, action)) in icons.iter().enumerate() { + if bottom_center_texture( + state, + icon, + icon_pos(-(icons.len() as i8) / 2 + i as i8, -1), + tooltip, + ) { + return action(); } } + StateUpdate::None } diff --git a/client/src/move_ui.rs b/client/src/move_ui.rs index e6327192..47afeafe 100644 --- a/client/src/move_ui.rs +++ b/client/src/move_ui.rs @@ -3,46 +3,48 @@ use macroquad::math::{u32, Vec2}; use server::action::Action; use server::game::Game; use server::game::GameState::Movement; +use server::player::Player; use server::position::Position; use server::unit::MovementAction; use crate::client_state::{ActiveDialog, StateUpdate}; use crate::unit_ui::{unit_at_pos, unit_selection_clicked}; -fn possible_destinations( +pub fn possible_destinations( game: &Game, start: Position, player_index: usize, - units: &Vec, + units: &[u32], ) -> Vec { - if let Movement { + let player = game.get_player(player_index); + + let (moved_units, movement_actions_left) = if let Movement { movement_actions_left, moved_units, } = &game.state { - let player = game.get_player(player_index); - - game.map - .tiles - .keys() - .copied() - .filter(|dest| { - start != *dest - && player - .can_move_units( - game, - units, - start, - *dest, - *movement_actions_left, - moved_units, - ) - .is_ok() - }) - .collect::>() + (moved_units, movement_actions_left) } else { - vec![] - } + (&vec![], &1) + }; + + start + .neighbors() + .into_iter() + .filter(|dest| { + game.map.tiles.contains_key(dest) + && player + .can_move_units( + game, + units, + start, + *dest, + *movement_actions_left, + moved_units, + ) + .is_ok() + }) + .collect::>() } pub fn click(pos: Position, s: &MoveSelection, mouse_pos: Vec2, game: &Game) -> StateUpdate { @@ -61,7 +63,11 @@ 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); - unit_selection_clicked(unit_id, &mut new.units); + if new.units.is_empty() { + new.units = movable_units(pos, game, p); + } else { + unit_selection_clicked(unit_id, &mut new.units); + } if new.units.is_empty() { new.destinations.clear(); new.start = None; @@ -73,6 +79,14 @@ pub fn click(pos: Position, s: &MoveSelection, mouse_pos: Vec2, game: &Game) -> } } +pub fn movable_units(pos: Position, game: &Game, p: &Player) -> Vec { + p.units + .iter() + .filter(|u| !possible_destinations(game, pos, p.index, &[u.id]).is_empty()) + .map(|u| u.id) + .collect() +} + #[derive(Clone, Debug)] pub struct MoveSelection { pub player_index: usize, @@ -82,12 +96,23 @@ pub struct MoveSelection { } impl MoveSelection { - pub fn new(player_index: usize) -> MoveSelection { - MoveSelection { - player_index, - units: vec![], - start: None, - destinations: vec![], + pub fn new(player_index: usize, start: Option, game: &Game) -> MoveSelection { + match start { + Some(pos) => { + let movable_units = movable_units(pos, game, game.get_player(player_index)); + MoveSelection { + player_index, + start: Some(pos), + destinations: possible_destinations(game, pos, player_index, &movable_units), + units: movable_units, + } + } + None => MoveSelection { + player_index, + start: None, + units: vec![], + destinations: vec![], + }, } } } diff --git a/client/src/player_ui.rs b/client/src/player_ui.rs index 6b4ebdc3..61ec6e68 100644 --- a/client/src/player_ui.rs +++ b/client/src/player_ui.rs @@ -2,7 +2,7 @@ use crate::assets::Assets; use crate::city_ui::city_labels; use crate::client::Features; use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate}; -use crate::happiness_ui::start_increase_happiness; +use crate::happiness_ui::IncreaseHappiness; use crate::layout_ui::{ bottom_center_texture, bottom_left_texture, bottom_right_texture, icon_pos, left_mouse_button_pressed_in_rect, top_center_texture, ICON_SIZE, @@ -307,7 +307,7 @@ fn action_buttons( player: &ShownPlayer, assets: &Assets, ) -> StateUpdate { - if bottom_left_texture(state, &assets.movement, icon_pos(0, -3), "Move units") { + if bottom_left_texture(state, &assets.move_units, icon_pos(0, -3), "Move units") { return StateUpdate::execute(Action::Playing(PlayingAction::MoveUnits)); } if bottom_left_texture( @@ -324,7 +324,9 @@ fn action_buttons( icon_pos(0, -2), "Increase happiness", ) { - return start_increase_happiness(game, player); + return StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( + player.get(game), + ))); } if bottom_left_texture( state, diff --git a/server/src/player.rs b/server/src/player.rs index f2a499e6..59459c4c 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -748,7 +748,7 @@ impl Player { pub fn can_move_units( &self, game: &Game, - units: &Vec, + units: &[u32], starting: Position, destination: Position, movement_actions_left: u32,