From aca1f115214634fa4c270fbf3e4aa339346cfc4d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 16:04:36 +0200 Subject: [PATCH 1/8] move active dialog add title --- client/src/advance_ui.rs | 9 ++-- client/src/collect_ui.rs | 2 +- client/src/combat_ui.rs | 7 ++- client/src/construct_ui.rs | 6 +++ client/src/dialog_ui.rs | 18 +++++-- client/src/game_loop.rs | 24 +++++---- client/src/happiness_ui.rs | 4 +- client/src/influence_ui.rs | 3 +- client/src/log_ui.rs | 4 +- client/src/map_ui.rs | 93 +++++++++++++++++------------------ client/src/move_ui.rs | 1 + client/src/payment_ui.rs | 3 +- client/src/recruit_unit_ui.rs | 4 ++ client/src/select_ui.rs | 6 +-- client/src/status_phase_ui.rs | 10 ++-- client/src/unit_ui.rs | 3 +- 16 files changed, 106 insertions(+), 91 deletions(-) diff --git a/client/src/advance_ui.rs b/client/src/advance_ui.rs index ad392055..c7dbaac4 100644 --- a/client/src/advance_ui.rs +++ b/client/src/advance_ui.rs @@ -1,5 +1,6 @@ use std::cmp::min; use std::collections::HashMap; +use std::fmt::format; use macroquad::math::bool; @@ -78,7 +79,7 @@ impl HasPayment for AdvancePayment { } pub fn show_advance_menu(game: &Game, player_index: usize) -> StateUpdate { - show_generic_advance_menu(game, player_index, true, |name| { + show_generic_advance_menu("Advances", game, player_index, true, |name| { StateUpdate::SetDialog(ActiveDialog::AdvancePayment(AdvancePayment::new( game, player_index, @@ -88,18 +89,19 @@ pub fn show_advance_menu(game: &Game, player_index: usize) -> StateUpdate { } pub fn show_free_advance_menu(game: &Game, player_index: usize) -> StateUpdate { - show_generic_advance_menu(game, player_index, false, |name| { + show_generic_advance_menu("Select a free advance", game, player_index, false, |name| { StateUpdate::status_phase(StatusPhaseAction::FreeAdvance(name)) }) } pub fn show_generic_advance_menu( + title: &str, game: &Game, player_index: usize, close_button: bool, new_update: impl Fn(String) -> StateUpdate, ) -> StateUpdate { - dialog_window(close_button, |ui| { + dialog_window(title, close_button, |ui| { for a in get_all() { let name = a.name; let p = game.get_player(player_index); @@ -125,6 +127,7 @@ pub fn show_generic_advance_menu( pub fn pay_advance_dialog(ap: &AdvancePayment) -> StateUpdate { payment_dialog( + &format!("Pay for advance {}", ap.name), ap, AdvancePayment::valid, |ap| { diff --git a/client/src/collect_ui.rs b/client/src/collect_ui.rs index eef9ee4f..c9e41e33 100644 --- a/client/src/collect_ui.rs +++ b/client/src/collect_ui.rs @@ -48,7 +48,7 @@ impl CollectResources { } pub fn collect_resources_dialog(game: &Game, collect: &CollectResources) -> StateUpdate { - active_dialog_window(|ui| { + active_dialog_window("Collect Resources", |ui| { let r: ResourcePile = collect .collections .clone() diff --git a/client/src/combat_ui.rs b/client/src/combat_ui.rs index 2a017172..7c5d9404 100644 --- a/client/src/combat_ui.rs +++ b/client/src/combat_ui.rs @@ -10,8 +10,7 @@ use crate::unit_ui; use crate::unit_ui::UnitSelection; pub fn retreat_dialog() -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Do you want to retreat?"); + active_dialog_window("Do you want to retreat?", |ui| { if ui.button(None, "Retreat") { return retreat(true); } @@ -27,8 +26,7 @@ fn retreat(retreat: bool) -> StateUpdate { } pub fn place_settler_dialog() -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Select a city to place a settler in."); + active_dialog_window("Select a city to place a settler in.", |ui| { StateUpdate::None }) } @@ -87,6 +85,7 @@ impl ConfirmSelection for RemoveCasualtiesSelection { pub fn remove_casualties_dialog(game: &Game, sel: &RemoveCasualtiesSelection) -> StateUpdate { unit_ui::unit_selection_dialog::( game, + "Remove casualties", sel, |new| StateUpdate::SetDialog(ActiveDialog::RemoveCasualties(new.clone())), |new: RemoveCasualtiesSelection| { diff --git a/client/src/construct_ui.rs b/client/src/construct_ui.rs index 0ced4018..e3d6552e 100644 --- a/client/src/construct_ui.rs +++ b/client/src/construct_ui.rs @@ -43,6 +43,7 @@ pub fn add_construct_button( return StateUpdate::SetDialog(ActiveDialog::ConstructionPayment( ConstructionPayment::new( game, + name, menu.player_index, menu.city_position, ConstructionProject::Building(building.clone(), pos), @@ -81,6 +82,7 @@ pub fn add_wonder_buttons(game: &Game, menu: &CityMenu, ui: &mut Ui) -> StateUpd return StateUpdate::SetDialog(ActiveDialog::ConstructionPayment( ConstructionPayment::new( game, + &w.name, menu.player_index, menu.city_position, ConstructionProject::Wonder(w.name.clone()), @@ -93,6 +95,7 @@ pub fn add_wonder_buttons(game: &Game, menu: &CityMenu, ui: &mut Ui) -> StateUpd pub fn pay_construction_dialog(game: &Game, payment: &ConstructionPayment) -> StateUpdate { payment_dialog( + &format!("Pay for {}", payment.name), payment, |cp| cp.payment.get(ResourceType::Discount).selectable.current == 0, |cp| match &cp.project { @@ -170,6 +173,7 @@ pub enum ConstructionProject { #[derive(Clone)] pub struct ConstructionPayment { + pub name: String, pub player_index: usize, pub city_position: Position, pub project: ConstructionProject, @@ -180,6 +184,7 @@ pub struct ConstructionPayment { impl ConstructionPayment { pub fn new( game: &Game, + name: &str, player_index: usize, city_position: Position, project: ConstructionProject, @@ -211,6 +216,7 @@ impl ConstructionPayment { let payment = ConstructionPayment::new_payment(&payment_options); ConstructionPayment { + name: name.to_string(), player_index, city_position, project, diff --git a/client/src/dialog_ui.rs b/client/src/dialog_ui.rs index 999bfc37..f4bd853e 100644 --- a/client/src/dialog_ui.rs +++ b/client/src/dialog_ui.rs @@ -5,20 +5,28 @@ use macroquad::ui::{root_ui, Ui}; use crate::ui_state::StateUpdate; -pub fn active_dialog_window(f: F) -> StateUpdate +pub fn active_dialog_window(title: &str, f: F) -> StateUpdate where F: FnOnce(&mut Ui) -> StateUpdate, { - dialog_window(false, f) + dialog_window(title, false, f) } -pub fn dialog_window(close_button: bool, f: F) -> StateUpdate +pub fn closeable_dialog_window(title: &str, f: F) -> StateUpdate where F: FnOnce(&mut Ui) -> StateUpdate, { - let window = Window::new(hash!(), vec2(100., 100.), vec2(1000., 1000.)) + dialog_window(title, true, f) +} + +pub fn dialog_window(title: &str, close_button: bool, f: F) -> StateUpdate +where + F: FnOnce(&mut Ui) -> StateUpdate, +{ + let window = Window::new(hash!(), vec2(1100., 400.), vec2(800., 350.)) .titlebar(true) - .movable(true) + .movable(false) + .label(title) .close_button(close_button); let ui = &mut root_ui(); diff --git a/client/src/game_loop.rs b/client/src/game_loop.rs index f1183c50..fba65645 100644 --- a/client/src/game_loop.rs +++ b/client/src/game_loop.rs @@ -21,7 +21,7 @@ use crate::hex_ui::pixel_to_coordinate; use crate::log_ui::show_log; use crate::map_ui::{draw_map, show_tile_menu}; use crate::player_ui::{show_global_controls, show_globals, show_resources, show_wonders}; -use crate::ui_state::{ActiveDialog, State, StateUpdate, StateUpdates}; +use crate::ui_state::{ActiveDialog, PendingUpdate, State, StateUpdate, StateUpdates}; use crate::{combat_ui, influence_ui, move_ui, recruit_unit_ui, status_phase_ui}; const EXPORT_FILE: &str = "game.json"; @@ -63,8 +63,8 @@ fn game_loop(game: &mut Game, state: &State) -> StateUpdate { return StateUpdate::None; }; - if state.pending_update.is_some() { - updates.add(show_pending_update(state)); + if let Some(u) = &state.pending_update { + updates.add(show_pending_update(u)); return updates.result(); } @@ -124,16 +124,14 @@ fn export(game: &Game) { .expect("Failed to write export file"); } -fn show_pending_update(state: &State) -> StateUpdate { - active_dialog_window(|ui| { - if let Some(update) = &state.pending_update { - ui.label(None, &format!("Warning: {}", update.warning.join(", "))); - if ui.button(None, "OK") { - return StateUpdate::ResolvePendingUpdate(true); - } - if ui.button(None, "Cancel") { - return StateUpdate::ResolvePendingUpdate(false); - } +fn show_pending_update(update: &PendingUpdate) -> StateUpdate { + active_dialog_window("Are you sure?", |ui| { + ui.label(None, &format!("Warning: {}", update.warning.join(", "))); + if ui.button(None, "OK") { + return StateUpdate::ResolvePendingUpdate(true); + } + if ui.button(None, "Cancel") { + return StateUpdate::ResolvePendingUpdate(false); } StateUpdate::None }) diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index dfeb0186..2501fbf5 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -77,7 +77,8 @@ fn increase_happiness_new_steps( } pub fn increase_happiness_menu(h: &IncreaseHappiness) -> StateUpdate { - active_dialog_window(|ui| { + active_dialog_window("Increase Happiness", |ui| { + ui.label(None, &format!("Cost: {:?}", h.cost)); if ui.button(None, "Cancel") { return StateUpdate::Cancel; } @@ -86,7 +87,6 @@ pub fn increase_happiness_menu(h: &IncreaseHappiness) -> StateUpdate { happiness_increases: h.steps.clone(), })); } - ui.label(None, &format!("Cost: {:?}", h.cost)); StateUpdate::None }) } diff --git a/client/src/influence_ui.rs b/client/src/influence_ui.rs index fc7feacd..8bab80b2 100644 --- a/client/src/influence_ui.rs +++ b/client/src/influence_ui.rs @@ -57,8 +57,7 @@ pub fn closest_city(game: &Game, menu: &CityMenu) -> Position { } pub fn cultural_influence_resolution_dialog(c: &CulturalInfluenceResolution) -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Cultural Influence Resolution"); + active_dialog_window("Cultural Influence Resolution", |ui| { if ui.button( None, format!( diff --git a/client/src/log_ui.rs b/client/src/log_ui.rs index cb92593a..c6469099 100644 --- a/client/src/log_ui.rs +++ b/client/src/log_ui.rs @@ -1,10 +1,10 @@ use server::game::Game; -use crate::dialog_ui::dialog_window; +use crate::dialog_ui::closeable_dialog_window; use crate::ui_state::StateUpdate; pub fn show_log(game: &Game) -> StateUpdate { - dialog_window(true, |ui| { + closeable_dialog_window("Log", |ui| { game.log.iter().for_each(|l| { ui.label(None, l); }); diff --git a/client/src/map_ui.rs b/client/src/map_ui.rs index 9b808097..10702dcf 100644 --- a/client/src/map_ui.rs +++ b/client/src/map_ui.rs @@ -12,7 +12,7 @@ use server::position::Position; use server::unit::{MovementRestriction, Unit}; use crate::city_ui::{draw_city, show_city_menu}; -use crate::dialog_ui::dialog_window; +use crate::dialog_ui::closeable_dialog_window; use crate::ui_state::{can_play_action, ActiveDialog, CityMenu, State, StateUpdate}; use crate::{collect_ui, hex_ui, unit_ui}; @@ -128,54 +128,53 @@ pub fn show_generic_tile_menu( suffix: Vec, additional: impl FnOnce(&mut Ui) -> StateUpdate, ) -> StateUpdate { - dialog_window(true, |ui| { - ui.label( - None, - &format!( - "{}/{}", - position, - game.map - .tiles - .get(&position) - .map_or("outside the map", terrain_name), - ), - ); - let units: Vec<(&Unit, String)> = unit_ui::units_on_tile(game, position) - .map(|(p, u)| { - let unit = game.get_player(p).get_unit(u).unwrap(); - (unit, unit_ui::label(unit)) - }) - .collect(); - - let units_str = &units.iter().map(|(_, l)| l).join(", "); - if !units_str.is_empty() { - ui.label(None, units_str); - } - for s in suffix { - ui.label(None, &s); - } + closeable_dialog_window( + &format!( + "{}/{}", + position, + game.map + .tiles + .get(&position) + .map_or("outside the map", terrain_name), + ), + |ui| { + let units: Vec<(&Unit, String)> = unit_ui::units_on_tile(game, position) + .map(|(p, u)| { + let unit = game.get_player(p).get_unit(u).unwrap(); + (unit, unit_ui::label(unit)) + }) + .collect(); - let settlers = &units - .iter() - .filter_map(|(unit, _)| { - if unit.can_found_city(game) { - Some(unit) - } else { - None - } - }) - .collect::>(); + let units_str = &units.iter().map(|(_, l)| l).join(", "); + if !units_str.is_empty() { + ui.label(None, units_str); + } + for s in suffix { + ui.label(None, &s); + } - if can_play_action(game) && !settlers.is_empty() && ui.button(None, "Settle") { - let settler = settlers + let settlers = &units .iter() - .find(|u| u.movement_restriction != MovementRestriction::None) - .unwrap_or(&settlers[0]); - return StateUpdate::execute(Action::Playing(PlayingAction::FoundCity { - settler: settler.id, - })); - } + .filter_map(|(unit, _)| { + if unit.can_found_city(game) { + Some(unit) + } else { + None + } + }) + .collect::>(); + + if can_play_action(game) && !settlers.is_empty() && ui.button(None, "Settle") { + let settler = settlers + .iter() + .find(|u| u.movement_restriction != MovementRestriction::None) + .unwrap_or(&settlers[0]); + return StateUpdate::execute(Action::Playing(PlayingAction::FoundCity { + settler: settler.id, + })); + } - additional(ui) - }) + additional(ui) + }, + ) } diff --git a/client/src/move_ui.rs b/client/src/move_ui.rs index cba62f46..7778fa4b 100644 --- a/client/src/move_ui.rs +++ b/client/src/move_ui.rs @@ -14,6 +14,7 @@ use crate::unit_ui::UnitSelection; pub fn move_units_dialog(game: &Game, sel: &MoveSelection) -> StateUpdate { unit_ui::unit_selection_dialog::( game, + "Move Units", sel, |new| update_possible_destinations(game, new.clone()), |_new| StateUpdate::None, diff --git a/client/src/payment_ui.rs b/client/src/payment_ui.rs index 05736c97..a1c1b934 100644 --- a/client/src/payment_ui.rs +++ b/client/src/payment_ui.rs @@ -77,6 +77,7 @@ pub trait HasPayment { } pub fn payment_dialog( + title: &str, has_payment: &T, is_valid: impl FnOnce(&T) -> bool, execute_action: impl FnOnce(&T) -> StateUpdate, @@ -84,7 +85,7 @@ pub fn payment_dialog( plus: impl Fn(&T, ResourceType) -> StateUpdate, minus: impl Fn(&T, ResourceType) -> StateUpdate, ) -> StateUpdate { - select_ui::count_dialog( + select_ui::count_dialog(title, has_payment, |p| p.payment().resources.clone(), |p| resource_name(p.resource), diff --git a/client/src/recruit_unit_ui.rs b/client/src/recruit_unit_ui.rs index 3cc14700..bb9e32a5 100644 --- a/client/src/recruit_unit_ui.rs +++ b/client/src/recruit_unit_ui.rs @@ -212,6 +212,7 @@ impl ConfirmSelection for RecruitSelection { pub fn select_dialog(game: &Game, a: &RecruitAmount) -> StateUpdate { select_ui::count_dialog( + "Recruit units", a, |s| s.selectable.clone(), |s| s.name.as_ref(), @@ -222,6 +223,7 @@ pub fn select_dialog(game: &Game, a: &RecruitAmount) -> StateUpdate { if sel.is_finished() { StateUpdate::SetDialog(ActiveDialog::ConstructionPayment(ConstructionPayment::new( game, + "units", amount.player_index, amount.city_position, ConstructionProject::Units(sel), @@ -272,11 +274,13 @@ fn update_selection( pub fn replace_dialog(game: &Game, sel: &RecruitSelection) -> StateUpdate { unit_ui::unit_selection_dialog::( game, + "Replace units", sel, |new| StateUpdate::SetDialog(ActiveDialog::ReplaceUnits(new.clone())), |new: RecruitSelection| { StateUpdate::SetDialog(ActiveDialog::ConstructionPayment(ConstructionPayment::new( game, + "units", new.amount.player_index, new.amount.city_position, ConstructionProject::Units(new), diff --git a/client/src/select_ui.rs b/client/src/select_ui.rs index 18f78f47..c51bed6b 100644 --- a/client/src/select_ui.rs +++ b/client/src/select_ui.rs @@ -20,6 +20,7 @@ pub trait HasCountSelectableObject { #[allow(clippy::too_many_arguments)] pub fn count_dialog( + title: &str, container: &C, get_objects: impl Fn(&C) -> Vec, label: impl Fn(&O) -> &str, @@ -29,7 +30,7 @@ pub fn count_dialog( plus: impl Fn(&C, &O) -> StateUpdate, minus: impl Fn(&C, &O) -> StateUpdate, ) -> StateUpdate { - active_dialog_window(|ui| { + active_dialog_window(title, |ui| { let mut updates = StateUpdates::new(); for (i, p) in get_objects(container).iter().enumerate() { if show(container, p) { @@ -85,8 +86,7 @@ pub fn selection_dialog( on_change: impl Fn(T) -> StateUpdate, on_ok: impl FnOnce(T) -> StateUpdate, ) -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, title); + active_dialog_window(title, |ui| { for name in sel.all() { let can_sel = sel.can_select(game, name); let is_selected = sel.selected().contains(name); diff --git a/client/src/status_phase_ui.rs b/client/src/status_phase_ui.rs index 4361418f..4ea53bf0 100644 --- a/client/src/status_phase_ui.rs +++ b/client/src/status_phase_ui.rs @@ -7,8 +7,7 @@ use server::game::Game; use server::status_phase::{ChangeGovernmentType, StatusPhaseAction}; pub fn determine_first_player_dialog(game: &Game) -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Who should be the first player in the next age?"); + active_dialog_window("Who should be the first player in the next age?", |ui| { for p in &game.players { if ui.button( None, @@ -22,8 +21,7 @@ pub fn determine_first_player_dialog(game: &Game) -> StateUpdate { } pub fn raze_city_dialog() -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Select a city to raze - or decline."); + active_dialog_window("Select a city to raze - or decline.", |ui| { if ui.button(None, "Decline") { return StateUpdate::status_phase(StatusPhaseAction::RaseSize1City(None)); } @@ -85,9 +83,7 @@ impl ConfirmSelection for ChooseAdditionalAdvances { } pub fn change_government_type_dialog(game: &Game) -> StateUpdate { - active_dialog_window(|ui| { - ui.label(None, "Select additional advances:"); - + active_dialog_window("Select additional advances", |ui| { let current = game .get_player(game.active_player()) .government() diff --git a/client/src/unit_ui.rs b/client/src/unit_ui.rs index b8485277..722d8a1b 100644 --- a/client/src/unit_ui.rs +++ b/client/src/unit_ui.rs @@ -80,12 +80,13 @@ pub trait UnitSelection: ConfirmSelection { pub fn unit_selection_dialog( game: &Game, + title: &str, sel: &T, on_change: impl Fn(T) -> StateUpdate, on_ok: impl FnOnce(T) -> StateUpdate, additional: impl FnOnce(&mut Ui) -> StateUpdate, ) -> StateUpdate { - active_dialog_window(|ui| { + active_dialog_window(title, |ui| { if let Some(current_tile) = sel.current_tile() { for (p, unit_id) in units_on_tile(game, current_tile) { let unit = game.get_player(p).get_unit(unit_id).unwrap(); From ac233b06d37952bd42e46fc62a4bd9860c59547b Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 17:11:16 +0200 Subject: [PATCH 2/8] new client layout --- client/src/advance_ui.rs | 1 - client/src/combat_ui.rs | 2 +- client/src/game_loop.rs | 8 +++--- client/src/happiness_ui.rs | 2 +- client/src/payment_ui.rs | 3 ++- client/src/player_ui.rs | 50 +++++++++++++++----------------------- server/src/game.rs | 46 ++++++++++------------------------- 7 files changed, 41 insertions(+), 71 deletions(-) diff --git a/client/src/advance_ui.rs b/client/src/advance_ui.rs index c7dbaac4..7375b1d9 100644 --- a/client/src/advance_ui.rs +++ b/client/src/advance_ui.rs @@ -1,6 +1,5 @@ use std::cmp::min; use std::collections::HashMap; -use std::fmt::format; use macroquad::math::bool; diff --git a/client/src/combat_ui.rs b/client/src/combat_ui.rs index 7c5d9404..2856a561 100644 --- a/client/src/combat_ui.rs +++ b/client/src/combat_ui.rs @@ -26,7 +26,7 @@ fn retreat(retreat: bool) -> StateUpdate { } pub fn place_settler_dialog() -> StateUpdate { - active_dialog_window("Select a city to place a settler in.", |ui| { + active_dialog_window("Select a city to place a settler in.", |_| { StateUpdate::None }) } diff --git a/client/src/game_loop.rs b/client/src/game_loop.rs index fba65645..1e39590d 100644 --- a/client/src/game_loop.rs +++ b/client/src/game_loop.rs @@ -48,17 +48,17 @@ fn game_loop(game: &mut Game, state: &State) -> StateUpdate { show_resources(game, player_index); show_wonders(game, player_index); - if root_ui().button(vec2(1200., 310.), "Log") { + if root_ui().button(vec2(1200., 130.), "Log") { return StateUpdate::OpenDialog(ActiveDialog::Log); }; - if root_ui().button(vec2(1200., 350.), "Advances") { + if root_ui().button(vec2(1200., 100.), "Advances") { return StateUpdate::OpenDialog(ActiveDialog::AdvanceMenu); }; - if root_ui().button(vec2(1200., 450.), "Import") { + if root_ui().button(vec2(1200., 290.), "Import") { import(game); return StateUpdate::Cancel; }; - if root_ui().button(vec2(1250., 450.), "Export") { + if root_ui().button(vec2(1250., 290.), "Export") { export(game); return StateUpdate::None; }; diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index 2501fbf5..ed2a86d8 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -92,7 +92,7 @@ pub fn increase_happiness_menu(h: &IncreaseHappiness) -> StateUpdate { } pub fn show_increase_happiness(game: &Game, player_index: usize) -> StateUpdate { - if can_play_action(game) && root_ui().button(vec2(1200., 480.), "Increase Happiness") { + if can_play_action(game) && root_ui().button(vec2(1200., 60.), "Increase Happiness") { return StateUpdate::SetDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( game.get_player(player_index) .cities diff --git a/client/src/payment_ui.rs b/client/src/payment_ui.rs index a1c1b934..c738700e 100644 --- a/client/src/payment_ui.rs +++ b/client/src/payment_ui.rs @@ -85,7 +85,8 @@ pub fn payment_dialog( plus: impl Fn(&T, ResourceType) -> StateUpdate, minus: impl Fn(&T, ResourceType) -> StateUpdate, ) -> StateUpdate { - select_ui::count_dialog(title, + select_ui::count_dialog( + title, has_payment, |p| p.payment().resources.clone(), |p| resource_name(p.resource), diff --git a/client/src/player_ui.rs b/client/src/player_ui.rs index 3f78604c..22a233e5 100644 --- a/client/src/player_ui.rs +++ b/client/src/player_ui.rs @@ -1,4 +1,3 @@ -use itertools::Itertools; use macroquad::color::BLACK; use macroquad::math::vec2; use macroquad::prelude::*; @@ -6,7 +5,6 @@ use macroquad::text::draw_text; use macroquad::ui::root_ui; use server::action::Action; -use server::combat::Combat; use server::game::{Game, GameState}; use server::playing_actions::PlayingAction; use server::resource_pile::ResourcePile; @@ -14,42 +12,34 @@ use server::resource_pile::ResourcePile; use crate::ui_state::{can_play_action, State, StateUpdate}; pub fn show_globals(game: &Game) { - draw_text(&format!("Age {}", game.age), 1200., 20., 20., BLACK); - draw_text(&format!("Round {}", game.round), 1200., 50., 20., BLACK); + draw_text(&format!("Age {}", game.age), 30., 30., 20., BLACK); + draw_text(&format!("Round {}", game.round), 30., 60., 20., BLACK); draw_text( - &format!("Player {}", game.active_player()), - 1200., - 80., + &format!("Player {}", game.players[game.active_player()].get_name()), + 1400., + 60., 20., BLACK, ); - let status = match game.state { + let status = match &game.state { GameState::Playing => String::from("Play Actions"), GameState::StatusPhase(ref p) => format!("Status Phase: {p:?}"), GameState::Movement { .. } => String::from("Movement"), GameState::CulturalInfluenceResolution(_) => String::from("Cultural Influence Resolution"), - GameState::Combat(Combat { - round, ref phase, .. - }) => { - format!("Combat Round {} Phase {:?}", round, *phase) + GameState::Combat(c) => { + format!("Combat Round {} Phase {:?}", c.round, c.phase) } GameState::PlaceSettler { .. } => String::from("Place Settler"), GameState::Finished => String::from("Finished"), }; - draw_text(&status, 1200., 110., 20., BLACK); + draw_text(&status, 30., 90., 20., BLACK); draw_text( &format!("Actions Left {}", game.actions_left), - 1200., - 140., + 1400., + 30., 20., BLACK, ); - let rolls = game - .dice_roll_log - .iter() - .map(std::string::ToString::to_string) - .join(", "); - draw_text(&format!("Last Dice Rolls {rolls}"), 1200., 600., 20., BLACK); } pub fn show_wonders(game: &Game, player_index: usize) { @@ -57,8 +47,8 @@ pub fn show_wonders(game: &Game, player_index: usize) { for (i, name) in player.wonders.iter().enumerate() { draw_text( &format!("Wonder {name}"), - 1200., - 600. + i as f32 * 30.0, + 1100., + 800. + i as f32 * 30.0, 20., BLACK, ); @@ -73,8 +63,8 @@ pub fn show_wonders(game: &Game, player_index: usize) { "Wonder Card {} cost {} requires {}", &card.name, card.cost, req ), - 1200., - 800. + i as f32 * 30.0, + 1100., + 900. + i as f32 * 30.0, 20., BLACK, ); @@ -87,7 +77,7 @@ pub fn show_resources(game: &Game, player_index: usize) { let mut i: f32 = 0.; let mut res = |label: String| { - draw_text(&label, 1200., 200. + i, 20., BLACK); + draw_text(&label, 1100., 30. + i, 20., BLACK); i += 30.; }; @@ -101,14 +91,14 @@ pub fn show_resources(game: &Game, player_index: usize) { } pub fn show_global_controls(game: &Game, state: &State) -> StateUpdate { - if game.can_undo() && root_ui().button(vec2(1200., 410.), "Undo") { + if game.can_undo() && root_ui().button(vec2(1200., 320.), "Undo") { return StateUpdate::Execute(Action::Undo); } - if game.can_redo() && root_ui().button(vec2(1250., 410.), "Redo") { + if game.can_redo() && root_ui().button(vec2(1250., 320.), "Redo") { return StateUpdate::Execute(Action::Redo); } match game.state { - GameState::Playing if root_ui().button(vec2(1200., 540.), "End Turn") => { + GameState::Playing if root_ui().button(vec2(1200., 350.), "End Turn") => { let left = game.actions_left; StateUpdate::execute_with_warning( Action::Playing(PlayingAction::EndTurn), @@ -122,7 +112,7 @@ pub fn show_global_controls(game: &Game, state: &State) -> StateUpdate { GameState::Playing if !state.has_dialog() && can_play_action(game) - && root_ui().button(vec2(1200., 510.), "Move Units") => + && root_ui().button(vec2(1200., 30.), "Move Units") => { StateUpdate::execute(Action::Playing(PlayingAction::MoveUnits)) } diff --git a/server/src/game.rs b/server/src/game.rs index 00f4dca2..7a65f560 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -361,31 +361,21 @@ impl Game { player_index, ); } - Combat(Combat { - initiation, - round, - phase, - defender, - defender_position, - attacker, - attacker_position, - attackers, - can_retreat, - }) => { + Combat(c) => { let action = action.combat().expect("action should be a combat action"); self.add_action_log_item(ActionLogItem::Combat(action.clone())); execute_combat_action( self, action, - initiation, - round, - phase, - defender, - defender_position, - attacker, - attacker_position, - attackers, - can_retreat, + c.initiation, + c.round, + c.phase, + c.defender, + c.defender_position, + c.attacker, + c.attacker_position, + c.attackers, + c.can_retreat, ); } PlaceSettler { @@ -733,24 +723,14 @@ impl Game { #[must_use] pub fn active_player(&self) -> usize { match &self.state { - Combat(Combat { - initiation: _, - round: _, - phase, - defender: _, - defender_position: _, - attacker, - attacker_position: _, - attackers: _, - can_retreat: _, - }) => match phase { + Combat(c) => match c.phase { CombatPhase::RemoveCasualties { player, casualties: _, defender_hits: _, } - | CombatPhase::PlayActionCard(player) => *player, - CombatPhase::Retreat => *attacker, + | CombatPhase::PlayActionCard(player) => player, + CombatPhase::Retreat => c.attacker, }, PlaceSettler { player_index, From a65efc815dd5d269e94dfd9f185a81b539d5d54c Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 17:49:53 +0200 Subject: [PATCH 3/8] use combat struct --- server/src/combat.rs | 291 ++++++++++++++++--------------------------- server/src/game.rs | 15 +-- 2 files changed, 109 insertions(+), 197 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index 1b79ce1f..63996136 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -1,5 +1,4 @@ use crate::action::CombatAction; -use crate::game::GameState::Playing; use crate::game::{Game, GameState}; use crate::map::Terrain::Water; use crate::position::Position; @@ -46,7 +45,8 @@ pub struct Combat { } impl Combat { - fn new( + #[must_use] + pub fn new( initiation: Box, round: u32, phase: CombatPhase, @@ -77,22 +77,24 @@ pub fn initiate_combat( defender_position: Position, attacker: usize, attacker_position: Position, - mut attackers: Vec, + attackers: Vec, can_retreat: bool, next_game_state: Option, ) { - let mut round = 1; game.lock_undo(); combat_loop( game, - next_game_state.map(Box::new), - &mut round, - defender, - defender_position, - attacker, - attacker_position, - &mut attackers, - can_retreat, + crate::combat::Combat::new( + next_game_state.map_or_else(|| Box::new(game.state.clone()), Box::new), + 1, + CombatPhase::Retreat, // is not used + defender, + defender_position, + attacker, + attacker_position, + attackers, + can_retreat, + ), ); } @@ -102,36 +104,14 @@ pub fn initiate_combat( /// # Panics /// /// Panics if the action is not compatible with the phase -pub fn execute_combat_action( - game: &mut Game, - action: CombatAction, - initiation: Box, - mut round: u32, - phase: CombatPhase, - defender: usize, - defender_position: Position, - attacker: usize, - attacker_position: Position, - mut attackers: Vec, - can_retreat: bool, -) { - assert!(phase.is_compatible_action(&action), "Illegal action"); +pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Combat) { + assert!(c.phase.is_compatible_action(&action), "Illegal action"); game.lock_undo(); match action { CombatAction::PlayActionCard(card) => { assert!(card.is_none()); //todo use card - combat_loop( - game, - Some(initiation), - &mut round, - defender, - defender_position, - attacker, - attacker_position, - &mut attackers, - can_retreat, - ); + combat_loop(game, c); return; } CombatAction::RemoveCasualties(units) => { @@ -139,22 +119,22 @@ pub fn execute_combat_action( player, casualties, defender_hits, - } = phase + } = c.phase else { panic!("Illegal action"); }; assert_eq!(casualties, units.len() as u8, "Illegal action"); - let (fighting_units, opponent) = if player == defender { + let (fighting_units, opponent) = if player == c.defender { ( game.players[player] - .get_units(defender_position) + .get_units(c.defender_position) .iter() .map(|unit| unit.id) .collect(), - attacker, + c.attacker, ) - } else if player == attacker { - (attackers.clone(), defender) + } else if player == c.attacker { + (c.attackers.clone(), c.defender) } else { panic!("Illegal action") }; @@ -164,271 +144,219 @@ pub fn execute_combat_action( ); for unit in units { game.kill_unit(unit, player, opponent); - if player == attacker { - attackers.retain(|id| *id != unit); + if player == c.attacker { + c.attackers.retain(|id| *id != unit); } } if let Some(defender_hits) = defender_hits { - if defender_hits < attackers.len() as u8 && defender_hits > 0 { + if defender_hits < c.attackers.len() as u8 && defender_hits > 0 { game.add_info_log_item(format!( "\t{} has to remove {} of his attacking units", - game.players[attacker].get_name(), + game.players[c.attacker].get_name(), defender_hits )); - game.state = GameState::Combat(Combat::new( - initiation, - round, - CombatPhase::RemoveCasualties { - player: defender, + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.defender, casualties: defender_hits, defender_hits: None, }, - defender, - defender_position, - attacker, - attacker_position, - attackers, - can_retreat, - )); + ..c + }); return; } - if defender_hits >= attackers.len() as u8 { - for id in mem::take(&mut attackers) { - game.kill_unit(id, attacker, defender); + if defender_hits >= c.attackers.len() as u8 { + for id in mem::take(&mut c.attackers) { + game.kill_unit(id, c.attacker, c.defender); } } } - let defenders_left = game.players[defender].get_units(defender_position).len(); - if attackers.is_empty() && defenders_left == 0 { + let defenders_left = game.players[c.defender] + .get_units(c.defender_position) + .len(); + if c.attackers.is_empty() && defenders_left == 0 { //todo if the defender has a fortress he wins game.add_info_log_item(String::from("\tAll attacking and defending units killed each other, ending the battle in a draw")); //todo otherwise: draw - game.state = *initiation; + end_combat(game, c); return; } - if attackers.is_empty() { + if c.attackers.is_empty() { game.add_info_log_item(format!( "\t{} killed all attacking units and wins", - game.players[defender].get_name() + game.players[c.defender].get_name() )); //todo defender wins - game.state = *initiation; + end_combat(game, c); return; } if defenders_left == 0 { game.add_info_log_item(format!( "\t{} killed all defending units and wins", - game.players[attacker].get_name() + game.players[c.attacker].get_name() )); - for unit in &attackers { - let unit = game.players[attacker] + for unit in &c.attackers { + let unit = game.players[c.attacker] .get_unit_mut(*unit) .expect("attacker should have all attacking units"); - unit.position = defender_position; + unit.position = c.defender_position; } - capture_position(game, defender, defender_position, attacker); + capture_position(game, c.defender, c.defender_position, c.attacker); //todo attacker wins - game.state = *initiation; + end_combat(game, c); return; } - if can_retreat { + if c.can_retreat { game.add_info_log_item(format!( "\t{} may retreat", - game.players[attacker].get_name() - )); - game.state = GameState::Combat(Combat::new( - initiation, - round, - CombatPhase::Retreat, - defender, - defender_position, - attacker, - attacker_position, - attackers, - true, + game.players[c.attacker].get_name() )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::Retreat, + ..c + }); return; } - round += 1; + c.round += 1; } CombatAction::Retreat(action) => { if action { //todo draw return; } - round += 1; + c.round += 1; } } - combat_loop( - game, - Some(initiation), - &mut round, - defender, - defender_position, - attacker, - attacker_position, - &mut attackers, - can_retreat, - ); + combat_loop(game, c); } -fn combat_loop( - game: &mut Game, - initiation: Option>, - round: &mut u32, - defender: usize, - defender_position: Position, - attacker: usize, - attacker_position: Position, - attackers: &mut Vec, - can_retreat: bool, -) { - let defender_fortress = game.players[defender] - .get_city(defender_position) +fn combat_loop(game: &mut Game, mut c: Combat) { + let defender_fortress = game.players[c.defender] + .get_city(c.defender_position) .is_some_and(|city| city.pieces.fortress.is_some()); loop { - game.add_info_log_item(format!("\nCombat round {round}")); + game.add_info_log_item(format!("\nCombat round {}", c.round)); //todo: go into tactics phase if either player has tactics card (also if they can not play it unless otherwise specified via setting) - let mut active_attackers = active_attackers(game, attacker, attackers, defender_position); - let attacker_rolls = roll(game, attacker, &active_attackers); - let active_defenders = active_defenders(game, defender, defender_position); - let defender_rolls = roll(game, defender, &active_defenders); + let mut active_attackers = + active_attackers(game, c.attacker, &c.attackers, c.defender_position); + let attacker_rolls = roll(game, c.attacker, &active_attackers); + let active_defenders = active_defenders(game, c.defender, c.defender_position); + let defender_rolls = roll(game, c.defender, &active_defenders); let attacker_combat_value = attacker_rolls.combat_value; let attacker_hit_cancels = attacker_rolls.hit_cancels; let defender_combat_value = defender_rolls.combat_value; let mut defender_hit_cancels = defender_rolls.hit_cancels; - if defender_fortress && *round == 1 { + if defender_fortress && c.round == 1 { defender_hit_cancels += 1; } let attacker_hits = (attacker_combat_value / 5).saturating_sub(defender_hit_cancels); let defender_hits = (defender_combat_value / 5).saturating_sub(attacker_hit_cancels); - game.add_info_log_item(format!("\t{} rolled a combined combat value of {attacker_combat_value} and gets {attacker_hits} hits against defending units. {} rolled a combined combat value of {defender_combat_value} and gets {defender_hits} hits against attacking units.", game.players[attacker].get_name(), game.players[defender].get_name())); + game.add_info_log_item(format!("\t{} rolled a combined combat value of {attacker_combat_value} and gets {attacker_hits} hits against defending units. {} rolled a combined combat value of {defender_combat_value} and gets {defender_hits} hits against attacking units.", game.players[c.attacker].get_name(), game.players[c.defender].get_name())); if attacker_hits < active_defenders.len() as u8 && attacker_hits > 0 { // attacker kills some defending units, but not all game.add_info_log_item(format!( "\t{} has to remove {} of his defending units", - game.players[defender].get_name(), + game.players[c.defender].get_name(), attacker_hits )); - game.state = GameState::Combat(Combat::new( - get_initiation(game, initiation), - *round, - CombatPhase::RemoveCasualties { - player: defender, + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.defender, casualties: attacker_hits, defender_hits: Some(defender_hits), }, - defender, - defender_position, - attacker, - attacker_position, - attackers.clone(), - can_retreat, - )); + ..c + }); return; } if attacker_hits >= active_defenders.len() as u8 { // attacker kills all defending units - let defender_units = game.players[defender] - .get_units(defender_position) + let defender_units = game.players[c.defender] + .get_units(c.defender_position) .iter() .map(|unit| unit.id) .collect::>(); for id in defender_units { - game.kill_unit(id, defender, attacker); + game.kill_unit(id, c.defender, c.attacker); } } if defender_hits < active_attackers.len() as u8 && defender_hits > 0 { // defender kills some attacking units, but not all game.add_info_log_item(format!( "\t{} has to remove {} of his attacking units", - game.players[attacker].get_name(), + game.players[c.attacker].get_name(), defender_hits )); - game.state = GameState::Combat(Combat::new( - get_initiation(game, initiation), - *round, - CombatPhase::RemoveCasualties { - player: attacker, + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.attacker, casualties: defender_hits, defender_hits: None, }, - defender, - defender_position, - attacker, - attacker_position, - attackers.clone(), - can_retreat, - )); + ..c + }); return; } if defender_hits >= active_attackers.len() as u8 { // defender kills all attacking unuts for id in active_attackers { - game.kill_unit(id, attacker, defender); + game.kill_unit(id, c.attacker, c.defender); } active_attackers = vec![]; } - let defenders_left = game.players[defender].get_units(defender_position); + let defenders_left = game.players[c.defender].get_units(c.defender_position); if active_attackers.is_empty() && defenders_left.is_empty() { - if defender_fortress && *round == 1 { - game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[defender].get_name())); + if defender_fortress && c.round == 1 { + game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); //todo defender wins - end_combat(game, initiation); + end_combat(game, c); return; } game.add_info_log_item(String::from( "\tAll attacking and defending units where eliminated, ending the battle in a draw", )); //todo draw - end_combat(game, initiation); + end_combat(game, c); return; } if active_attackers.is_empty() { game.add_info_log_item(format!( "\t{} killed all attacking units", - game.players[defender].get_name() + game.players[c.defender].get_name() )); //todo defender wins - end_combat(game, initiation); + end_combat(game, c); return; } if defenders_left.is_empty() { game.add_info_log_item(format!( "\t{} killed all defending units", - game.players[attacker].get_name() + game.players[c.attacker].get_name() )); - for unit in &*attackers { - let unit = game.players[attacker] + for unit in &c.attackers { + let unit = game.players[c.attacker] .get_unit_mut(*unit) .expect("attacker should have all attacking units"); - unit.position = defender_position; + unit.position = c.defender_position; } - end_combat(game, initiation); - capture_position(game, defender, defender_position, attacker); + capture_position(game, c.defender, c.defender_position, c.attacker); + end_combat(game, c); //todo attacker wins return; } - if can_retreat { + if c.can_retreat { game.add_info_log_item(format!( "\t{} may retreat", - game.players[attacker].get_name() - )); - game.state = GameState::Combat(Combat::new( - get_initiation(game, initiation), - *round, - CombatPhase::Retreat, - defender, - defender_position, - attacker, - attacker_position, - attackers.clone(), - true, + game.players[c.attacker].get_name() )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::Retreat, + ..c + }); return; } - *round += 1; + c.round += 1; } } @@ -451,13 +379,8 @@ pub fn capture_position(game: &mut Game, old_player: usize, position: Position, game.conquer_city(position, new_player, old_player); } -fn end_combat(game: &mut Game, initiation: Option>) { - game.state = *get_initiation(game, initiation); -} - -#[allow(clippy::unnecessary_box_returns)] -fn get_initiation(game: &mut Game, initiation: Option>) -> Box { - initiation.unwrap_or_else(|| Box::new(mem::replace(&mut game.state, Playing))) +fn end_combat(game: &mut Game, c: Combat) { + game.state = *c.initiation; } pub struct CombatRolls { diff --git a/server/src/game.rs b/server/src/game.rs index 7a65f560..584f3a6d 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -364,19 +364,7 @@ impl Game { Combat(c) => { let action = action.combat().expect("action should be a combat action"); self.add_action_log_item(ActionLogItem::Combat(action.clone())); - execute_combat_action( - self, - action, - c.initiation, - c.round, - c.phase, - c.defender, - c.defender_position, - c.attacker, - c.attacker_position, - c.attackers, - c.can_retreat, - ); + execute_combat_action(self, action, c); } PlaceSettler { player_index, @@ -527,6 +515,7 @@ impl Game { } else { Playing }; + initiate_combat( self, defender, From c6d13e9da32c11b2831746c840d7a4209f9f7f49 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 18:37:35 +0200 Subject: [PATCH 4/8] extract resolve_combat --- server/src/combat.rs | 182 ++++++++---------- .../remove_casualties_attacker.outcome.json | 4 +- .../remove_casualties_defender.outcome.json | 4 +- 3 files changed, 85 insertions(+), 105 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index 63996136..f895e655 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -1,4 +1,5 @@ use crate::action::CombatAction; +use crate::game::GameState::Playing; use crate::game::{Game, GameState}; use crate::map::Terrain::Water; use crate::position::Position; @@ -69,6 +70,18 @@ impl Combat { can_retreat, } } + + #[must_use] + pub fn active_attackers(&self, game: &Game) -> Vec { + active_attackers(game, self.attacker, &self.attackers, self.defender_position) + } + + #[must_use] + pub fn defender_fortress(&self, game: &Game) -> bool { + game.players[self.defender] + .get_city(self.defender_position) + .is_some_and(|city| city.pieces.fortress.is_some()) + } } pub fn initiate_combat( @@ -82,10 +95,14 @@ pub fn initiate_combat( next_game_state: Option, ) { game.lock_undo(); + let initiation = next_game_state.map_or_else( + || Box::new(mem::replace(&mut game.state, Playing)), + Box::new, + ); combat_loop( game, - crate::combat::Combat::new( - next_game_state.map_or_else(|| Box::new(game.state.clone()), Box::new), + Combat::new( + initiation, 1, CombatPhase::Retreat, // is not used defender, @@ -98,7 +115,6 @@ pub fn initiate_combat( ); } -#[allow(clippy::needless_pass_by_value)] //phase is consumed but it is not registered by clippy /// /// # Panics @@ -171,53 +187,9 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba } } } - let defenders_left = game.players[c.defender] - .get_units(c.defender_position) - .len(); - if c.attackers.is_empty() && defenders_left == 0 { - //todo if the defender has a fortress he wins - game.add_info_log_item(String::from("\tAll attacking and defending units killed each other, ending the battle in a draw")); - //todo otherwise: draw - end_combat(game, c); - return; - } - if c.attackers.is_empty() { - game.add_info_log_item(format!( - "\t{} killed all attacking units and wins", - game.players[c.defender].get_name() - )); - //todo defender wins - end_combat(game, c); - return; - } - if defenders_left == 0 { - game.add_info_log_item(format!( - "\t{} killed all defending units and wins", - game.players[c.attacker].get_name() - )); - for unit in &c.attackers { - let unit = game.players[c.attacker] - .get_unit_mut(*unit) - .expect("attacker should have all attacking units"); - unit.position = c.defender_position; - } - capture_position(game, c.defender, c.defender_position, c.attacker); - //todo attacker wins - end_combat(game, c); - return; - } - if c.can_retreat { - game.add_info_log_item(format!( - "\t{} may retreat", - game.players[c.attacker].get_name() - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::Retreat, - ..c - }); + if resolve_combat(game, &mut c) { return; } - c.round += 1; } CombatAction::Retreat(action) => { if action { @@ -238,8 +210,7 @@ fn combat_loop(game: &mut Game, mut c: Combat) { game.add_info_log_item(format!("\nCombat round {}", c.round)); //todo: go into tactics phase if either player has tactics card (also if they can not play it unless otherwise specified via setting) - let mut active_attackers = - active_attackers(game, c.attacker, &c.attackers, c.defender_position); + let active_attackers = c.active_attackers(game); let attacker_rolls = roll(game, c.attacker, &active_attackers); let active_defenders = active_defenders(game, c.defender, c.defender_position); let defender_rolls = roll(game, c.defender, &active_defenders); @@ -300,64 +271,73 @@ fn combat_loop(game: &mut Game, mut c: Combat) { } if defender_hits >= active_attackers.len() as u8 { // defender kills all attacking unuts - for id in active_attackers { - game.kill_unit(id, c.attacker, c.defender); + for id in &c.attackers { + game.kill_unit(*id, c.attacker, c.defender); } - active_attackers = vec![]; + c.attackers = vec![]; } - let defenders_left = game.players[c.defender].get_units(c.defender_position); - if active_attackers.is_empty() && defenders_left.is_empty() { - if defender_fortress && c.round == 1 { - game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); - //todo defender wins - end_combat(game, c); - return; - } - game.add_info_log_item(String::from( - "\tAll attacking and defending units where eliminated, ending the battle in a draw", - )); - //todo draw - end_combat(game, c); + + if resolve_combat(game, &mut c) { return; } - if active_attackers.is_empty() { - game.add_info_log_item(format!( - "\t{} killed all attacking units", - game.players[c.defender].get_name() - )); + } +} + +fn resolve_combat(game: &mut Game, c: &mut Combat) -> bool { + let active_attackers = c.active_attackers(game); + let defenders_left = game.players[c.defender].get_units(c.defender_position); + if active_attackers.is_empty() && defenders_left.is_empty() { + if c.defender_fortress(game) && c.round == 1 { + game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); //todo defender wins end_combat(game, c); - return; - } - if defenders_left.is_empty() { - game.add_info_log_item(format!( - "\t{} killed all defending units", - game.players[c.attacker].get_name() - )); - for unit in &c.attackers { - let unit = game.players[c.attacker] - .get_unit_mut(*unit) - .expect("attacker should have all attacking units"); - unit.position = c.defender_position; - } - capture_position(game, c.defender, c.defender_position, c.attacker); - end_combat(game, c); - //todo attacker wins - return; + return true; } - if c.can_retreat { - game.add_info_log_item(format!( - "\t{} may retreat", - game.players[c.attacker].get_name() - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::Retreat, - ..c - }); - return; + game.add_info_log_item(String::from( + "\tAll attacking and defending units where eliminated, ending the battle in a draw", + )); + //todo draw + end_combat(game, c); + return true; + } + if active_attackers.is_empty() { + game.add_info_log_item(format!( + "\t{} killed all attacking units", + game.players[c.defender].get_name() + )); + //todo defender wins + end_combat(game, c); + return true; + } + if defenders_left.is_empty() { + game.add_info_log_item(format!( + "\t{} killed all defending units", + game.players[c.attacker].get_name() + )); + for unit in &c.attackers { + let unit = game.players[c.attacker] + .get_unit_mut(*unit) + .expect("attacker should have all attacking units"); + unit.position = c.defender_position; } - c.round += 1; + capture_position(game, c.defender, c.defender_position, c.attacker); + end_combat(game, c); + //todo attacker wins + return true; + } + if c.can_retreat { + game.add_info_log_item(format!( + "\t{} may retreat", + game.players[c.attacker].get_name() + )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::Retreat, + ..c.clone() + }); + return true; } + c.round += 1; + false } pub fn capture_position(game: &mut Game, old_player: usize, position: Position, new_player: usize) { @@ -379,8 +359,8 @@ pub fn capture_position(game: &mut Game, old_player: usize, position: Position, game.conquer_city(position, new_player, old_player); } -fn end_combat(game: &mut Game, c: Combat) { - game.state = *c.initiation; +fn end_combat(game: &mut Game, c: &Combat) { + game.state = *c.initiation.clone(); } pub struct CombatRolls { diff --git a/server/tests/test_games/remove_casualties_attacker.outcome.json b/server/tests/test_games/remove_casualties_attacker.outcome.json index 7ce4842d..ff419a5d 100644 --- a/server/tests/test_games/remove_casualties_attacker.outcome.json +++ b/server/tests/test_games/remove_casualties_attacker.outcome.json @@ -421,7 +421,7 @@ "\tPlayer1 rolled a combined combat value of 24 and gets 4 hits against defending units. Player2 rolled a combined combat value of 12 and gets 2 hits against attacking units.", "\tPlayer1 has to remove 2 of his attacking units", "Player1 removed 1 infantry and 1 cavalry", - "\tPlayer1 killed all defending units and wins and captured Player2's city at C1" + "\tPlayer1 killed all defending units and captured Player2's city at C1" ], "undo_limit": 4, "played_once_per_turn_actions": [], @@ -513,4 +513,4 @@ } } ] -} +} \ No newline at end of file diff --git a/server/tests/test_games/remove_casualties_defender.outcome.json b/server/tests/test_games/remove_casualties_defender.outcome.json index 37b75d86..af24d084 100644 --- a/server/tests/test_games/remove_casualties_defender.outcome.json +++ b/server/tests/test_games/remove_casualties_defender.outcome.json @@ -430,7 +430,7 @@ "\tPlayer1 rolled a combined combat value of 6 and gets 1 hits against defending units. Player2 rolled a combined combat value of 12 and gets 2 hits against attacking units.", "\tPlayer2 has to remove 1 of his defending units", "Player2 removed 1 infantry", - "\tPlayer2 killed all attacking units and wins" + "\tPlayer2 killed all attacking units" ], "undo_limit": 4, "played_once_per_turn_actions": [], @@ -518,4 +518,4 @@ } } ] -} +} \ No newline at end of file From d17522964c9b8f9252af8308bf9cd5ad5615e292 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 18:41:13 +0200 Subject: [PATCH 5/8] extract resolve_combat --- server/src/combat.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index f895e655..a36b1398 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -203,9 +203,6 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba } fn combat_loop(game: &mut Game, mut c: Combat) { - let defender_fortress = game.players[c.defender] - .get_city(c.defender_position) - .is_some_and(|city| city.pieces.fortress.is_some()); loop { game.add_info_log_item(format!("\nCombat round {}", c.round)); //todo: go into tactics phase if either player has tactics card (also if they can not play it unless otherwise specified via setting) @@ -218,7 +215,7 @@ fn combat_loop(game: &mut Game, mut c: Combat) { let attacker_hit_cancels = attacker_rolls.hit_cancels; let defender_combat_value = defender_rolls.combat_value; let mut defender_hit_cancels = defender_rolls.hit_cancels; - if defender_fortress && c.round == 1 { + if c.defender_fortress(game) && c.round == 1 { defender_hit_cancels += 1; } let attacker_hits = (attacker_combat_value / 5).saturating_sub(defender_hit_cancels); From fa23ed440b57c3957a741a3292cedc94e56788c5 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 3 Oct 2023 18:45:02 +0200 Subject: [PATCH 6/8] extract remove_casualties --- server/src/combat.rs | 118 ++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index a36b1398..c09a46be 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -131,63 +131,7 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba return; } CombatAction::RemoveCasualties(units) => { - let CombatPhase::RemoveCasualties { - player, - casualties, - defender_hits, - } = c.phase - else { - panic!("Illegal action"); - }; - assert_eq!(casualties, units.len() as u8, "Illegal action"); - let (fighting_units, opponent) = if player == c.defender { - ( - game.players[player] - .get_units(c.defender_position) - .iter() - .map(|unit| unit.id) - .collect(), - c.attacker, - ) - } else if player == c.attacker { - (c.attackers.clone(), c.defender) - } else { - panic!("Illegal action") - }; - assert!( - units.iter().all(|unit| fighting_units.contains(unit)), - "Illegal action" - ); - for unit in units { - game.kill_unit(unit, player, opponent); - if player == c.attacker { - c.attackers.retain(|id| *id != unit); - } - } - if let Some(defender_hits) = defender_hits { - if defender_hits < c.attackers.len() as u8 && defender_hits > 0 { - game.add_info_log_item(format!( - "\t{} has to remove {} of his attacking units", - game.players[c.attacker].get_name(), - defender_hits - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::RemoveCasualties { - player: c.defender, - casualties: defender_hits, - defender_hits: None, - }, - ..c - }); - return; - } - if defender_hits >= c.attackers.len() as u8 { - for id in mem::take(&mut c.attackers) { - game.kill_unit(id, c.attacker, c.defender); - } - } - } - if resolve_combat(game, &mut c) { + if remove_casualties(game, &mut c, units) { return; } } @@ -202,6 +146,66 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba combat_loop(game, c); } +fn remove_casualties(game: &mut Game, c: &mut Combat, units: Vec) -> bool { + let CombatPhase::RemoveCasualties { + player, + casualties, + defender_hits, + } = c.phase + else { + panic!("Illegal action"); + }; + assert_eq!(casualties, units.len() as u8, "Illegal action"); + let (fighting_units, opponent) = if player == c.defender { + ( + game.players[player] + .get_units(c.defender_position) + .iter() + .map(|unit| unit.id) + .collect(), + c.attacker, + ) + } else if player == c.attacker { + (c.attackers.clone(), c.defender) + } else { + panic!("Illegal action") + }; + assert!( + units.iter().all(|unit| fighting_units.contains(unit)), + "Illegal action" + ); + for unit in units { + game.kill_unit(unit, player, opponent); + if player == c.attacker { + c.attackers.retain(|id| *id != unit); + } + } + if let Some(defender_hits) = defender_hits { + if defender_hits < c.attackers.len() as u8 && defender_hits > 0 { + game.add_info_log_item(format!( + "\t{} has to remove {} of his attacking units", + game.players[c.attacker].get_name(), + defender_hits + )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.defender, + casualties: defender_hits, + defender_hits: None, + }, + ..c.clone() + }); + return true; + } + if defender_hits >= c.attackers.len() as u8 { + for id in mem::take(&mut c.attackers) { + game.kill_unit(id, c.attacker, c.defender); + } + } + } + resolve_combat(game, c) +} + fn combat_loop(game: &mut Game, mut c: Combat) { loop { game.add_info_log_item(format!("\nCombat round {}", c.round)); From e816a1b1f0c9517b01f9d2df3ac466d3007b81f1 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 4 Oct 2023 06:53:57 +0200 Subject: [PATCH 7/8] extract combat methods --- server/src/combat.rs | 205 ++++++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 91 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index c09a46be..a75712bd 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -226,56 +226,18 @@ fn combat_loop(game: &mut Game, mut c: Combat) { let defender_hits = (defender_combat_value / 5).saturating_sub(attacker_hit_cancels); game.add_info_log_item(format!("\t{} rolled a combined combat value of {attacker_combat_value} and gets {attacker_hits} hits against defending units. {} rolled a combined combat value of {defender_combat_value} and gets {defender_hits} hits against attacking units.", game.players[c.attacker].get_name(), game.players[c.defender].get_name())); if attacker_hits < active_defenders.len() as u8 && attacker_hits > 0 { - // attacker kills some defending units, but not all - game.add_info_log_item(format!( - "\t{} has to remove {} of his defending units", - game.players[c.defender].get_name(), - attacker_hits - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::RemoveCasualties { - player: c.defender, - casualties: attacker_hits, - defender_hits: Some(defender_hits), - }, - ..c - }); + kill_some_defenders(game, c, attacker_hits, defender_hits); return; } if attacker_hits >= active_defenders.len() as u8 { - // attacker kills all defending units - let defender_units = game.players[c.defender] - .get_units(c.defender_position) - .iter() - .map(|unit| unit.id) - .collect::>(); - for id in defender_units { - game.kill_unit(id, c.defender, c.attacker); - } + kill_all_defenders(game, &mut c); } if defender_hits < active_attackers.len() as u8 && defender_hits > 0 { - // defender kills some attacking units, but not all - game.add_info_log_item(format!( - "\t{} has to remove {} of his attacking units", - game.players[c.attacker].get_name(), - defender_hits - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::RemoveCasualties { - player: c.attacker, - casualties: defender_hits, - defender_hits: None, - }, - ..c - }); + kill_some_attackers(game, c, defender_hits); return; } if defender_hits >= active_attackers.len() as u8 { - // defender kills all attacking unuts - for id in &c.attackers { - game.kill_unit(*id, c.attacker, c.defender); - } - c.attackers = vec![]; + kill_all_attackers(game, &mut c); } if resolve_combat(game, &mut c) { @@ -284,61 +246,121 @@ fn combat_loop(game: &mut Game, mut c: Combat) { } } +fn kill_all_attackers(game: &mut Game, c: &mut Combat) { + for id in &c.attackers { + game.kill_unit(*id, c.attacker, c.defender); + } + c.attackers = vec![]; +} + +fn kill_some_attackers(game: &mut Game, c: Combat, defender_hits: u8) { + game.add_info_log_item(format!( + "\t{} has to remove {} of his attacking units", + game.players[c.attacker].get_name(), + defender_hits + )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.attacker, + casualties: defender_hits, + defender_hits: None, + }, + ..c + }); +} + +fn kill_all_defenders(game: &mut Game, c: &mut Combat) { + let defender_units = game.players[c.defender] + .get_units(c.defender_position) + .iter() + .map(|unit| unit.id) + .collect::>(); + for id in defender_units { + game.kill_unit(id, c.defender, c.attacker); + } +} + +fn kill_some_defenders(game: &mut Game, c: Combat, attacker_hits: u8, defender_hits: u8) { + game.add_info_log_item(format!( + "\t{} has to remove {} of his defending units", + game.players[c.defender].get_name(), + attacker_hits + )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::RemoveCasualties { + player: c.defender, + casualties: attacker_hits, + defender_hits: Some(defender_hits), + }, + ..c + }); +} + fn resolve_combat(game: &mut Game, c: &mut Combat) -> bool { let active_attackers = c.active_attackers(game); let defenders_left = game.players[c.defender].get_units(c.defender_position); if active_attackers.is_empty() && defenders_left.is_empty() { - if c.defender_fortress(game) && c.round == 1 { - game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); - //todo defender wins - end_combat(game, c); - return true; - } - game.add_info_log_item(String::from( - "\tAll attacking and defending units where eliminated, ending the battle in a draw", - )); - //todo draw - end_combat(game, c); - return true; - } - if active_attackers.is_empty() { - game.add_info_log_item(format!( - "\t{} killed all attacking units", - game.players[c.defender].get_name() - )); - //todo defender wins - end_combat(game, c); - return true; + draw(game, c) + } else if active_attackers.is_empty() { + defender_wins(game, c) + } else if defenders_left.is_empty() { + attacker_wins(game, c) + } else if c.can_retreat { + offer_retreat(game, c) + } else { + c.round += 1; + false } - if defenders_left.is_empty() { - game.add_info_log_item(format!( - "\t{} killed all defending units", - game.players[c.attacker].get_name() - )); - for unit in &c.attackers { - let unit = game.players[c.attacker] - .get_unit_mut(*unit) - .expect("attacker should have all attacking units"); - unit.position = c.defender_position; - } - capture_position(game, c.defender, c.defender_position, c.attacker); - end_combat(game, c); - //todo attacker wins - return true; +} + +fn offer_retreat(game: &mut Game, c: &mut Combat) -> bool { + game.add_info_log_item(format!( + "\t{} may retreat", + game.players[c.attacker].get_name() + )); + game.state = GameState::Combat(Combat { + phase: CombatPhase::Retreat, + ..c.clone() + }); + true +} + +fn attacker_wins(game: &mut Game, c: &mut Combat) -> bool { + game.add_info_log_item(format!( + "\t{} killed all defending units", + game.players[c.attacker].get_name() + )); + for unit in &c.attackers { + let unit = game.players[c.attacker] + .get_unit_mut(*unit) + .expect("attacker should have all attacking units"); + unit.position = c.defender_position; } - if c.can_retreat { - game.add_info_log_item(format!( - "\t{} may retreat", - game.players[c.attacker].get_name() - )); - game.state = GameState::Combat(Combat { - phase: CombatPhase::Retreat, - ..c.clone() - }); - return true; + capture_position(game, c.defender, c.defender_position, c.attacker); + //todo attacker wins + end_combat(game, c) +} + +fn defender_wins(game: &mut Game, c: &mut Combat) -> bool { + game.add_info_log_item(format!( + "\t{} killed all attacking units", + game.players[c.defender].get_name() + )); + //todo defender wins + end_combat(game, c) +} + +fn draw(game: &mut Game, c: &mut Combat) -> bool { + if c.defender_fortress(game) && c.round == 1 { + game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); + //todo defender wins + return end_combat(game, c); } - c.round += 1; - false + game.add_info_log_item(String::from( + "\tAll attacking and defending units where eliminated, ending the battle in a draw", + )); + //todo draw + end_combat(game, c) } pub fn capture_position(game: &mut Game, old_player: usize, position: Position, new_player: usize) { @@ -360,8 +382,9 @@ pub fn capture_position(game: &mut Game, old_player: usize, position: Position, game.conquer_city(position, new_player, old_player); } -fn end_combat(game: &mut Game, c: &Combat) { +fn end_combat(game: &mut Game, c: &Combat) -> bool { game.state = *c.initiation.clone(); + true } pub struct CombatRolls { From 962031c9d650559320713d49f704dff975201022 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Wed, 4 Oct 2023 07:02:01 +0200 Subject: [PATCH 8/8] add CombatControl --- server/src/combat.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/server/src/combat.rs b/server/src/combat.rs index a75712bd..89c33262 100644 --- a/server/src/combat.rs +++ b/server/src/combat.rs @@ -84,6 +84,11 @@ impl Combat { } } +enum CombatControl { + Exit, // exit to player, doesn't mean the combat has ended + Continue, // continue to combat loop +} + pub fn initiate_combat( game: &mut Game, defender: usize, @@ -131,7 +136,7 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba return; } CombatAction::RemoveCasualties(units) => { - if remove_casualties(game, &mut c, units) { + if matches!(remove_casualties(game, &mut c, units), CombatControl::Exit) { return; } } @@ -146,7 +151,7 @@ pub fn execute_combat_action(game: &mut Game, action: CombatAction, mut c: Comba combat_loop(game, c); } -fn remove_casualties(game: &mut Game, c: &mut Combat, units: Vec) -> bool { +fn remove_casualties(game: &mut Game, c: &mut Combat, units: Vec) -> CombatControl { let CombatPhase::RemoveCasualties { player, casualties, @@ -195,7 +200,7 @@ fn remove_casualties(game: &mut Game, c: &mut Combat, units: Vec) -> bool { }, ..c.clone() }); - return true; + return CombatControl::Exit; } if defender_hits >= c.attackers.len() as u8 { for id in mem::take(&mut c.attackers) { @@ -240,7 +245,7 @@ fn combat_loop(game: &mut Game, mut c: Combat) { kill_all_attackers(game, &mut c); } - if resolve_combat(game, &mut c) { + if matches!(resolve_combat(game, &mut c), CombatControl::Exit) { return; } } @@ -296,7 +301,7 @@ fn kill_some_defenders(game: &mut Game, c: Combat, attacker_hits: u8, defender_h }); } -fn resolve_combat(game: &mut Game, c: &mut Combat) -> bool { +fn resolve_combat(game: &mut Game, c: &mut Combat) -> CombatControl { let active_attackers = c.active_attackers(game); let defenders_left = game.players[c.defender].get_units(c.defender_position); if active_attackers.is_empty() && defenders_left.is_empty() { @@ -309,11 +314,11 @@ fn resolve_combat(game: &mut Game, c: &mut Combat) -> bool { offer_retreat(game, c) } else { c.round += 1; - false + CombatControl::Continue } } -fn offer_retreat(game: &mut Game, c: &mut Combat) -> bool { +fn offer_retreat(game: &mut Game, c: &mut Combat) -> CombatControl { game.add_info_log_item(format!( "\t{} may retreat", game.players[c.attacker].get_name() @@ -322,10 +327,10 @@ fn offer_retreat(game: &mut Game, c: &mut Combat) -> bool { phase: CombatPhase::Retreat, ..c.clone() }); - true + CombatControl::Exit } -fn attacker_wins(game: &mut Game, c: &mut Combat) -> bool { +fn attacker_wins(game: &mut Game, c: &mut Combat) -> CombatControl { game.add_info_log_item(format!( "\t{} killed all defending units", game.players[c.attacker].get_name() @@ -341,7 +346,7 @@ fn attacker_wins(game: &mut Game, c: &mut Combat) -> bool { end_combat(game, c) } -fn defender_wins(game: &mut Game, c: &mut Combat) -> bool { +fn defender_wins(game: &mut Game, c: &mut Combat) -> CombatControl { game.add_info_log_item(format!( "\t{} killed all attacking units", game.players[c.defender].get_name() @@ -350,7 +355,7 @@ fn defender_wins(game: &mut Game, c: &mut Combat) -> bool { end_combat(game, c) } -fn draw(game: &mut Game, c: &mut Combat) -> bool { +fn draw(game: &mut Game, c: &mut Combat) -> CombatControl { if c.defender_fortress(game) && c.round == 1 { game.add_info_log_item(format!("\tAll attacking and defending units where eliminated. {} wins the battle because he has a defending fortress", game.players[c.defender].get_name())); //todo defender wins @@ -382,9 +387,9 @@ pub fn capture_position(game: &mut Game, old_player: usize, position: Position, game.conquer_city(position, new_player, old_player); } -fn end_combat(game: &mut Game, c: &Combat) -> bool { +fn end_combat(game: &mut Game, c: &Combat) -> CombatControl { game.state = *c.initiation.clone(); - true + CombatControl::Exit } pub struct CombatRolls {