From 194136dfc7184a34028e6bf03462a34f7da9bd49 Mon Sep 17 00:00:00 2001 From: cool-mist Date: Sun, 15 Mar 2026 12:04:48 +0530 Subject: [PATCH] --> retained mode ui --- gui/src/game.rs | 264 +++++++++++++++++-- gui/src/game/constants.rs | 1 - gui/src/game/draw.rs | 40 ++- gui/src/game/logic.rs | 419 +++++++++++-------------------- gui/src/main.rs | 36 ++- gui/src/resources.rs | 17 +- gui/src/widgets.rs | 13 +- gui/src/widgets/board.rs | 62 ++--- gui/src/widgets/button.rs | 57 +++-- gui/src/widgets/counter.rs | 89 +++++-- gui/src/widgets/id_text_input.rs | 73 ------ gui/src/widgets/label.rs | 3 +- 12 files changed, 590 insertions(+), 484 deletions(-) delete mode 100644 gui/src/widgets/id_text_input.rs diff --git a/gui/src/game.rs b/gui/src/game.rs index 5cb264c..8511b0e 100644 --- a/gui/src/game.rs +++ b/gui/src/game.rs @@ -4,32 +4,33 @@ mod logic; use std::collections::HashMap; -use crate::{resources::Resources, widgets::*}; +use crate::{ + game, + resources::{self, Resources, SoundKind}, + widgets::*, +}; use macroquad::prelude::*; -use sol_lib::generator::Puzzle; +use sol_lib::{board::BoardState, generator::Puzzle}; #[derive(Default)] pub struct Game { + resources: Resources, + // Game state - settings: GameSettings, + settings: Settings, puzzle: Puzzle, show_rules: bool, game_mode: GameMode, + game_mode_btns: HashMap, // Render state - resources: Resources, - window_height: f32, window_width: f32, - rules_font_size: f32, // TODO: remove - - generate_btn: ButtonWidget, - board: BoardWidget, - game_mode_btns: HashMap, - widgets: HashMap, + board: WidgetId, + counter_pieces: WidgetId, counter_moves: WidgetId, @@ -40,6 +41,7 @@ pub struct Game { btn_rules: WidgetId, btn_reset: WidgetId, btn_next: WidgetId, + btn_generate: WidgetId, } #[derive(Eq, PartialEq, Copy, Clone, Hash, Default)] @@ -49,16 +51,65 @@ pub enum Widget { Label(LabelWidget), Counter(CounterWidget), Button(ButtonWidget), + Board(BoardWidget), } impl Widget { - fn draw(&mut self, resources: &Resources) { + fn draw(&mut self, resources: &Resources, settings: &Settings) { match self { Widget::Label(label) => label.draw(&resources), Widget::Counter(counter) => counter.draw(&resources), Widget::Button(btn) => btn.draw(&resources), + Widget::Board(board) => board.draw(&resources, &settings), } } + + fn update( + &mut self, + interaction: &Interaction, + resources: &Resources, + settings: &Settings, + ) -> Command { + match self { + Widget::Label(_) => Command::NoInteraction, + Widget::Counter(c) => handle_counter_input(interaction, c, resources, settings), + Widget::Button(b) => handle_button_input(interaction, b, resources, settings), + Widget::Board(_) => Command::NoInteraction, + } + } +} + +fn handle_counter_input( + interaction: &Interaction, + counter: &mut CounterWidget, + resources: &Resources, + settings: &Settings, +) -> Command { + let counter_interaction = counter.handle_input(interaction, resources, settings); + let Some(interaction_type) = counter_interaction.interaction_type else { + return Command::NoInteraction; + }; + + match interaction_type { + CounterInteractionType::Increment => (counter.on_increment)(counter_interaction), + CounterInteractionType::Decrement => (counter.on_decrement)(counter_interaction), + } +} + +fn handle_button_input( + interaction: &Interaction, + button: &mut ButtonWidget, + resources: &Resources, + settings: &Settings, +) -> Command { + let clicked = button + .handle_input(interaction, resources, settings) + .clicked; + if clicked { + return (button.on_click)(button); + } + + Command::NoInteraction } macro_rules! get_widget_fn { @@ -78,6 +129,7 @@ macro_rules! get_widget_fn { get_widget_fn!(label, LabelWidget, Widget::Label); get_widget_fn!(button, ButtonWidget, Widget::Button); get_widget_fn!(counter, CounterWidget, Widget::Counter); +get_widget_fn!(board, BoardWidget, Widget::Board); macro_rules! widget_accessor { ($name:ident, $fn:ident) => { @@ -93,13 +145,14 @@ macro_rules! widget_accessor { widget_accessor!(w_button, button); widget_accessor!(w_label, label); widget_accessor!(w_counter, counter); +widget_accessor!(w_board, board); #[derive(Default)] -pub struct GameSettings { +pub struct Settings { pub volume: f32, - pub max_moves_per_piece: u32, pub debug: bool, - pub num_pieces: u32, + pub max_pieces: u32, + pub max_age: u32, } #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -115,3 +168,184 @@ impl Default for GameMode { GameMode::Medium } } + +pub enum Command { + NoInteraction, + ResetBoard, + NextPuzzle, + ToggleRules, + UpdateBoard(BoardState), + SwitchGameMode(GameMode), + UpdateMaxPieceCount(u32), + UpdateMaxAge(u32), + NewCustomPuzzle, + ToggleDebug, + HideRules, + Quit, +} + +pub async fn new_game() -> Game { + let resources = resources::init().await; + let game_mode = GameMode::Medium; + let settings = Settings { + volume: constants::VOLUME, + max_age: 3, + max_pieces: 2, + ..Default::default() + }; + let puzzle = logic::generate_new_puzzle(game_mode, &settings); + let game_mode = GameMode::Medium; + let label_heading = LabelWidget::initialize_state(constants::HEADING_TEXT); + let label_pieces = LabelWidget::initialize_state(constants::PIECES_LABEL_TEXT); + let label_max_age = LabelWidget::initialize_state(constants::AGE_LABEL_TEXT); + let show_rules = false; + + let mut widgets = HashMap::new(); + let label_pieces = add_widget(&mut widgets, Widget::Label(label_pieces)); + let label_max_age = add_widget(&mut widgets, Widget::Label(label_max_age)); + let label_heading = add_widget(&mut widgets, Widget::Label(label_heading)); + + let mut w_counter_pieces = + CounterWidget::new(2, 7, settings.max_pieces, false, UiColor::Yellow); + w_counter_pieces.on_increment = |c| Command::UpdateMaxPieceCount(c.current); + w_counter_pieces.on_decrement = |c| Command::UpdateMaxPieceCount(c.current); + let counter_pieces = add_widget(&mut widgets, Widget::Counter(w_counter_pieces)); + + let mut w_counter_moves = CounterWidget::new(1, 7, settings.max_age, false, UiColor::Yellow); + w_counter_moves.on_increment = |c| Command::UpdateMaxAge(c.current); + w_counter_moves.on_decrement = |c| Command::UpdateMaxAge(c.current); + let counter_moves = add_widget(&mut widgets, Widget::Counter(w_counter_moves)); + + let btn_reset = add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Yellow, + constants::RESET_BUTTON_TEXT, + SoundKind::Button, + )), + ); + + let btn_next = add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + false, + UiColor::Green, + constants::NEXT_BUTTON_TEXT, + SoundKind::Button, + )), + ); + + let btn_rules = add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Brown, + constants::RULES_BUTTON_TEXT, + SoundKind::Button, + )), + ); + + let btn_generate = add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + false, + UiColor::Pink, + constants::GENERATE_BUTTON_TEXT, + SoundKind::Button, + )), + ); + + let mut game_mode_btns = HashMap::new(); + game_mode_btns.insert( + GameMode::Easy, + add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Yellow, + constants::EASY_BUTTON_TEXT, + SoundKind::Mode, + )), + ), + ); + + game_mode_btns.insert( + GameMode::Medium, + add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Yellow, + constants::MEDIUM_BUTTON_TEXT, + SoundKind::Mode, + )), + ), + ); + + game_mode_btns.insert( + GameMode::Hard, + add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Yellow, + constants::HARD_BUTTON_TEXT, + SoundKind::Mode, + )), + ), + ); + + game_mode_btns.insert( + GameMode::Custom, + add_widget( + &mut widgets, + Widget::Button(ButtonWidget::new( + true, + UiColor::Blue, + constants::CUSTOM_BUTTON_TEXT, + SoundKind::Mode, + )), + ), + ); + + let active_game_mode_btn = game_mode_btns.get(&game_mode).unwrap(); + game::button(&mut widgets, *active_game_mode_btn).is_active = false; + + let board = add_widget( + &mut widgets, + Widget::Board(BoardWidget::initialize_state( + puzzle.board.size, + puzzle.board.clone(), + )), + ); + + Game { + puzzle, + board, + resources, + settings, + game_mode, + game_mode_btns, + btn_reset, + btn_next, + btn_rules, + show_rules, + widgets, + counter_pieces, + counter_moves, + btn_generate, + label_pieces, + label_max_age, + label_heading, + ..Default::default() + } +} + +fn add_widget(widgets: &mut HashMap, w: Widget) -> WidgetId { + let id = rand::rand() as u64; + let widget_id = WidgetId(id); + widgets.insert(widget_id, w); + + widget_id +} diff --git a/gui/src/game/constants.rs b/gui/src/game/constants.rs index 0a180f7..7e3ea92 100644 --- a/gui/src/game/constants.rs +++ b/gui/src/game/constants.rs @@ -42,4 +42,3 @@ pub const CUSTOM_BUTTON_TEXT: &str = "CUSTOM"; pub const GENERATE_BUTTON_TEXT: &str = "GENERATE"; pub const PIECES_LABEL_TEXT: &str = "Pieces"; pub const AGE_LABEL_TEXT: &str = " Age"; - diff --git a/gui/src/game/draw.rs b/gui/src/game/draw.rs index f716158..0217276 100644 --- a/gui/src/game/draw.rs +++ b/gui/src/game/draw.rs @@ -1,6 +1,6 @@ use super::{Game, constants}; use crate::{ - game::{self, GameMode, w_button, w_counter, w_label}, + game::{self, GameMode, w_board, w_button, w_counter, w_label}, widgets::*, }; use macroquad::{math, prelude::*}; @@ -12,12 +12,10 @@ impl Game { let min_dimension = f32::min(self.window_height, self.window_width); let square_width = constants::BOARD_SQUARE_WIDTH_MULTIPLIER * min_dimension; - let num_squares = self.board.num_squares; + let num_squares = w_board!(self, board).num_squares; let board_width = square_width * num_squares as f32; let board_x = (self.window_width * 0.6 - board_width) / 2.0; let board_y = (self.window_height - board_width) / 2.0; - let board_rect = Rect::new(board_x, board_y, board_width, board_width); - self.board.initialize_drawables(square_width, board_rect); let heading_font_size = constants::HEADING_FONT_SIZE_MULTIPLIER * min_dimension; let f = heading_font_size.floor() as u16; @@ -41,6 +39,13 @@ impl Game { &self.resources, ); + let board_rect = Rect::new(board_x, board_y, board_width, board_width); + w_board!(self, board).initialize_drawables( + square_width, + board_rect, + heading_font_size * 0.4, + ); + // Buttons let btn_h = constants::BUTTON_HEIGHT_MULTIPLIER * min_dimension; let btn_w = constants::BUTTON_WIDTH_MULTIPLIER * board_width; @@ -63,15 +68,13 @@ impl Game { )); let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w) / 2.; - w_button!(self, btn_reset).place(Rect::new( + w_button!(self, btn_next).place(Rect::new( board_x + btn_next_x_offset, bottom_row_y, btn_w, btn_h, )); - self.rules_font_size = heading_font_size; - // Right column let right_column_x = board_x + board_width + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; @@ -108,7 +111,7 @@ impl Game { // Right column 2 let right_column_2_x = right_column_x + btn_w + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; - self.generate_btn.place(Rect::new( + w_button!(self, btn_generate).place(Rect::new( right_column_2_x, board_y + board_width - square_width + (square_width - btn_h) / 2., 2. * btn_w, @@ -164,31 +167,16 @@ impl Game { pub fn draw(&mut self) { self.update_window_size(); - self.draw_board(); - - self.generate_btn.draw(&self.resources); - for (_, btn) in &self.game_mode_btns { - btn.draw(&self.resources); - } - for (_, widget) in &mut self.widgets { - widget.draw(&self.resources); + widget.draw(&self.resources, &self.settings); } self.draw_fps(); } - fn draw_board(&mut self) { - let params = BoardDrawParams { - show_rules: self.show_rules, - rules_font_size: self.rules_font_size, - }; - - self.board.draw(¶ms, &self.resources, &self.settings); - } - fn get_game_mode_button<'a>(&'a mut self, mode: &GameMode) -> &'a mut ButtonWidget { - self.game_mode_btns.get_mut(mode).unwrap() + let id = self.game_mode_btns.get(mode).unwrap(); + game::button(&mut self.widgets, *id) } fn draw_fps(&self) { diff --git a/gui/src/game/logic.rs b/gui/src/game/logic.rs index d6b72fe..40ea8aa 100644 --- a/gui/src/game/logic.rs +++ b/gui/src/game/logic.rs @@ -1,9 +1,7 @@ -use std::collections::HashMap; - -use super::{Game, GameMode, GameSettings, constants}; +use super::{Game, GameMode, Settings, constants}; use crate::{ - game::{self, Widget, WidgetId, w_button, w_counter}, - resources::{self, *}, + game::{self, Command, w_board, w_button, w_counter}, + resources::*, widgets::*, }; use macroquad::prelude::*; @@ -13,266 +11,149 @@ use sol_lib::{ }; impl Game { - pub async fn new() -> Self { - let resources = resources::init().await; - let game_mode = GameMode::Medium; - let settings = GameSettings { - volume: constants::VOLUME, - max_moves_per_piece: 3, - num_pieces: 2, - ..Default::default() - }; - let puzzle = Game::generate_puzzle(game_mode, &settings); - let board = BoardWidget::initialize_state(puzzle.board.size, puzzle.board.clone()); - let mut game_mode_btns = HashMap::new(); - let game_mode = GameMode::Medium; - game_mode_btns.insert( - GameMode::Easy, - ButtonWidget::new(true, UiColor::Yellow, constants::EASY_BUTTON_TEXT), - ); - game_mode_btns.insert( - GameMode::Medium, - ButtonWidget::new(true, UiColor::Yellow, constants::MEDIUM_BUTTON_TEXT), - ); - game_mode_btns.insert( - GameMode::Hard, - ButtonWidget::new(true, UiColor::Yellow, constants::HARD_BUTTON_TEXT), - ); - game_mode_btns.insert( - GameMode::Custom, - ButtonWidget::new(true, UiColor::Blue, constants::CUSTOM_BUTTON_TEXT), - ); - let generate_btn = ButtonWidget::new(false, UiColor::Pink, constants::GENERATE_BUTTON_TEXT); - let heading = LabelWidget::initialize_state(constants::HEADING_TEXT); - let pieces_label = LabelWidget::initialize_state(constants::PIECES_LABEL_TEXT); - let max_age_label = LabelWidget::initialize_state(constants::AGE_LABEL_TEXT); - let show_rules = false; - game_mode_btns.get_mut(&game_mode).unwrap().is_active = false; - - let mut widgets = HashMap::new(); - let label_pieces = add_widget(&mut widgets, Widget::Label(pieces_label)); - let label_max_age = add_widget(&mut widgets, Widget::Label(max_age_label)); - let label_heading = add_widget(&mut widgets, Widget::Label(heading)); - - let counter_pieces = add_widget( - &mut widgets, - Widget::Counter(CounterWidget::new( - 2, - 7, - settings.num_pieces, - false, - UiColor::Yellow, - )), - ); - - let counter_moves = add_widget( - &mut widgets, - Widget::Counter(CounterWidget::new( - 1, - 7, - settings.max_moves_per_piece, - false, - UiColor::Yellow, - )), - ); - - let btn_reset = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Yellow, - constants::RESET_BUTTON_TEXT, - )), - ); - - let btn_next = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - false, - UiColor::Green, - constants::NEXT_BUTTON_TEXT, - )), - ); - - let btn_rules = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Brown, - constants::RULES_BUTTON_TEXT, - )), - ); - - Self { - puzzle, - board, - resources, - settings, - game_mode, - game_mode_btns, - btn_reset, - btn_next, - btn_rules, - show_rules, - widgets, - counter_pieces, - counter_moves, - generate_btn, - label_pieces, - label_max_age, - label_heading, - ..Default::default() - } + pub fn update(&mut self, interaction: Interaction) { + let command = self.get_command(interaction); + self.update_state(command); } - pub fn handle_interaction(&mut self, interaction: GameInteraction) { - match interaction { - GameInteraction::NoInteraction => {} - GameInteraction::Board(new_board_state) => match new_board_state { - BoardState::Won => { - w_button!(self, btn_next).is_active = true; - } - _ => {} - }, - GameInteraction::ButtonRelease(button_name) => match button_name { - ButtonName::Reset => self.reset(), - ButtonName::Next => self.next_puzzle(), - ButtonName::Rules => { - self.show_rules = !self.show_rules; - w_button!(self, btn_rules).text = match self.show_rules { - true => constants::RULES_BUTTON_ALT_TEXT.to_string(), - false => constants::RULES_BUTTON_TEXT.to_string(), - }; - } - ButtonName::Generate => { - if self.game_mode == GameMode::Custom { - self.next_puzzle(); - } - } - }, - GameInteraction::GameMode(mode) => { - self.game_mode = mode; - for (mode, btn) in &mut self.game_mode_btns { - if *mode == self.game_mode { - btn.is_active = false; - } else { - btn.is_active = true; - } - } + fn get_command(&mut self, interaction: Interaction) -> Command { - let is_custom_game_mode = self.game_mode == GameMode::Custom; - self.generate_btn.is_active = is_custom_game_mode; - w_counter!(self, counter_pieces).set_active(is_custom_game_mode); - w_counter!(self, counter_moves).set_active(is_custom_game_mode); + for (_, widget) in &mut self.widgets { + let cmd = widget.update(&interaction, &self.resources, &self.settings); + let ret_cmd = match cmd { + Command::NoInteraction => false, + _ => true, + }; - // A change in game mode, immediately generate new puzzle - if !is_custom_game_mode { - self.next_puzzle(); - } + if ret_cmd { + return cmd; } - GameInteraction::Counter(counter_name, new_value) => match counter_name { - CounterName::Pieces => { - self.settings.num_pieces = new_value; - } - CounterName::Age => { - self.settings.max_moves_per_piece = new_value; - } - }, - GameInteraction::ToggleDebug => self.settings.debug = !self.settings.debug, - GameInteraction::HideRules => { - if self.show_rules { - play_sound_once( - &self.resources.sound(&SoundKind::Button), - self.settings.volume, - ); - self.show_rules = false; - w_button!(self, btn_rules).text = constants::RULES_BUTTON_TEXT.to_string(); - } - } - GameInteraction::Quit => std::process::exit(0), } - } - pub fn handle_user_input(&mut self) -> GameInteraction { - let (mx, my) = mouse_position(); - let winput = UserInteraction { - mouse_pos: Circle::new(mx, my, 0.0), - }; - - // WIDGETS - let board_interaction = self - .board - .handle_input(&winput, &self.resources, &self.settings); + let board_interaction = + w_board!(self, board).handle_input(&interaction, &self.resources, &self.settings); if let Some(new_board_state) = board_interaction.new_board_state { - return GameInteraction::Board(new_board_state); + return Command::UpdateBoard(new_board_state); } - if w_button!(self, btn_reset).handle_input( - &self.resources.sound(&SoundKind::Button), - self.settings.volume, - ) { - return GameInteraction::ButtonRelease(ButtonName::Reset); + if w_button!(self, btn_reset) + .handle_input(&interaction, &self.resources, &self.settings) + .clicked + { + return Command::ResetBoard; } - if w_button!(self, btn_next).handle_input( - &self.resources.sound(&SoundKind::Button), - self.settings.volume, - ) { - return GameInteraction::ButtonRelease(ButtonName::Next); + if w_button!(self, btn_next) + .handle_input(&interaction, &self.resources, &self.settings) + .clicked + { + return Command::NextPuzzle; } for (mode, btn) in &mut self.game_mode_btns { - if btn.handle_input( - &self.resources.sound(&SoundKind::Mode), - self.settings.volume, - ) { - return GameInteraction::GameMode(*mode); + if game::button(&mut self.widgets, *btn) + .handle_input(&interaction, &self.resources, &self.settings) + .clicked + { + return Command::SwitchGameMode(*mode); } } - if w_button!(self, btn_rules).handle_input( - &self.resources.sound(&SoundKind::Button), - self.settings.volume, - ) { - return GameInteraction::ButtonRelease(ButtonName::Rules); + if w_button!(self, btn_rules) + .handle_input(&interaction, &self.resources, &self.settings) + .clicked + { + return Command::ToggleRules; } - let pieces_counter_interaction = - w_counter!(self, counter_pieces).handle_input(&winput, &self.resources, &self.settings); - if pieces_counter_interaction.interacted { - return GameInteraction::Counter( - CounterName::Pieces, - pieces_counter_interaction.current, + if w_button!(self, btn_generate) + .handle_input(&interaction, &self.resources, &self.settings) + .clicked + { + return Command::NewCustomPuzzle; + } + + if interaction.keys_released.contains(&KeyCode::Escape) { + return Command::HideRules; + } + + if interaction.keys_released.contains(&KeyCode::D) { + return Command::ToggleDebug; + } + + if interaction.keys_released.contains(&KeyCode::Q) { + return Command::Quit; + } + + return Command::NoInteraction; + } + + fn update_state(&mut self, command: Command) { + match command { + Command::NoInteraction => (), + Command::UpdateBoard(new_board_state) => self.update_board(new_board_state), + Command::SwitchGameMode(mode) => self.switch_game_mode(mode), + Command::ToggleDebug => self.settings.debug = !self.settings.debug, + Command::HideRules => self.hide_rules(), + Command::ResetBoard => self.reset(), + Command::NextPuzzle => self.next_puzzle(), + Command::ToggleRules => self.toggle_rules(), + Command::UpdateMaxPieceCount(new_value) => self.settings.max_pieces = new_value, + Command::UpdateMaxAge(new_value) => self.settings.max_age = new_value, + Command::NewCustomPuzzle => self.new_custom_puzzle(), + Command::Quit => std::process::exit(0), + } + } + + fn new_custom_puzzle(&mut self) { + if self.game_mode == GameMode::Custom { + self.next_puzzle(); + } + } + + fn toggle_rules(&mut self) { + self.show_rules = !self.show_rules; + w_board!(self, board).show_rules = self.show_rules; + w_button!(self, btn_rules).text = match self.show_rules { + true => constants::RULES_BUTTON_ALT_TEXT.to_string(), + false => constants::RULES_BUTTON_TEXT.to_string(), + }; + } + + fn hide_rules(&mut self) { + if self.show_rules { + play_sound_once( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, ); + self.show_rules = false; + w_board!(self, board).show_rules = false; + w_button!(self, btn_rules).text = constants::RULES_BUTTON_TEXT.to_string(); + } + } + + fn switch_game_mode(&mut self, mode: GameMode) { + self.game_mode = mode; + for (mode, btn_id) in &mut self.game_mode_btns { + // The active mode button is 'disabled' to indicate it is active + let is_active = !(*mode == self.game_mode); + game::button(&mut self.widgets, *btn_id).is_active = is_active; } - let age_counter_interaction = - w_counter!(self, counter_moves).handle_input(&winput, &self.resources, &self.settings); - if age_counter_interaction.interacted { - return GameInteraction::Counter(CounterName::Age, age_counter_interaction.current); - } + let is_custom_game_mode = self.game_mode == GameMode::Custom; + w_button!(self, btn_generate).is_active = is_custom_game_mode; + w_counter!(self, counter_pieces).set_active(is_custom_game_mode); + w_counter!(self, counter_moves).set_active(is_custom_game_mode); + self.next_puzzle(); + } - if self.generate_btn.handle_input( - &self.resources.sound(&SoundKind::Button), - self.settings.volume, - ) { - return GameInteraction::ButtonRelease(ButtonName::Generate); + fn update_board(&mut self, new_board_state: BoardState) { + match new_board_state { + BoardState::Won => { + w_button!(self, btn_next).is_active = true; + } + _ => {} } - - // KEY INPUTS - if is_key_released(KeyCode::Escape) { - return GameInteraction::HideRules; - } - - if is_key_released(KeyCode::D) { - return GameInteraction::ToggleDebug; - } - - if is_key_released(KeyCode::Q) { - return GameInteraction::Quit; - } - - return GameInteraction::NoInteraction; } fn reset(&mut self) { @@ -281,11 +162,11 @@ impl Game { fn reset_game(&mut self, options: ResetOptions) { if options.create_new_puzzle { - self.puzzle = Game::generate_puzzle(self.game_mode, &self.settings); + self.puzzle = generate_new_puzzle(self.game_mode, &self.settings); } w_button!(self, btn_next).is_active = false; - self.board.reset(&self.puzzle); + w_board!(self, board).reset(&self.puzzle); } fn next_puzzle(&mut self) { @@ -294,39 +175,31 @@ impl Game { ..Default::default() }); } - - fn generate_puzzle(mode: GameMode, settings: &GameSettings) -> Puzzle { - let piece_count = match mode { - GameMode::Easy => 3, - GameMode::Medium => 5, - GameMode::Hard => 7, - GameMode::Custom => settings.num_pieces, - }; - - let max_moves_per_piece = match mode { - GameMode::Easy => 2, - GameMode::Medium => 2, - GameMode::Hard => 2, - GameMode::Custom => settings.max_moves_per_piece, - }; - - let generated = generator::generate_weighted_random( - piece_count, - 100, - max_moves_per_piece, - &MacroquadRandAdapter, - ); - let puzzle = generated.puzzle(); - puzzle.expect("No puzzle was generated") - } } -fn add_widget(widgets: &mut HashMap, w: Widget) -> WidgetId { - let id = rand::rand() as u64; - let widget_id = WidgetId(id); - widgets.insert(widget_id, w); +pub fn generate_new_puzzle(mode: GameMode, settings: &Settings) -> Puzzle { + let piece_count = match mode { + GameMode::Easy => 3, + GameMode::Medium => 5, + GameMode::Hard => 7, + GameMode::Custom => settings.max_pieces, + }; - widget_id + let max_moves_per_piece = match mode { + GameMode::Easy => 2, + GameMode::Medium => 2, + GameMode::Hard => 2, + GameMode::Custom => settings.max_age, + }; + + let generated = generator::generate_weighted_random( + piece_count, + 100, + max_moves_per_piece, + &MacroquadRandAdapter, + ); + let puzzle = generated.puzzle(); + puzzle.expect("No puzzle was generated") } #[derive(Default)] @@ -334,7 +207,7 @@ struct ResetOptions { create_new_puzzle: bool, } -struct MacroquadRandAdapter; +pub struct MacroquadRandAdapter; impl RandomRange for MacroquadRandAdapter { fn gen_range(&self, min: usize, max: usize) -> usize { rand::gen_range(min, max) diff --git a/gui/src/main.rs b/gui/src/main.rs index c34156f..e6104d1 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -2,12 +2,15 @@ mod game; mod resources; mod widgets; -use game::Game; +use std::collections::HashSet; + use macroquad::prelude::*; use miniquad::date; use game::constants; +use crate::widgets::Interaction; + fn window_conf() -> Conf { let window_title = { if cfg!(debug_assertions) { @@ -28,15 +31,36 @@ fn window_conf() -> Conf { async fn main() { rand::srand(date::now() as u64); let background_color = Color::from_rgba(196, 195, 208, 255); - let mut game = Game::new().await; + let mut game = game::new_game().await; loop { clear_background(background_color); - let interaction = game.handle_user_input(); - game.handle_interaction(interaction); - + game.update(get_interaction()); game.draw(); - next_frame().await + + next_frame().await; } } +fn get_interaction() -> Interaction { + let mut mouse_pressed = HashSet::new(); + let mut mouse_released = HashSet::new(); + let buttons = vec![MouseButton::Left, MouseButton::Right]; + for btn in buttons { + if is_mouse_button_pressed(btn) { + mouse_pressed.insert(btn); + } + + if is_mouse_button_released(btn) { + mouse_released.insert(btn); + } + } + + let (mx, my) = mouse_position(); + Interaction { + mouse_pos: Circle::new(mx, my, 0.), + keys_released: get_keys_released(), + mouse_pressed, + mouse_released, + } +} diff --git a/gui/src/resources.rs b/gui/src/resources.rs index cdf9615..0a707f1 100644 --- a/gui/src/resources.rs +++ b/gui/src/resources.rs @@ -68,10 +68,19 @@ pub async fn init() -> Resources { fn generate_texture_rects_pieces(active: bool) -> HashMap { let mut texture_rects_pieces = HashMap::new(); texture_rects_pieces.insert(PieceKind::Pawn, piece_texture_rect(PieceKind::Pawn, active)); - texture_rects_pieces.insert(PieceKind::Knight, piece_texture_rect(PieceKind::Knight, active)); - texture_rects_pieces.insert(PieceKind::Bishop, piece_texture_rect(PieceKind::Bishop, active)); + texture_rects_pieces.insert( + PieceKind::Knight, + piece_texture_rect(PieceKind::Knight, active), + ); + texture_rects_pieces.insert( + PieceKind::Bishop, + piece_texture_rect(PieceKind::Bishop, active), + ); texture_rects_pieces.insert(PieceKind::Rook, piece_texture_rect(PieceKind::Rook, active)); - texture_rects_pieces.insert(PieceKind::Queen, piece_texture_rect(PieceKind::Queen, active)); + texture_rects_pieces.insert( + PieceKind::Queen, + piece_texture_rect(PieceKind::Queen, active), + ); texture_rects_pieces.insert(PieceKind::King, piece_texture_rect(PieceKind::King, active)); texture_rects_pieces @@ -93,7 +102,7 @@ fn piece_texture_rect(piece: PieceKind, active: bool) -> Rect { Rect::new(index as f32 * 128.0, color as f32 * 128.0, 128.0, 128.0) } -#[derive(Eq, PartialEq, Hash)] +#[derive(Eq, Copy, Clone, PartialEq, Hash)] pub enum SoundKind { Click, Win, diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs index 9be2d0e..38bcf0b 100644 --- a/gui/src/widgets.rs +++ b/gui/src/widgets.rs @@ -1,11 +1,11 @@ mod board; +use std::collections::HashSet; + pub use board::*; mod button; pub use button::*; -mod id_text_input; - mod label; pub use label::*; @@ -18,8 +18,11 @@ use macroquad::{ }; use quad_snd::PlaySoundParams; -pub struct UserInteraction { +pub struct Interaction { pub mouse_pos: Circle, + pub keys_released: HashSet, + pub mouse_pressed: HashSet, + pub mouse_released: HashSet, } #[allow(unused)] @@ -67,10 +70,6 @@ impl UiColor { } } -pub fn draw_rect(rect: &Rect, color: UiColor) { - draw_rectangle(rect.x, rect.y, rect.w, rect.h, color.to_bg_color()); -} - pub fn draw_shadow(rect: &Rect, shadow_width: f32) { let shadow_color = Color::new(0., 0., 0., 0.8); draw_rectangle( diff --git a/gui/src/widgets/board.rs b/gui/src/widgets/board.rs index 2e23980..a21a478 100644 --- a/gui/src/widgets/board.rs +++ b/gui/src/widgets/board.rs @@ -2,11 +2,10 @@ use core::fmt; use std::fmt::{Display, Formatter}; use crate::{ - game::{GameMode, GameSettings, constants}, + game::{Settings, constants}, resources::{Resources, SoundKind}, widgets::*, }; -use macroquad::prelude::*; use sol_lib::{ board::{Board, BoardState, piece::Piece}, generator::Puzzle, @@ -17,35 +16,13 @@ pub struct BoardWidget { current_board: Board, square_width: f32, pub num_squares: usize, // this is a constant = 4 for now. + pub show_rules: bool, + rules_font_size: f32, board_rect: Rect, squares: Vec, state: GameState, } -#[derive(Copy, Clone)] -pub enum ButtonName { - Reset, - Next, - Rules, - Generate, -} - -pub enum CounterName { - Pieces, - Age, -} - -pub enum GameInteraction { - NoInteraction, - Board(BoardState), - ButtonRelease(ButtonName), - GameMode(GameMode), - Counter(CounterName, u32), - ToggleDebug, - HideRules, - Quit, -} - pub struct BoardInteraction { pub new_board_state: Option, } @@ -58,9 +35,15 @@ impl BoardWidget { board } - pub fn initialize_drawables(&mut self, square_width: f32, board_rect: Rect) { + pub fn initialize_drawables( + &mut self, + square_width: f32, + board_rect: Rect, + rules_font_size: f32, + ) { self.square_width = square_width; self.board_rect = board_rect; + self.rules_font_size = rules_font_size; let dark = UiColor::Brown.to_bg_color(); let light = UiColor::Yellow.to_bg_color(); @@ -90,11 +73,11 @@ impl BoardWidget { self.squares = rects; } - pub fn draw(&self, params: &BoardDrawParams, resources: &Resources, settings: &GameSettings) { + pub fn draw(&self, resources: &Resources, settings: &Settings) { let board_shadow_width = constants::BOARD_SHADOW_MULTIPLIER * self.square_width; draw_shadow(&self.board_rect, board_shadow_width); - if params.show_rules { + if self.show_rules { draw_rectangle( self.board_rect.x, self.board_rect.y, @@ -103,16 +86,20 @@ impl BoardWidget { UiColor::Yellow.to_bg_color(), ); - let font_size = params.rules_font_size * 0.4; let rules = "\ Every move should be a \n\ capture. Win when only \n\ one piece is left.\n\ Age: Each piece can only \n\ move 'age' times"; - let measurement = measure_text(rules, Some(resources.font()), font_size as u16, 1.0); + let measurement = measure_text( + rules, + Some(resources.font()), + self.rules_font_size as u16, + 1.0, + ); let draw_text_params = TextParams { - font_size: font_size as u16, + font_size: self.rules_font_size as u16, font: Some(resources.font()), color: UiColor::Brown.to_bg_color(), ..Default::default() @@ -214,9 +201,9 @@ impl BoardWidget { pub fn handle_input( &mut self, - interaction: &UserInteraction, + interaction: &Interaction, resources: &Resources, - settings: &GameSettings, + settings: &Settings, ) -> BoardInteraction { let mouse_pos = interaction.mouse_pos; if mouse_pos.overlaps_rect(&self.board_rect) == false { @@ -315,7 +302,7 @@ impl BoardWidget { mouse_pos: Circle, source: (usize, usize), resources: &Resources, - settings: &GameSettings, + settings: &Settings, ) -> GameState { let mut selected = None; for square in &mut self.squares { @@ -444,8 +431,3 @@ struct PieceDrawTextureParams<'a> { texture: &'a Texture2D, draw_text_params: DrawTextureParams, } - -pub struct BoardDrawParams { - pub show_rules: bool, - pub rules_font_size: f32, -} diff --git a/gui/src/widgets/button.rs b/gui/src/widgets/button.rs index 63a6228..d351f45 100644 --- a/gui/src/widgets/button.rs +++ b/gui/src/widgets/button.rs @@ -1,5 +1,8 @@ -use crate::{resources::Resources, widgets::*}; -use macroquad::{audio::Sound, prelude::*}; +use crate::{ + game::{Command, Settings}, + resources::{Resources, SoundKind}, + widgets::*, +}; pub struct ButtonWidget { pub is_active: bool, @@ -8,15 +11,32 @@ pub struct ButtonWidget { is_down: bool, rect: Rect, shadow_width: f32, + sound: SoundKind, + pub on_click: fn(&mut ButtonWidget) -> Command, +} + +pub struct ButtonInteraction { + pub clicked: bool, +} + +impl ButtonInteraction { + fn not_clicked() -> Self { + Self { clicked: false } + } + + fn clicked() -> Self { + Self { clicked: true } + } } impl ButtonWidget { - pub fn new(is_active: bool, color: UiColor, text: impl Into) -> Self { + pub fn new(is_active: bool, color: UiColor, text: impl Into, sound: SoundKind) -> Self { Self { is_active, text: text.into(), color, shadow_width: 5.0, + sound, ..Default::default() } } @@ -105,33 +125,38 @@ impl ButtonWidget { button_pressed_correction } - pub fn handle_input(&mut self, click_sound: &Sound, volume: f32) -> bool { + pub fn handle_input( + &mut self, + interaction: &Interaction, + resources: &Resources, + settings: &Settings, + ) -> ButtonInteraction { if !self.is_active { self.is_down = false; - return false; + return ButtonInteraction::not_clicked(); } - let (mx, my) = mouse_position(); - let c = Circle::new(mx, my, 0.0); - - if is_mouse_button_pressed(MouseButton::Left) { - if c.overlaps_rect(&self.rect) { + let mouse = interaction.mouse_pos; + if interaction.mouse_pressed.contains(&MouseButton::Left) { + if mouse.overlaps_rect(&self.rect) { self.is_down = true; - return false; + return ButtonInteraction::not_clicked(); } } - if is_mouse_button_released(MouseButton::Left) { - if c.overlaps_rect(&self.rect) { + if interaction.mouse_released.contains(&MouseButton::Left) { + if mouse.overlaps_rect(&self.rect) { + let click_sound = resources.sound(&self.sound); + let volume = settings.volume; play_sound_once(&click_sound, volume); self.is_down = false; - return true; + return ButtonInteraction::clicked(); } self.is_down = false; } - false + ButtonInteraction::not_clicked() } } @@ -144,6 +169,8 @@ impl Default for ButtonWidget { shadow_width: Default::default(), color: UiColor::Grey, text: String::new(), + sound: SoundKind::Click, + on_click: |_| Command::NoInteraction, } } } diff --git a/gui/src/widgets/counter.rs b/gui/src/widgets/counter.rs index d421a16..60adc21 100644 --- a/gui/src/widgets/counter.rs +++ b/gui/src/widgets/counter.rs @@ -1,15 +1,16 @@ use crate::{ - game::GameSettings, + game::{Command, Settings}, resources::{Resources, SoundKind}, widgets::*, }; use macroquad::prelude::*; -#[derive(Default)] pub struct CounterWidget { visual_rect: Rect, increment: ButtonWidget, decrement: ButtonWidget, + pub on_increment: fn(CounterWidgetInteraction) -> Command, + pub on_decrement: fn(CounterWidgetInteraction) -> Command, min: u32, max: u32, current: u32, @@ -22,19 +23,43 @@ pub struct CounterWidget { pub struct CounterWidgetInteraction { pub current: u32, - pub interacted: bool, + pub interaction_type: Option, +} + +#[derive(Copy, Clone)] +pub enum CounterInteractionType { + Increment, + Decrement, +} + +impl CounterWidgetInteraction { + fn decrement(current: u32) -> Self { + Self { + current, + interaction_type: Some(CounterInteractionType::Decrement), + } + } + + fn increment(current: u32) -> Self { + Self { + current, + interaction_type: Some(CounterInteractionType::Increment), + } + } + + fn not_interacted(current: u32) -> Self { + Self { + current, + interaction_type: None, + } + } } impl CounterWidget { - pub fn new( - min: u32, - max: u32, - current: u32, - is_active: bool, - color: UiColor, - ) -> Self { - let increment = ButtonWidget::new(is_active, color, "+"); - let decrement = ButtonWidget::new(is_active, color, "-"); + pub fn new(min: u32, max: u32, current: u32, is_active: bool, color: UiColor) -> Self { + let increment = ButtonWidget::new(is_active, color, "+", SoundKind::Click); + let decrement = ButtonWidget::new(is_active, color, "-", SoundKind::Click); + Self { min, max, @@ -83,35 +108,33 @@ impl CounterWidget { pub fn handle_input( &mut self, - _: &UserInteraction, + interaction: &Interaction, resources: &Resources, - settings: &GameSettings, + settings: &Settings, ) -> CounterWidgetInteraction { - let mut interacted = false; if self .increment - .handle_input(resources.sound(&SoundKind::Click), settings.volume) + .handle_input(&interaction, &resources, &settings) + .clicked { if self.current < self.max { self.current += 1; - interacted = true; + return CounterWidgetInteraction::increment(self.current); } } if self .decrement - .handle_input(resources.sound(&SoundKind::Click), settings.volume) + .handle_input(&interaction, &resources, &settings) + .clicked { if self.current > self.min { self.current -= 1; - interacted = true; + return CounterWidgetInteraction::decrement(self.current); } } - return CounterWidgetInteraction { - current: self.current, - interacted, - }; + return CounterWidgetInteraction::not_interacted(self.current); } pub fn draw(&mut self, resources: &Resources) { @@ -149,3 +172,23 @@ impl CounterWidget { ); } } + +impl Default for CounterWidget { + fn default() -> Self { + Self { + on_increment: |_| Command::NoInteraction, + on_decrement: |_| Command::NoInteraction, + visual_rect: Default::default(), + increment: Default::default(), + decrement: Default::default(), + min: Default::default(), + max: Default::default(), + current: Default::default(), + font_size: Default::default(), + visual_dims: Default::default(), + increment_dims: Default::default(), + decrement_dims: Default::default(), + is_active: Default::default(), + } + } +} diff --git a/gui/src/widgets/id_text_input.rs b/gui/src/widgets/id_text_input.rs deleted file mode 100644 index effa5e8..0000000 --- a/gui/src/widgets/id_text_input.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[allow(dead_code)] - -use crate::{resources::Resources, widgets::*}; -use macroquad::prelude::*; - -#[derive(Default)] -pub struct IdTextInput { - rect: Rect, - text_rect: Rect, - copy_box_rect: Rect, - paste_box_rect: Rect, -} - -pub struct IdTextInputDrawParams<'a> { - pub text: &'a str, - pub color: UiColor, - pub copy_color: UiColor, - pub paste_color: UiColor, - pub shadow_width: f32, -} - -impl IdTextInput { - pub fn initialize_state() -> Self { - Default::default() - } - - pub fn initialize_drawables(&mut self, rect: Rect) { - let copy_button_w = 0.25 * rect.w; - let paste_button_h = 0.5 * rect.h; - self.rect = rect; - self.text_rect = Rect::new(rect.x, rect.y, rect.w - copy_button_w, rect.h); - self.copy_box_rect = Rect::new( - rect.x + (rect.w - copy_button_w), - rect.y, - copy_button_w, - paste_button_h, - ); - self.paste_box_rect = Rect::new( - rect.x + (rect.w - copy_button_w), - rect.y + paste_button_h, - copy_button_w, - paste_button_h, - ); - } - - pub fn draw(&self, params: &IdTextInputDrawParams, resources: &Resources) { - draw_rect(&self.text_rect, params.color); - draw_rect(&self.copy_box_rect, params.copy_color); - draw_rect(&self.paste_box_rect, params.paste_color); - draw_shadow(&self.rect, params.shadow_width); - - let font = resources.font(); - let font_size = (0.15 * self.text_rect.w) as u16; - let text_dims = measure_text(params.text, Some(font), font_size, 1.0); - draw_text_ex( - params.text, - self.rect.x + (self.text_rect.w - text_dims.width) / 2., - self.rect.y + (self.text_rect.h - text_dims.height) / 2. + text_dims.offset_y, - TextParams { - font: Some(font), - font_size: font_size, - color: params.color.to_fg_color(), - ..Default::default() - }, - ); - } - - pub fn handle_input(&self, _: &UserInteraction) -> IdTextInputInteraction { - IdTextInputInteraction {} - } -} - -pub struct IdTextInputInteraction {} diff --git a/gui/src/widgets/label.rs b/gui/src/widgets/label.rs index 7223350..2643714 100644 --- a/gui/src/widgets/label.rs +++ b/gui/src/widgets/label.rs @@ -21,7 +21,8 @@ impl LabelWidget { pub fn initialize_drawables(&mut self, rect: Rect, font_size: f32, resources: &Resources) { self.rect = rect; self.font_size = font_size; - self.text_dimensions = measure_text(&self.text, Some(resources.font()), font_size as u16, 1.0); + self.text_dimensions = + measure_text(&self.text, Some(resources.font()), font_size as u16, 1.0); } pub fn draw(&self, resources: &Resources) {