diff --git a/client/src/advance_ui.rs b/client/src/advance_ui.rs index 6b654737..b801767f 100644 --- a/client/src/advance_ui.rs +++ b/client/src/advance_ui.rs @@ -15,7 +15,7 @@ use server::resource_pile::AdvancePaymentOptions; use server::status_phase::{StatusPhaseAction, StatusPhaseState}; use crate::client_state::{ActiveDialog, ShownPlayer, StateUpdate}; -use crate::dialog_ui::full_dialog; +use crate::dialog_ui::dialog; use crate::payment_ui::{payment_dialog, HasPayment, Payment, ResourcePayment}; use crate::resource_ui::{new_resource_map, ResourceType}; use crate::select_ui::HasCountSelectableObject; @@ -101,7 +101,7 @@ pub fn show_generic_advance_menu( player: &ShownPlayer, new_update: impl Fn(&str) -> StateUpdate, ) -> StateUpdate { - full_dialog(title, |ui| { + dialog(player, title, |ui| { let p = player.get(game); let mut update = StateUpdate::None; let mut current_group = None; diff --git a/client/src/client.rs b/client/src/client.rs index eca7dd8c..b6d4fc7c 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -1,6 +1,5 @@ use macroquad::input::{is_mouse_button_pressed, mouse_position, MouseButton}; -use macroquad::prelude::{clear_background, vec2, WHITE}; -use macroquad::ui::root_ui; +use macroquad::prelude::*; use server::action::Action; use server::game::Game; @@ -12,13 +11,11 @@ use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate, StateUp use crate::collect_ui::{click_collect_option, collect_resources_dialog}; use crate::construct_ui::pay_construction_dialog; use crate::dialog_ui::active_dialog_window; -use crate::happiness_ui::{ - add_increase_happiness, increase_happiness_menu, show_increase_happiness, -}; +use crate::happiness_ui::{add_increase_happiness, increase_happiness_menu}; 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_player_status, show_wonders}; +use crate::player_ui::{show_global_controls, show_globals}; use crate::{combat_ui, dialog_ui, influence_ui, move_ui, recruit_unit_ui, status_phase_ui}; pub async fn init(features: &Features) -> State { @@ -45,40 +42,24 @@ pub fn render_and_update( state.update(game, update) } -fn render(game: &Game, state: &State, features: &Features) -> StateUpdate { - let player_index = game.active_player(); +fn render(game: &Game, state: &mut State, features: &Features) -> StateUpdate { let player = &state.shown_player(game); clear_background(WHITE); - draw_map(game, state); + state.camera = Camera2D { + zoom: vec2(state.zoom, state.zoom * screen_width() / screen_height()), + offset: state.offset, + ..Default::default() + }; + set_camera(&state.camera); + + if matches!(state.active_dialog, ActiveDialog::None) || state.active_dialog.is_map_dialog() { + draw_map(game, state); + } let mut updates = StateUpdates::new(); let update = show_globals(game, player); updates.add(update); - show_player_status(game, player_index); - show_wonders(game, player_index); - - if root_ui().button(vec2(1200., 100.), "Advances") { - return StateUpdate::OpenDialog(ActiveDialog::AdvanceMenu); - }; - if root_ui().button(vec2(1200., 130.), "Log") { - return StateUpdate::OpenDialog(ActiveDialog::Log); - }; - let d = state.game_state_dialog(game, &ActiveDialog::None); - if !matches!(d, ActiveDialog::None) - && d.title() != state.active_dialog.title() - && root_ui().button(vec2(1200., 160.), format!("Back to {}", d.title())) - { - return StateUpdate::OpenDialog(d); - } - if features.import_export && player.can_control { - if root_ui().button(vec2(1200., 290.), "Import") { - return StateUpdate::Import; - }; - if root_ui().button(vec2(1250., 290.), "Export") { - return StateUpdate::Export; - }; - } if player.can_control { if let Some(u) = &state.pending_update { updates.add(dialog_ui::show_pending_update(u, player)); @@ -86,14 +67,11 @@ fn render(game: &Game, state: &State, features: &Features) -> StateUpdate { } } - if player.can_play_action { - updates.add(show_increase_happiness(game, player_index)); - } - updates.add(show_global_controls(game, state)); + updates.add(show_global_controls(game, state, features)); updates.add(match &state.active_dialog { ActiveDialog::None => StateUpdate::None, - ActiveDialog::Log => show_log(game), + ActiveDialog::Log => show_log(game, player), ActiveDialog::TileMenu(p) => show_tile_menu(game, *p, player), ActiveDialog::WaitingForUpdate => { active_dialog_window(player, "Waiting for update", |_ui| StateUpdate::None) @@ -143,8 +121,9 @@ pub fn try_click(game: &Game, state: &State, player: &ShownPlayer) -> StateUpdat return StateUpdate::None; } let (x, y) = mouse_position(); - - let pos = Position::from_coordinate(pixel_to_coordinate(x, y)); + let pos = Position::from_coordinate(pixel_to_coordinate( + state.camera.screen_to_world(vec2(x, y)), + )); if !game.map.tiles.contains_key(&pos) { return StateUpdate::None; } diff --git a/client/src/client_state.rs b/client/src/client_state.rs index 79761e9c..45d929d5 100644 --- a/client/src/client_state.rs +++ b/client/src/client_state.rs @@ -80,6 +80,19 @@ impl ActiveDialog { ActiveDialog::RemoveCasualties(_) => "remove casualties", } } + + #[must_use] + pub fn is_map_dialog(&self) -> bool { + matches!( + self, + ActiveDialog::TileMenu(_) + | ActiveDialog::IncreaseHappiness(_) + | ActiveDialog::CollectResources(_) + | ActiveDialog::MoveUnits(_) + | ActiveDialog::PlaceSettler + | ActiveDialog::RazeSize1City + ) + } } pub struct PendingUpdate { @@ -181,6 +194,7 @@ pub struct ShownPlayer { pub index: usize, pub can_control: bool, pub can_play_action: bool, + pub active_dialog: ActiveDialog, } impl ShownPlayer { @@ -195,19 +209,28 @@ pub struct State { pub control_player: Option, pub show_player: usize, pub active_dialog: ActiveDialog, - dialog_stack: Vec, pub pending_update: Option, + pub camera: Camera2D, + pub zoom: f32, + pub offset: Vec2, } +pub const ZOOM: f32 = 0.001; +pub const OFFSET: Vec2 = vec2(-0.8, 0.45); + impl State { pub async fn new(features: &Features) -> State { State { active_dialog: ActiveDialog::None, - dialog_stack: vec![], pending_update: None, assets: Assets::new(features).await, control_player: None, show_player: 0, + camera: Camera2D { + ..Default::default() + }, + zoom: ZOOM, + offset: OFFSET, } } @@ -219,12 +242,12 @@ impl State { index: self.show_player, can_control: control, can_play_action: control && game.state == GameState::Playing && game.actions_left > 0, + active_dialog: self.active_dialog.clone(), } } pub fn clear(&mut self) { self.active_dialog = ActiveDialog::None; - self.dialog_stack.clear(); self.pending_update = None; } @@ -280,26 +303,22 @@ impl State { } fn open_dialog(&mut self, dialog: ActiveDialog) { - if matches!(self.active_dialog, ActiveDialog::TileMenu(_)) { + if self.active_dialog.title() == dialog.title() { self.close_dialog(); + return; } - if !matches!(self.active_dialog, ActiveDialog::None) { - self.dialog_stack.push(self.active_dialog.clone()); + if matches!(self.active_dialog, ActiveDialog::TileMenu(_)) { + self.close_dialog(); } self.active_dialog = dialog; } pub fn set_dialog(&mut self, dialog: ActiveDialog) { self.active_dialog = dialog; - self.dialog_stack.clear(); } fn close_dialog(&mut self) { - if let Some(dialog) = self.dialog_stack.pop() { - self.active_dialog = dialog; - } else { - self.active_dialog = ActiveDialog::None; - } + self.active_dialog = ActiveDialog::None; } pub fn update_from_game(&mut self, game: &Game) -> GameSyncRequest { diff --git a/client/src/dialog_ui.rs b/client/src/dialog_ui.rs index 30b79b2a..4c6b8f3e 100644 --- a/client/src/dialog_ui.rs +++ b/client/src/dialog_ui.rs @@ -1,15 +1,16 @@ +use crate::client_state::{PendingUpdate, ShownPlayer, StateUpdate}; use macroquad::hash; use macroquad::math::{vec2, Vec2}; +use macroquad::prelude::screen_height; use macroquad::ui::widgets::Window; use macroquad::ui::{root_ui, Ui}; - -use crate::client_state::{PendingUpdate, ShownPlayer, StateUpdate}; +use macroquad::window::screen_width; pub fn active_dialog_window(player: &ShownPlayer, title: &str, f: F) -> StateUpdate where F: FnOnce(&mut Ui) -> StateUpdate, { - dialog(title, |ui| { + dialog(player, title, |ui| { if player.can_control { f(ui) } else { @@ -18,18 +19,17 @@ where }) } -pub fn dialog(title: &str, f: F) -> StateUpdate +pub fn dialog(player: &ShownPlayer, title: &str, f: F) -> StateUpdate where F: FnOnce(&mut Ui) -> StateUpdate, { - custom_dialog(title, vec2(1100., 400.), vec2(800., 350.), f) -} - -pub fn full_dialog(title: &str, f: F) -> StateUpdate -where - F: FnOnce(&mut Ui) -> StateUpdate, -{ - custom_dialog(title, vec2(100., 100.), vec2(1600., 800.), f) + let width = screen_width() - 20.; + let size = if player.active_dialog.is_map_dialog() { + vec2(width / 2.0, 100.) + } else { + vec2(width, screen_height() - 100.) + }; + custom_dialog(title, vec2(10., 70.), size, f) } pub fn custom_dialog(title: &str, position: Vec2, size: Vec2, f: F) -> StateUpdate @@ -42,10 +42,7 @@ where .label(title) .close_button(true); - let ui = &mut root_ui(); - let token = window.begin(ui); - let update = f(ui); - let open = token.end(ui); + let (update, open) = show_window(window, f); if matches!(update, StateUpdate::None) { if open { StateUpdate::None @@ -57,6 +54,17 @@ where } } +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) +} + pub fn show_pending_update(update: &PendingUpdate, player: &ShownPlayer) -> StateUpdate { active_dialog_window(player, "Are you sure?", |ui| { ui.label(None, &format!("Warning: {}", update.warning.join(", "))); diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index 483f2542..456e7030 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -1,6 +1,3 @@ -use macroquad::math::vec2; -use macroquad::ui::root_ui; - use server::action::Action; use server::city::City; use server::game::Game; @@ -103,17 +100,13 @@ pub fn increase_happiness_menu(h: &IncreaseHappiness, player: &ShownPlayer) -> S }) } -pub fn show_increase_happiness(game: &Game, player_index: usize) -> StateUpdate { - if root_ui().button(vec2(1200., 60.), "Increase Happiness") { - return StateUpdate::SetDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( - game.get_player(player_index) - .cities - .iter() - .map(|c| (c.position, 0)) - .collect(), - ResourcePile::empty(), - ))); - } - - StateUpdate::None +pub fn start_increase_happiness(game: &Game, player: &ShownPlayer) -> StateUpdate { + StateUpdate::OpenDialog(ActiveDialog::IncreaseHappiness(IncreaseHappiness::new( + game.get_player(player.index) + .cities + .iter() + .map(|c| (c.position, 0)) + .collect(), + ResourcePile::empty(), + ))) } diff --git a/client/src/hex_ui.rs b/client/src/hex_ui.rs index 7ed8c3bf..f3b03894 100644 --- a/client/src/hex_ui.rs +++ b/client/src/hex_ui.rs @@ -2,7 +2,7 @@ use std::f32::consts::PI; use hex2d::{Coordinate, Spacing}; use macroquad::color::Color; -use macroquad::math::{f32, i32, vec2}; +use macroquad::math::{f32, i32, vec2, Vec2}; use macroquad::prelude::{ draw_text, draw_texture_ex, DrawTextureParams, Rect, Texture2D, BLACK, DARKGRAY, WHITE, }; @@ -49,8 +49,8 @@ pub fn draw_hex_center_text(p: Position, text: &str) { draw_text(text, c.x - 5., c.y + 6., 25.0, BLACK); } -pub fn pixel_to_coordinate(x: f32, y: f32) -> Coordinate { - let p = Point::new(x, y).to_game(); +pub fn pixel_to_coordinate(p: Vec2) -> Coordinate { + let p = Point::new(p.x, p.y).to_game(); Coordinate::from_pixel(p.x, p.y, SPACING) } @@ -89,5 +89,5 @@ impl Point { } } -const TOP_BORDER: f32 = 130.0; -const LEFT_BORDER: f32 = 90.0; +const TOP_BORDER: f32 = 0.0; +const LEFT_BORDER: f32 = 0.0; diff --git a/client/src/local_client/bin/main.rs b/client/src/local_client/bin/main.rs index d791185c..1e69b492 100644 --- a/client/src/local_client/bin/main.rs +++ b/client/src/local_client/bin/main.rs @@ -1,14 +1,13 @@ use client::client::Features; use client::local_client; -use macroquad::window::set_fullscreen; use server::game::Game; #[macroquad::main("Clash")] async fn main() { - set_fullscreen(true); + // set_fullscreen(true); let features = Features { - import_export: false, + import_export: true, assets_url: "assets/".to_string(), }; diff --git a/client/src/log_ui.rs b/client/src/log_ui.rs index 79fa5355..e1cb476d 100644 --- a/client/src/log_ui.rs +++ b/client/src/log_ui.rs @@ -1,11 +1,11 @@ use macroquad::ui::Ui; use server::game::Game; -use crate::client_state::StateUpdate; +use crate::client_state::{ShownPlayer, StateUpdate}; use crate::dialog_ui::dialog; -pub fn show_log(game: &Game) -> StateUpdate { - dialog("Log", |ui| { +pub fn show_log(game: &Game, player: &ShownPlayer) -> StateUpdate { + dialog(player, "Log", |ui| { game.log.iter().for_each(|l| { multiline(ui, l); }); diff --git a/client/src/map_ui.rs b/client/src/map_ui.rs index e4f2075d..409ad96a 100644 --- a/client/src/map_ui.rs +++ b/client/src/map_ui.rs @@ -132,6 +132,7 @@ pub fn show_generic_tile_menu( additional: impl FnOnce(&mut Ui) -> StateUpdate, ) -> StateUpdate { dialog( + player, &format!( "{}/{}", position, diff --git a/client/src/player_ui.rs b/client/src/player_ui.rs index 857b6f0e..2c6dfda1 100644 --- a/client/src/player_ui.rs +++ b/client/src/player_ui.rs @@ -1,20 +1,26 @@ -use macroquad::color::BLACK; +use crate::client::Features; +use crate::client_state::{ActiveDialog, ShownPlayer, State, StateUpdate, OFFSET, ZOOM}; +use crate::happiness_ui::start_increase_happiness; use macroquad::math::vec2; use macroquad::prelude::*; -use macroquad::text::draw_text; -use macroquad::ui::root_ui; -use macroquad::ui::widgets::Button; +use macroquad::ui::{root_ui, Ui}; use server::action::Action; use server::game::{Game, GameState}; use server::player::Player; use server::playing_actions::PlayingAction; use server::resource_pile::ResourcePile; -use crate::client_state::{ShownPlayer, State, StateUpdate}; +pub fn show_globals(game: &Game, player: &ShownPlayer) -> StateUpdate { + let y = 5.; -pub fn show_globals(game: &Game, shown_player: &ShownPlayer) -> StateUpdate { - draw_text(&format!("Age {}", game.age), 1400., 60., 20., BLACK); - draw_text(&format!("Round {}", game.round), 1400., 90., 20., BLACK); + let ui = &mut root_ui(); + ui.label(vec2(10., y), &format!("Age {}", game.age)); + ui.label(vec2(45., y), &format!("Round {}", game.round)); + + show_player_status(game, player, ui); + show_wonders(game, player, ui); + + let y = 50.; let i = game .players @@ -25,21 +31,17 @@ pub fn show_globals(game: &Game, shown_player: &ShownPlayer) -> StateUpdate { players.rotate_left(i); for (i, &p) in players.iter().enumerate() { - let player = game.get_player(p); - let shown = shown_player.index == p; + let p = game.get_player(p); + let shown = player.index == p.index; let prefix = if shown { "* " } else { "" }; - let suffix = &player_suffix(game, player); - let name = player.get_name(); - let y = 180. + i as f32 * 50.; - let x = 1400.; + let suffix = &player_suffix(game, p); + let name = p.get_name(); + let x = i as f32 * 500.; let label = format!("{prefix}{name}{suffix}"); if shown { - draw_text(&label, x, y, 20., BLACK); - } else if Button::new(label) - .position(vec2(x, y - 10.)) - .ui(&mut root_ui()) - { - return StateUpdate::SetShownPlayer(p); + ui.label(vec2(x, y), &label); + } else if ui.button(vec2(x, y), label) { + return StateUpdate::SetShownPlayer(p.index); } } StateUpdate::None @@ -107,43 +109,43 @@ fn moves_left(state: &GameState) -> Option { } } -pub fn show_wonders(game: &Game, player_index: usize) { - let player = game.get_player(player_index); +pub fn show_wonders(game: &Game, player: &ShownPlayer, ui: &mut Ui) { + let player = game.get_player(player.index); + let y = 5.; for (i, name) in player.wonders.iter().enumerate() { - draw_text( - &format!("Wonder {name}"), - 1100., - 800. + i as f32 * 30.0, - 20., - BLACK, - ); + ui.label(vec2(500. + i as f32 * 30.0, y), &format!("Wonder {name}")); } for (i, card) in player.wonder_cards.iter().enumerate() { let req = match card.required_advances[..] { [] => String::from("no advances"), _ => card.required_advances.join(", "), }; - draw_text( + ui.label( + vec2(900. + i as f32 * 30.0, y), &format!( "Wonder Card {} cost {} requires {}", &card.name, card.cost, req ), - 1100., - 900. + i as f32 * 30.0, - 20., - BLACK, ); } } -pub fn show_player_status(game: &Game, player_index: usize) { - let player = game.get_player(player_index); +pub fn show_player_status(game: &Game, player: &ShownPlayer, ui: &mut Ui) { + let player = game.get_player(player.index); let mut i: f32 = 0.; let mut res = |label: String| { - draw_text(&label, 1000., 30. + i, 20., BLACK); - i += 30.; + ui.label(vec2(110. + i, 5.), &label); + i += 70.; }; + res(resource_ui(player, "Food", |r| r.food)); + res(resource_ui(player, "Wood", |r| r.wood)); + res(resource_ui(player, "Ore", |r| r.ore)); + res(resource_ui(player, "Ideas", |r| r.ideas)); + res(resource_ui(player, "Gold", |r| r.gold as u32)); + res(resource_ui(player, "Mood", |r| r.mood_tokens)); + res(resource_ui(player, "Culture", |r| r.culture_tokens)); + res(format!("Civ {}", player.civilization.name)); res(format!("VP {}", player.victory_points())); res(format!( @@ -154,13 +156,6 @@ pub fn show_player_status(game: &Game, player_index: usize) { "-" } )); - res(resource_ui(player, "Food", |r| r.food)); - res(resource_ui(player, "Wood", |r| r.wood)); - res(resource_ui(player, "Ore", |r| r.ore)); - res(resource_ui(player, "Ideas", |r| r.ideas)); - res(resource_ui(player, "Gold", |r| r.gold as u32)); - res(resource_ui(player, "Mood", |r| r.mood_tokens)); - res(resource_ui(player, "Culture", |r| r.culture_tokens)); } fn resource_ui(player: &Player, name: &str, f: impl Fn(&ResourcePile) -> u32) -> String { @@ -169,17 +164,50 @@ fn resource_ui(player: &Player, name: &str, f: impl Fn(&ResourcePile) -> u32) -> format!("{name} {}/{}", f(r), f(l)) } -pub fn show_global_controls(game: &Game, state: &State) -> StateUpdate { +pub fn show_global_controls(game: &Game, state: &mut State, features: &Features) -> StateUpdate { let player = state.shown_player(game); - if game.can_undo() && root_ui().button(vec2(1200., 320.), "Undo") { + let y = 30.; + + let ui = &mut root_ui(); + if ui.button(vec2(10., y), "+") { + state.zoom *= 1.1; + return StateUpdate::None; + } + if ui.button(vec2(25., y), "-") { + state.zoom /= 1.1; + return StateUpdate::None; + } + if ui.button(vec2(40., y), "Reset") { + state.zoom = ZOOM; + state.offset = OFFSET; + return StateUpdate::None; + } + if ui.button(vec2(100., y), "L") { + state.offset += vec2(-0.1, 0.); + return StateUpdate::None; + } + if ui.button(vec2(120., y), "R") { + state.offset += vec2(0.1, 0.); + return StateUpdate::None; + } + if ui.button(vec2(140., y), "U") { + state.offset += vec2(0., 0.1); + return StateUpdate::None; + } + if ui.button(vec2(160., y), "D") { + state.offset += vec2(0., -0.1); + return StateUpdate::None; + } + + if game.can_undo() && ui.button(vec2(180., y), "Undo") { return StateUpdate::Execute(Action::Undo); } - if game.can_redo() && root_ui().button(vec2(1250., 320.), "Redo") { + if game.can_redo() && ui.button(vec2(220., y), "Redo") { return StateUpdate::Execute(Action::Redo); } if player.can_control && matches!(game.state, GameState::Playing) - && root_ui().button(vec2(1200., 350.), "End Turn") + && ui.button(vec2(270., y), "End Turn") { let left = game.actions_left; return StateUpdate::execute_with_warning( @@ -192,9 +220,34 @@ pub fn show_global_controls(game: &Game, state: &State) -> StateUpdate { ); } - if player.can_play_action && root_ui().button(vec2(1200., 30.), "Move Units") { + if player.can_play_action && ui.button(vec2(340., y), "Move") { return StateUpdate::execute(Action::Playing(PlayingAction::MoveUnits)); } + if player.can_play_action && ui.button(vec2(390., y), "Happiness") { + return start_increase_happiness(game, &player); + } + if ui.button(vec2(490., y), "Advances") { + return StateUpdate::OpenDialog(ActiveDialog::AdvanceMenu); + }; + if ui.button(vec2(560., y), "Log") { + return StateUpdate::OpenDialog(ActiveDialog::Log); + }; + let d = state.game_state_dialog(game, &ActiveDialog::None); + if !matches!(d, ActiveDialog::None) + && d.title() != state.active_dialog.title() + && ui.button(vec2(600., y), format!("Back to {}", d.title())) + { + return StateUpdate::OpenDialog(d); + } + + if features.import_export { + if ui.button(vec2(1000., y), "Import") { + return StateUpdate::Import; + }; + if ui.button(vec2(1100., y), "Export") { + return StateUpdate::Export; + }; + } StateUpdate::None }